07 volatile不保证原子性问题解决
代码
package volatile1; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; class MyData{//MyData.java ===> MyData.class ===> JVM字节码 volatile int number=0; public void addTo60(){ this.number=60; } //请注意,此时number前面是加了volatile关键字修饰的。 public void addPlusPlus(){ number++; } AtomicInteger atomicInteger=new AtomicInteger(); public void addMyAtomic(){ atomicInteger.getAndIncrement(); } } /** * 1 验证volatile的可见性 * 1.1 假如 int number =0; number变量之前根本没有添加volatile关键字修饰,没有可见性 * 1.2 添加了volatile,可以解决可见性问题 * * 2 验证volatile不保证原子性 * 2.1 原子性是指的是什么意思? * 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整 * 要么同时成功,要么同时失败 * 2.2 volatile不保证原子性 * * 2.3 why * number++;并非原子操作 * * 2.4 如何解决原子性? * * 加sync * * 使用我们的juc下AtomicInteger * */ public class VolatileDemo { public static void main(String[] args) {//main是一切方法的运行入口 MyData myData=new MyData(); //forthread10 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { myData.addPlusPlus(); myData.addMyAtomic(); } },String.valueOf(i)).start(); } while (Thread.activeCount()>2){//GC Main Thread.yield(); } System.out.println(Thread.currentThread().getName()+"\t int type , finally number value:"+myData.number); System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type , finally number value:"+myData.atomicInteger); } }
结果
main int type , finally number value:19499 main AtomicInteger type , finally number value:20000
2.3 VolatileDemo代码演示可见性+原子性代码
package volatile1; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; class MyData{//MyData.java ===> MyData.class ===> JVM字节码 volatile int number=0; public void addTo60(){ this.number=60; } //请注意,此时number前面是加了volatile关键字修饰的。 public void addPlusPlus(){ number++; } AtomicInteger atomicInteger=new AtomicInteger(); public void addMyAtomic(){ atomicInteger.getAndIncrement(); } } /** * 1 验证volatile的可见性 * 1.1 假如 int number =0; number变量之前根本没有添加volatile关键字修饰,没有可见性 * 1.2 添加了volatile,可以解决可见性问题 * * 2 验证volatile不保证原子性 * 2.1 原子性是指的是什么意思? * 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整 * 要么同时成功,要么同时失败 * 2.2 volatile不保证原子性 * * 2.3 why * number++;并非原子操作 * * 2.4 如何解决原子性? * * 加sync * * 使用我们的juc下AtomicInteger */ public class VolatileDemo { public static void main(String[] args) {//main是一切方法的运行入口 MyData myData=new MyData(); //forthread10 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { myData.addPlusPlus(); myData.addMyAtomic(); } },String.valueOf(i)).start(); } //需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值看是多少 // try{ // TimeUnit.SECONDS.sleep(5); // }catch (InterruptedException e){ // e.printStackTrace(); // } while (Thread.activeCount()>2){//GC Main Thread.yield(); } System.out.println(Thread.currentThread().getName()+"\t int type , finally number value:"+myData.number); System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type , finally number value:"+myData.atomicInteger); } //volatile可以保证可见性,及时通知其他线程,物理内存的值已经被修改。 public static void seeOKByVolatile() { MyData myData=new MyData();//资源类 new Thread(()->{ System.out.println(Thread.currentThread().getName()+"\t come in"); //暂停一会儿线程 try{ TimeUnit.SECONDS.sleep(3); }catch (InterruptedException e){ e.printStackTrace(); } myData.addTo60(); System.out.println(Thread.currentThread().getName()+"\t updated number value:"+myData.number); },"AAA").start(); //第二个线程就是我们的main线程 while (myData.number==0){ //main线程就一直等待循环,直到number的值不再等于0。 } System.out.println(Thread.currentThread().getName()+"\t mission is over,main get number value:"+myData.number); } }
2.4 有序性
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
类似:进程的特征——并发性和异步性
并发执行 —— 间断性 + 失去封闭性 + 不可再现性 ——决定了通常的程序是不能参与并发执行的
为了能使程序并发执行,并且可以对并发执行的程序加以控制和描述 —— 进程
第二章 进程的描述与控制【操作系统】
08 volatile指令重排案例1
重排1
public void mySort() int x = 11; //语句1 int y = 12; //语句2 x= x + 5; //语句3 y = x * x; //语司4
1234
2134
1324
问题:请问语句4可以重排后变成第一个条吗?
不能,语句4必须在1和2后面,数据依赖性
操作系统:前驱图
1 2 – 4和1 – 3
重排2
09 volatile指令重排案例2
案例
public class ReSortSeqDemo int a = 0; boolean flag = false; public void method01(){ a = 1; //语句1 flag = true; //语句2 } //多线程环境中线程交替执行,由于编译器优化重排的存在, //两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测 public void method02(){ if(f1ag){ a= a+ 5; //语句3 System.out.println( "*****retValue: "+a); } } }
禁止指令重排小总结(了解)
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行顺序,
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
线程安全性获得保证
工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。
3. 你在哪些地方用到过volatile ?
10 单例模式在多线程环境下可能存在安全问题
代码
package volatile1; public class SingletonDemo { private static SingletonDemo instance=null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo()"); } public static SingletonDemo getInstance(){ if (instance==null){ instance=new SingletonDemo(); } return instance; } public static void main(String[] args) { //单线程(main线程的操作动作......) // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance()); // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance()); // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance()); //并发多线程后,情况发生了很大的变化 //forthread10 for (int i = 1; i <= 10; i++) { new Thread(()->{ SingletonDemo.getInstance(); },String.valueOf(i)).start(); } } }
结果
1 我是构造方法SingletonDemo() 5 我是构造方法SingletonDemo() 7 我是构造方法SingletonDemo() 4 我是构造方法SingletonDemo() 3 我是构造方法SingletonDemo() 2 我是构造方法SingletonDemo()
修改
public static synchronized SingletonDemo getInstance(){ if (instance==null){ instance=new SingletonDemo(); } return instance; }
锁粒度太粗了
synchronized
仅需作用于 instance=new SingletonDemo();
即可
3.1 单例模式DCL代码
package volatile1; public class SingletonDemo { private static volatile SingletonDemo instance=null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo()"); } //DCL(Double Check Lock双端解锁机制) public static synchronized SingletonDemo getInstance(){ if (instance==null){ synchronized (SingletonDemo.class){ if (instance==null){ instance=new SingletonDemo(); } } } return instance; } public static void main(String[] args) { //单线程(main线程的操作动作......) // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance()); // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance()); // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance()); //并发多线程后,情况发生了很大的变化 //forthread10 for (int i = 1; i <= 10; i++) { new Thread(()->{ SingletonDemo.getInstance(); },String.valueOf(i)).start(); } } }
11 单例模式volatile分析
3.2 单例模式volatile分析
DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
instance = new SingletonDemo();可以分为以下3步完成(伪代码)
memory = allocate(); //1.分配对象内存空间 instance(memory); //2.初始化对象 instance = memory; //3.设置instance指向刚分配的内存地址,此时instance! =null
步骤2和步骤3 不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //1.分配对象内存空间 instance = memory; //3.设置instance指向刚分配的内存地址,此时instance! =null,但是对象还没有初始化完成! instance(memory); //2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为nulli时,由于instance实例化未必己初始化完成,也就造成了线程安全问题。
理解不加volatile
线程A | 线程B |
1 if(instance=null) | |
2 if(instance=null) | |
memory = allocate(); | |
instance = memory;//此时A还未初始化,为null |
1 if(instance=null)//判断失败 |
|
return instance;//此时B就是null | |
instance(memory); //A初始化 |
这样就造成了线程的安全性问题
理解加volatile
线程A | 线程B |
1 if(instance=null) | |
2 if(instance=null) | |
memory = allocate(); |
instance(memory); //A初始化 | |
instance = memory;//此时A已初始化,不为null |
1 if(instance=null)//判断失败 |
|
return instance;//此时B不是null |
最后
2022 10/3 17:09
p2~p11
Markdown 14141 字数 905 行数
HTML 13219 字数 587 段落