并发编程之volatile
一、Java内存模型内存交互操作
1、lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
2、unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
3、read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
4、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
5、use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
6、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
7、store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
8、write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
整个执行流程如图
read ---load store----writr必须成对执行
通过上面分析我们可以看出即使在java里面执行i++这样的操作,对于我们的底层来说也不是原子操作,因为i++,也需要将这八大操作走一遍,具体来说,read ---load 将主内存中i=0在工作内存中也copy一份,
线程读到工作内存中的i=0并加1操作即结果i=1写回工作内存(use---assign),然后将i=1写回主内存(store----writrt)这一步如果没有用缓存一致性协议,会有延时不会立即写到主内存,参考第一篇缓存一执行性协议讲解。
二、volatile原理与内存语义
volatile是Java虚拟机提供的轻量级的同步机制
volatile语义有如下两个作用
可见性:保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
有序性:禁止指令重排序优化。
volatile缓存可见性实现原理
JMM内存交互层面:volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步会主内存,使用时必须从主内存刷新,由此保证volatile变量的可见性。 底层实现:通过汇编lock前缀指令,它会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存内存会导致其他处理器的缓存无效
三、volatile可见性分析
先上一段代码:
public class VolatileVisibilitySample { private boolean initFlag = false; static Object object = new Object(); public void refresh(){ this.initFlag = true; //普通写操作,(volatile写) String threadname = Thread.currentThread().getName(); System.out.println("线程:"+threadname+":修改共享变量initFlag"); } public void load(){ String threadname = Thread.currentThread().getName(); int i = 0; while (!initFlag){ } System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i); } public static void main(String[] args){ VolatileVisibilitySample sample = new VolatileVisibilitySample(); Thread threadA = new Thread(()->{ sample.refresh(); },"threadA"); Thread threadB = new Thread(()->{ sample.load(); },"threadB"); threadB.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } threadA.start(); } }
代码很好理解,线程B读取成员变量initFlag 如果为false无线循环,如果为true,打出表示语,线程A负责将initFlag改为true,线程B先启动,线程A启动修改标志为true后,看看线程B能否感知到并终止循环
测试结果 :线程B无线循环,未能感知到标志被线程A修改,原因,线程B一直读的是工作空间的缓存数据,当线程A修改数据之后,线程B未能感知到.
降上诉代码修改,线程B的执行任务上加锁synchronized:
public void load(){ String threadname = Thread.currentThread().getName(); int i = 0; while (!initFlag){ synchronized (object){ i++; } } System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i); }
测试结果:加锁会导致线程B失去cpu执行权,当再次获取cpu执行权时,会引起线程上下文切换,这个过程会引起重新读取主内存数据。
volatile关键字测试
initFlag用volatile修饰后
private volatile boolean initFlag = false;
测试结果:当线程A修改initFlag后线程B能立即感知到,停止循环打出标志语;
原因:线程A修改initFlag,由于initFlag被volatile修饰,会立即从工作内存刷到主内存,同时让其他线程中工作内存中initFlag数据缓存失效,这样线程B中原来地缓存失效,从主内存中重新读取新值。
四、volatile不能保证原子性
先来一段代码:
public class VolatileAtomicSample { private static volatile int counter = 0; public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(()->{ for (int j = 0; j < 1000; j++) { counter++; //不是一个原子操作,第一轮循环结果是没有刷入主存,这一轮循环已经无效 //1 load counter 到工作内存 //2 add counter 执行自加 //其他的代码段? } }); thread.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } }
开10个线程每个线程对counter进行1000次+最总我们地结果也不是10000,而是小于10000,
原因:couter++并不是原子操作,比如两个线程读到counter=0都读到自己地工作内存,然后加1之后都要往我们地主内存写,这时候必然引起裁决,导致一个线程的+1有效果,一个线程的+1无效果,最后导致
两个线程一共加了两次1,只有一个有效,最后结果比预期结果小。
五、volatile保证有序性防止指令重排
有序性问题
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述volatile关键字)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
指令从排序发生在编译重排序和处理器重排序,禁止指令重排序的底层就是内存屏障,内存屏障分为4种
1、StoreStore 2、StoreLoad 3、LoadLoad 4、LoadStore
public void run() { //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间. shortWait(10000); a = 1; //是读还是写?store,volatile写 //storeload ,读写屏障,不允许volatile写与第二部volatile读发生重排 //手动加内存屏障 UnsafeInstance.reflectGetUnsafe().storeFence(); x = b; // 读还是写?读写都有,先读volatile,写普通变量 //分两步进行,第一步先volatile读,第二步再普通写 } });
六、总线风暴问题
大量使用volatile会引起工作缓存有大量的无效缓存,而且volatile会一起会引起线程之间相互监听,嗅探,这些都会占用总线资源,导致总线资源负载过高。这时候我们需要锁来解决问题,这就是为什么有了
volatile我们还需要synchronized,lock锁,因为volatile保证不了原子操作,且用的过多会导致总线风暴。
七、volatile,synchronized同时使用-----一个超高并发的单例场景
public class Singleton { /** * 查看汇编指令 * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp */ private volatile static Singleton myinstance; public static Singleton getInstance() { if (myinstance == null) { synchronized (Singleton.class) { if (myinstance == null) { myinstance = new Singleton();//对象创建过程,本质可以分文三步 //对象延迟初始化 // } } } return myinstance; } public static void main(String[] args) { Singleton.getInstance(); } }
解释:创建对象myinstance = new Singleton() 并不时一个原子操作,它可以分为三部,1、申请空间,2,实力化对象,3,地址赋值给myinstance 变量,加synchronized 保证了原子操作,但是无法防止指令重排,线程1申请完空间之后如果发生指令重排直接执行第3步赋值,那么线程2执行if判断时myinstance 不为空但是却没有实例化对象。这是指令重排导致的,所以volatile 修饰myinstance防止发生指令重排。----超高并发下的应用。