场景
volatile这个在多线程使用时能保证线程间的可见性。具体怎么用呢?举个例子:
public class VolatileVisibilityTest { private static boolean initFlag = false; //private static volatile boolean initFlag = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { System.out.println("waiting data"); while (!initFlag) { } System.out.println("========success!!!"); }).start(); Thread.sleep(2000); new Thread(() -> prepareData()).start(); } private static void prepareData() { System.out.println("开始修改initFlag..."); initFlag = true; System.out.println("initFlag修改成功..."); } }
如上面的代码,结果是什么呢?会不会打印 “========success!!!”。我们来看结果
"C:\Program Files\Java\jdk1.8.0_212\bin\java.exe" ...waiting data开始修改initFlag...initFlag修改成功...
程序一直在whlie循环中,如果想要走出while循环,需要对initFlag 添加Volatile关键字修饰。
"C:\Program Files\Java\jdk1.8.0_212\bin\java.exe" ...waiting data开始修改initFlag...initFlag修改成功...========success!!!
如上结果,加了Volatile后会打印 “========success!!!”。
JMM线程模型
如上图,线程获取数据是先从主内存中获取,然后将数据拷贝到工作内存。如果A线程改变了共享变量,此时只是在工作内存中改变了数据。对于其他线程来说是不可见的。
那共享变量加了关键字后为什么会可见了呢?
在解读上面图之前我们先来解释下几个指令操作。
read(读取):从主内存读取数据load(载入):将主内存读取到的数据写入工作内存use(使用):从工作内存读取数据来计算assign(赋值):将计算好的值重新赋值到工作内存中store(存储):将工作内存数据写入主内存write(写入):将store过去的变量值赋值给主内存中的变量lock(锁定):将主内存变量加锁,标识为线程独占状态unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
volatile保证可见性
如上图:线程1和线程2去主内存中拿(通过read指令)数据initFlag并将其放在(通过load指令)各自的工作内存中,此时initFlag的值为false。当线程2要修改initFlag的值为true,需要经历下面几步:
1)将工作内存中的initFlag 值加载(use指令)到线程2的执行引擎中
2)执行引擎将initFlag的值改为true
3) 将修改的值assign到工作内存中
4)将工作内存中的值store到主内存中
5)将主内存的变量赋值(通过write命令)
在第4步store时,数据会经过cpu总线,这时线程1会嗅探到值的变化。就会从主内存中获取新值。等等,这里可能你会有疑问,如果新值经过总线还没到达主内存中,这时线程1就去主内存中获取值,还是以前的旧值啊。那系统是怎么解决的呢。
当心智经过总线时,会上一把锁(lock),线程1是不能去主内存中获取值的,当执行了第5步,释放了锁(unlock),线程1才能去主内存中获取。
当线程1嗅探到值有变化,会让自己工作内存中的initFlag失效。然后线程1就执行下面步骤:
1)从主内存中read新值
2)将新值load进工作内存中
3)将工作内存中的变量赋新值
4)将新值use进线程1的执行引擎中
到这里变量initFlag就实现了可见性。
volatile不保证原子性
注意:volatile并不能保证原子性
public class VolatileAtomicTest { public static volatile int num = 0; public static void increase() { num++; } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { increase(); } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println(num); } }
上面的输出会是10000么,实际输出为:9985。为什么会这样呢
原因是虽然num在线程间是可见的,但是数据从线程的工作内存同步到主内存是需要时间的,这时其他线程自己有自己的计算,导致结果不可预测。num++经历了两步:a = num + 1;num = a。这两步不能保证原子性。如果想保证原子性,需要对increase()方法加锁。
volatile禁止指令重排
public class VolatileSerialTest { static int x = 0, y = 0; public static void main(String[] args) throws InterruptedException { Set<String> resultSet = new HashSet<>(); Map<String, Integer> resultMap = new HashMap<>(); for (int i = 0; i < 1000000; i++) { x = 0; y = 0; resultMap.clear(); Thread one = new Thread(() -> { int a = y; x = 1; resultMap.put("a", a); }); Thread other = new Thread(() -> { int b = x; y = 1; resultMap.put("b", b); }); one.start(); other.start(); one.join(); other.join(); resultSet.add("a=" + resultMap.get("a") + "," + "b=" + resultMap.get("b")); System.out.println(resultSet); } } }
上面代码你觉得会输出什么呢?
答案是:
[a=0,b=1][a=1,b=0][a=1,b=1] [a=0,b=0] //不可思意吧
第四种输出不可思议吧,按照程序逻辑。是不可能出现[a=0,b=0]的情况的。其实CPU会对代码的执行顺序进行优化,及指令重排。
指令重排可以提高CPU处理速度。
如果给x,y加上volatile修饰,则不会出现指令重排,[a=0,b=0]就不会出现。
我们在编写懒汉式的单列模式时,也需要给对象加volatile修饰。
public class LazySimpleSingleton { private static volatile LazySimpleSingleton instance = null; //需要volatile修饰 private LazySimpleSingleton(){ } public static LazySimpleSingleton getInstance() { if (instance == null) { synchronized (LazySimpleSingleton.class) { if (instance == null) { instance = new LazySimpleSingleton(); } } } return instance; } }
不加volatile,由于存在指令重排。线程A会出现b,c顺序颠倒情况。线程B进去获得实例可能未完全初始化。这时线程C执行方法getInstance(),返回的对象是未完全初始化的值。
我写出这样干净的代码,老板直夸我
云南丽江旅游攻略
使用ThreadLocal怕内存泄漏?
Java进阶之路思维导图
程序员必看书籍推荐
3万字的Java后端面试总结(附PDF)
扫码二维码,获取更多精彩。或微信搜Lvshen_9,可后台回复获取资料
1.回复"java" 获取java电子书; 2.回复"python"获取python电子书; 3.回复"算法"获取算法电子书; 4.回复"大数据"获取大数据电子书; 5.回复"spring"获取SpringBoot的学习视频。 6.回复"面试"获取一线大厂面试资料 7.回复"进阶之路"获取Java进阶之路的思维导图 8.回复"手册"获取阿里巴巴Java开发手册(嵩山终极版) 9.回复"总结"获取Java后端面试经验总结PDF版 10.回复"Redis"获取Redis命令手册,和Redis专项面试习题(PDF) 11.回复"并发导图"获取Java并发编程思维导图(xmind终极版)另:点击【我的福利】有更多惊喜哦。