Java多线程是一个庞大的知识体系,这里对其中的volatile进行一个总结,理清他的来龙去脉。
CPU缓存
要搞懂volatile,首先得了解CPU在运行过程中的存储是如何处理的,其结构如图
CPU会把一些经常使用的数据缓存在cache中,避免每次都去访问较慢的memory。在单线程环境下,如果一个变量的修改都在cache中,自然不会有什么问题,可是在多线程环境中就可能是下面这个图的示意图(单核另当别论)
CPU1 修改了一个变量a存入cache1,但是CPU2 在cache2中看到的a任然是之前的a,所以造成CPU1修改失效,我们来看看示例代码:
import java.util.concurrent.TimeUnit;public class Counter { private static boolean stop ; //private static volatile boolean stop ; public static void main(String[] args) throws Exception { Thread t = new Thread(new Runnable() { @Override public void run() { int i = 0; while (!stop) { i++; } } } ); t.start(); TimeUnit.MILLISECONDS .sleep(5); stop = true; }}
在我的4核笔记本上运行结果:
就一直运行着,没有停止(需要手工停止),这说明在主线程中修改的stop变量后,线程t没有读取到最新的stop的值,还一直是false。
volatile原理
volatile的原理就是,如果CPU1修改了一个变量a,不仅要修改自身的cache,还要同步到memory中去,并且使CPU2的cache中的变量a失效,如果CPU2要读取a,那么就必须到memory中去读取,这样就保证了不同的线程之间对于a的可见性,亦即,无论哪个线程,随时都能获得变量a最新的最新值。
我们来看看示例代码:import java.util.concurrent.TimeUnit;public class Counter { //private static boolean stop ; private static volatile boolean stop ; public static void main(String[] args) throws Exception { Thread t = new Thread(new Runnable() { @Override public void run() { int i = 0; while (!stop) { i++; } } } ); t.start(); TimeUnit.MILLISECONDS .sleep(5); stop = true; }}
在我的4核笔记本上运行结果:
很快程序就结束了,说明线程t读到了经主线程修改后的stop变量,然后就停止了。
(例子源于《effective Java》)
volatile使用场景
状态标志
就像上面的代码里,把简单地volatile变量作为状态标志,来达成线程之间通讯的目的,省去了用synchronized还要wait,notify或者interrupt的编码麻烦。
替换重量级锁
在Java中synchronized 又称为重量级锁,能够保重JMM的几大特性:一致性,原子性,可见性。但是由于使用了锁操作,在一定程度上会有更高的性能消耗(锁的线程互斥性亦即资源消耗)。而volatile能提供可见性,原子性(单个变量操作,不是a++这种符合操作),所以在读写上,可以用volatile来替换synchronized的读操作,而写操作仍然有synchronized实现,能取得更好的性能。
import java.util.ArrayList;import java.util.List;public class Counter1 { private class Count11 { private int value; public synchronized int getValue() { return value; } public synchronized int increment() { return value++; } }// private class Count11 {// private volatile int value=0;// int getValue() { return value; }// synchronized int increment() { return value++; }// } public static void main(String[] args) throws Exception { Counter1.Count11 count11 = new Counter1().new Count11(); ListthreadArrayList = new ArrayList<>(); final int[] a = {0}; Long allTime = 0l; long startTime = System.currentTimeMillis(); for (int i = 0; i < 4; i++) { Thread t = new Thread(() -> { int b = 0; for (int j = 0; j < 10000; j++) { count11.increment(); a[0] = count11.getValue(); } for (int j = 0; j < 10000; j++) { b++; a[0] = count11.getValue(); } }); t.start(); threadArrayList.add(t); } for (Thread t : threadArrayList) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } long endTime = System.currentTimeMillis(); allTime = ((endTime - startTime)); System.out.println("result: " + a[0] + ", average time: " + (allTime) + "ms"); }}
volatile优化结果:
result: 40000, average time: 124msresult: 40000, average time: 133msresult: 40000, average time: 141msresult: 40000, average time: 112msresult: 40000, average time: 123msresult: 40000, average time: 143msresult: 40000, average time: 120msresult: 40000, average time: 120ms
未优化结果:
result: 40000, average time: 144msresult: 40000, average time: 150msresult: 40000, average time: 149msresult: 40000, average time: 165msresult: 40000, average time: 134msresult: 40000, average time: 132msresult: 40000, average time: 157msresult: 40000, average time: 138msresult: 40000, average time: 158ms
可见使用volatile过后效果的确优于只使用synchronized的性能,不过试验中发现有个阈值,如果读取修改次数较小,比如1000以内,只使用synchronized效果略好,存取次数变大以后 volatile的优势才慢慢体现出来(次数达到10000的话,差距就在60ms左右)。
待挖掘
还有很多用法,在将来的学习中,不断总结与挖掘。
联想
无论处于应用的哪一层,优化的思路都是可以相互借鉴的,比如我们做一个服务集群,如果每一个节点都要保存所有用户的session,就很难使得session同步,我们就可以借鉴volatile这种思路,在集群之上搞一个调度器,如果某一个节点修改了一个用户session,就报告给调度器,然后调度器通知其他所有节点修改该用户session。而一般情况下,数据的读写比都比较高,所以这样做就能到达一个很好的性能。
注意事项
引用类型的volatile只在引用本身发生变化时具有可见性,其引用的对象的元素发生变化时不具有可见性
欢迎访问我的个人主页 (mageek.cn)