Volatile
在多线程编程中,我们常常用到Volatile来维护变量的可见性,他是一个比较弱的同步机制。相较于sychronized关键字,在访问共享变量的时候,volatile不会进行加锁,因此他也不会造成线程阻塞,所以他更加轻量!
深入点看的话,当我们将变量声明为volatile类型的时候,编译与运行的时候,我们都会注意到这个变量是共享的,因此不会对将对该变量的操作和其他内存块一起 重排序,volatile不会被缓存到寄存器或者其他处理器不可见的地方,因此在读取volatile变量时,总会返回最新写入的值。
上文提到了一个词,重排序 ,何为重排序?
重排序即编译器和处理器对代码指令的动态优化 会打乱原本的java代码的执行顺序。而在程序从源代码到机器指令执行的过程中会发生多次的重排序。
既然重排序这是编译器和处理器做的事情,那么我们是怎么来避免了重排序呢?那便是我们下面要说的java内存模型
JAVA内存模型
我们接着说回volatile,我们知道volatile保证了多线程下对变量的可见性。而普通变量做不到这一点,普通变量的值在线程间传递均需要通过主内存来完成。例如,线程A修改了普通变量的值,那么他需要将这个变量向主内存进行回写,另外一个线程B在A回写完成之后再从主内存中进行读取,新变量才会对线程B可见。
主内存与工作内存
java内存模型规定了所有的变量都存储在主内存中,每条线程都有自己的工作内存。线程的工作内存中保存了该线程使用到的变量在主内存中的拷贝(所谓拷贝并不是将整个对象拷贝,而是拷贝的对象的引用),线程对该对象的所有操作都是在工作内存中进行,而不是直接去读写主内存,他们之间的关系如下图所示
注意 这里讲的主内存,工作内存与java内存区域中的java堆,栈,方法区等不是同一个层次的内存划分,他们直接没有任何的关系
由上可知如果并发线程中也经过这样的内存流程,那么便会造成线程不安全,而java内存模型中针对volatile修饰的变量,是只要有一条线程修改了这个变量的值,会立即通知到其他使用到这个变量的线程,让其他线程立即可知,这便是volatile的可见性。
volatile是线程安全的吗?
从以上的论据可以得到volatile在各个线程中是一致的,所以基于volatile变量运算在并发下是线程安全的。但是! 我们不能说基于volatile的运算在并发下是线程安全的!
看以下代码
public class VolatileTest { public static volatile int race = 0; public static void addRace(){ race ++; } public static void main(String[] args){ Thread[] threads = new Thread[20]; for (int i=0;i < 20;i++){ threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0;i<10000;i++){ addRace(); } } }); threads[i].start(); } while (Thread.activeCount() > 1){ Thread.yield(); System.out.println(race); } } }
理论上,如果并发正确的话,最终打印出来的值应该是200000,但是每次得到的结果其实都是小于200000的某一个值,因为在我们进行++ 运算的时候其实包含了多个操作,取出race变量放到栈顶,然后再进行赋值,再返回给使用线程的操作,因为保证了栈顶元素的可见性,其他线程这个时候也可更改此变量,而当上一个线程执行完成后,他会刷到主内存中,这样,便会造成会有不正确的数据。
所以可得结论,volatile只能保证可见性,并不能保证数据的原子性,如果需要保证一致性,我们需要借助synchronized或者JUC中的原子类
public static volatile int race = 0; public static synchronized void addRace(){ race ++; }