在学习什么是volatile之前,首先应该认识一下Java内存模型。Java中的内存模型如图所示:
主内存:Java内存模型规定了所有的变量都存储在主内存中。主内存被所有的线程所共享。
工作内存:每条线程中都有自己的工作内存。
可以简单滴理解为计算机中的CPU高速缓存,但又不完全相同。线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来),相当于“副本”。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。
不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
那么为什么不在主内存上进行直接的操作呢?让我们一起来看一个例子:
假设有这样一个静态变量:
static int i = 0;
线程A执行如下代码:
i = 1;
那么Java内存模型工作流程如下:
首先主内存中i=0;线程A把静态变量i=0从主内存读到工作内存;然后再执行i=1操作,最后再把i=1 这个结果同步更新到主内存中。对于单线程来说,这个过程没有任何问题。
此时,我们引入线程B,执行如下操作:
System.out.println("i = " + i);
如果线程A先执行,线程B后执行,线程B的输出结果会是什么呢?有两种可能,是1或者0。
更大的可能是i=1;修改成功,原理很简单不再赘述。我们来看下另外一种i=0的可能。
首先,主内存中i=0;线程A把静态变量i=0从主内存读到工作内存;然后再执行i=1操作,因为工作内存所更新的变量并不会立即同步到主内存,所以虽然线程A在工作内存当中已经把变量i更新为1,但是线程B从主内存中获取的仍然是0,从而得到一个没有立即同步更新的结果 i=0。
那么这种问题如何解决呢?关键字volatile就可以很好地解决这个问题。Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去主内存中读取新值。为什么volatile关键字可以有这样的特性?这主要受益于java语言的先行发生原则(happens-before),即不需要通过任何手段就能够得到保证的有序性。
在上述例子中,如果在静态变量i之前加上volatile修饰符,就不会产生i=0的结果了:
volatile static int i = 0;
线程A、B依旧进行上个例子的操作,这样得到的结果一定就是i=1。
但是,volatile只能保证共享变量的可见性,不能保证变量的原子性。并且,一旦一个共享变量被volatile修饰了之后,禁止指令重排序。
小结:volatile除了保证可见性和阻止指令重排,但是不能保证原子性。