该篇文章用来总结笔者对于Volatile关键字的理解,并不会太过深入的探讨。
Volatile解决了什么问题?
并发编程中必须关注的三个问题:原子性、可见性、有序性。Volatile解决了其中的可见性、有序性问题,那Volatile是如何解决可见性、有序性的呢。解决有序性是因为Volatile禁止了指令重排,保证了程序运行时指令不会被重排,因为比如JIT就会对指令进行优化重排,而使用Volatile就可以禁止这样重排,这样就保证了不会因为指令重排导致变量赋值的紊乱。那Volatile是怎么解决可见性呢?说到可见性就必须提到java内存模型(JMM)了。
java内存模型是什么?
java内存模型(Java Memory Model)通俗的话来说就是每个线程在运行时都有着工作内存与主内存之分。线程的操作是工作内存完成的,之后才会将工作内存的信息更新到主内存。用一个图表示下JMM,如下:
这就是一个JMM的展示图,图中的箭头表示数据流向。从图中我们可以看出工作内存的数据都是来自主内存,同时工作内存中的数据也会同步到主内存,但是线程间工作内存是相互独立的。每个线程的工作内存数据都应该是来自主内存的拷贝。工作内存就像JVM内存结构中的程序计数器、虚拟机栈、本地方法栈一样都是线程私有,主内存就像是堆、方法区一样是线程共享的。但是必须要说明的是工作内存不能与程序计数器、虚拟机栈、本地方法栈画等号。他们并不是一个东西,他们其实是不同层面的一个描述,如果非要关联的话,笔者认为工作内存应该是包含了程序计数器、虚拟机栈、本地方法栈、以及部分堆空间区域的一块内存模型,为什么这么说呢,首先JMM只是一个内存模型,并没有严格的去根据java内存结构划分那块结构属于JMM中的工作区域,我们只能是根据JVM内存结构中各个区域的特点来反向推理哪些区域可能是属于JMM的工作内存范畴以便帮助于理解。程序计数器、虚拟机栈、本地方法栈都不难理解,他们都是线程私有我们可以看出是工作内存的一部分,那么为什么说堆中的部分内存也属于工作内存呢?
为什么说堆中的部分区域也属于工作内存呢?
两点原因:
第一点:工作内存会存储从主内存拷贝过来的变量,这个拷贝的变量不是仅仅只有栈中的引用,而且还是有真实的对象的,而对象都是存储在堆中,所以工作内存肯定会占用一部分堆空间用以存储从主内存拷贝过来的变量副本。
第二点:参考《深入理解java虚拟机第三版》49页第三行到第八行的解释,在并发场景下修改对象值时的安全问题,JVM一般有两种策略,一种是CAS+失败重试,另一种就是本地线程分配缓冲(TLAB),所谓本地线程分配缓冲就是为每个线程在堆中预先分配一块堆内存用以存储线程运行时产生的变量,既然可以存储线程运行时产生的变量,自然也可以存储从主内存拷贝过来的变量副本。
以上这两点是笔者的一点愚见。已经说偏了,言归正传上面都在说JMM是个啥,以及JMM中的工作内存如果类比到ava内存结构中可能包含的区域。那么说了JMM是啥了就应该聊聊Volatile是如何解决线程并发时的可见性问题了。
Volatile是如何解决可见性问题的?
先做个场景假设如下图,主内存中有一个变量叫object = 0,线程1和线程2都持有object这个变量的副本在各自的工作内存。且假设object这个变量被Volatile关键字修饰了,那么就会有如下的场景发生:
1.如果线程1中更改了object=1。 2.因为object被Volatile修饰了,所以在线程1中object发生变更时,会将线程1中工作内存中的object=1强制刷新到主内存中,并且只要其他线程引用了object这个变量就会将其他线程工作内存中的object进行强制失效。 3.线程2若是进行了object=2的操作,则会发现线程2工作内存中的object已经失效,需要从新将object从主内存中刷新过来,再进行操作。
总结
作个小小的总结,Volatile解决了两个问题:可见性、有序性问题。解决可见性是因为使用了Volatile会强制刷新工作内存的值到主内存,并让其他线程的引用失效,解决有序性是因为Volatile会禁止指令重排。
一点小小的感悟:被Volatile修饰的变量,若是在主内存中直接被改变了,所有引用了该变量的子线程中的该变量也都会失效,在他们使用该变量时会从新从主内存中获取。