【Java 并发编程】 05 一个能和面试官扯很久的 volatile 关键字
上一篇我们在Java内存模型中(JMM)中 Happens-before 中讲到了volatile原则,对于volatile变量的写操作会早于对其的读操作。
volatile 意思就是可见的,常用来修饰某个共享变量,意思是当共享变量的值被修改后,会及时通知到其它线程上,其它线程就能知道当前共享变量的值已经被修改了。
volatile 关键字用来修饰实例变量和类变量。被 volatile 修饰后的变量可以解决并发编程可见性和有序性问题。但是不能保证原子性,且volatile 只能作用于实例或者类变量,不能修饰作用于方法。
volatile 关键字解决可见性问题
可见性
:一个线程对变量进行了修改,另外一个线程能够立刻读取到此变量的最新值。
【Java 并发编程】 03 万恶的 Bug 起源—并发编程的三大特性 一文中我们讲到了,缓存一致性导致可见性问题,讲到了CPU发展了,目前电脑服务器都是基于多核的,为了均衡CPU 与内存的速度差异,提高程序运行速度,引入的CPU缓存的概念。由于每个CPU 都要自己的缓存,当某一个值被修改放回内存后,其它CPU 缓存中的值还是旧值,引发了缓存不一致的问题。此时我们设想,如果内存中的值被修改后,内存通知CPU缓存,告诉CPU缓存中的值已经失效,请重新获取。被 volatile 关键字修饰的变量,就会被识别成共享变量,内存中值被修改后,各 CPU 缓存旧值失效,CPU缓存就会从内存中获取最新的值。
线程 1 和线程 2 一开始都读取了 C 值,CPU 1 和 CPU 2 缓存中也都有了 C 值,然后线程 1 把 C 值修改了,这时候内存的值和 CPU 2 缓存中的 C 值就不等了,内存这时发现 C 值被 volatile 关键字修饰,发现其是共享变量,就会使 CPU 2 缓存中的 C 值状态置为无效,CPU 2 会从内存中重新拉取最新的值,这时候线程 2 再来读取 C 值时,读取的已经是内存中最新的值了。
缓存一致性协议
volatile 关键可以解决缓存一致性问题,保证了线程的可见性。缓存一致性协议。协议的类型很多(MSI、MESI、MOSI、Synapse、Firefly),最常见的就是Intel 的MESI 协议,它是目前主流的缓存一致性协议,此协议会保证,写操作发生时,线程独占该变量的缓存,CPU 并且会通知其它线程对于该变量所在的缓存段失效。只有在独占操纵完成之后,该线程才能修改此变量。而此时由于其它缓存全部失效,所以就不存在缓存一致性问题。而其它线程的读取操作,需要等写入操作完成
,恢复到共享状态。(啰嗦了半天就一句话,写操作在读操作之前)
其实 MESI 这个缓存一致性协议和volatile 实现内存可见性中间还差了很多抽象的东西,上面东西只是扩展,你只需要知道 volatile 修饰的共享变量是有内存可见性的就可以了。
volatile 关键解决有序性问题—禁止指令重排序。
有序性
:代码在运行期间保证按照编写的顺序,禁止指令重排序。
Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,保证共享变量操作的有序性。
内存屏障
: 就是在屏障前的所有指令可以重排序的,屏障之后的指令也可以重排序,但是重排序的时候不能越过内存屏障。也就是说内存屏障前的指令不会被重排序到内存屏障之后,反之亦然。
内存屏障指令:写操作的会让线程本地的共享内存变量写完强制刷新到主存。读操作让本地线程变量无效,强制从主内存读取,保证了共享内存变量的可见性。
(1)LoadLoad 屏障
执行顺序:Load1—>Loadload—>Load2
确保Load2及后续Load指令加载数据之前能访问到Load1加载的数据。
(2)StoreStore 屏障
执行顺序:Store1—>StoreStore—>Store2
确保Store2以及后续Store指令执行前,Store1操作的数据对其它处理器可见。
(3)LoadStore 屏障
执行顺序: Load1—>LoadStore—>Store2
确保Store2和后续Store指令执行前,可以访问到Load1加载的数据。
(4)StoreLoad 屏障
执行顺序: Store1—> StoreLoad—>Load2
确保Load2和后续的Load指令读取之前,Store1的数据对其他处理器是可见的。
对于volatile 关键字的认识我们还是需要时间磨合的,先了解基础的概念,它解决了什么问题,底层的东西我们后做了解。我还会回来的!