之前在一篇文章中跟大家一同学习了CPU缓存一致性,通过缓存一致性协议MESI我们可以让CPU各个计算核心中缓存的数据保持一致,避免造成计算结果的差异。

我们还知道Java内存模型中,各个线程还保存了一份主内存中数据的拷贝,那么不同线程的拷贝应该如何保证数据的一致性呢,今天我们就跟大家一起看看其中的一个技术。

一、问题

有如下代码:

Java内存屏障底层 java内存屏障详解_内存屏障

代码意思很简单,如果看不懂的可以私信我或者+我讨论。

我们重点关注work这个方法,worker在工作前需要先看看自己是否在onWork状态,如果一个worker工作在一个线程中,那么onWork的状态改变我们可以确信是不会有问题的,肯定是按照指令顺序执行的。

多线程状态下会不会有问题?当然会!看如下状态切换:

1,onWork状态初始为false;

2,线程A将onWork状态切换为true,但是true值还保留在线程A的局部内存中,没有刷新回主内存;

3,线程B调用work方法,但是这个时候看到的onWork状态还是false,所以认为自己还是下班状态,所以不上工。

上面第3步里面就出错了,本来已经上班的worker,却出现了下班状态的输出!

二、内存屏障

那上面的问题应该怎么解决呢?我们分析一下问题出在哪里。

其实很简单,就是线程A在给onWork赋值之后没有将最新的值从本地内存中刷回到主内存!

如果线程A更新了值,并且刷回了主内存,线程B在使用onWork之前是从主内存中读取最新的值而不使用局部内存中的值,那么这样就肯定能够避免上述问题了。

Java中也确实是这么做的,这个技术就是内存屏障,或者叫内存栅栏,以Coder大白话理解就是当CPU遇到某个特殊变量的时候,会碰到一个栅栏,这个栅栏会拦住你继续往下执行而让你必须跟主内存进行一次交互,所以叫内存栅栏。这个道理是不是很简单?但是确实解决了问题。

三、Volatile

在上面coder的大白话理解中提到CPU遇到特殊变量才有产生内存栅栏的效果,那么这个特殊变量如何定义呢?

很简单,就是在变量前面加上volatile关键字,所以上面的代码,我们修改如下:

Java内存屏障底层 java内存屏障详解_内存屏障_02

这样当某个核心对onWork进行赋值之后,CPU会强制将这个值写回主内存,在遇到要读取onWork状态的时候,必须从主内存中读取最新的值。这样就可以保证每个线程在使用的时候能够读取最新的状态,这便是内存的可见性。

四、volatile的不可用场景

还记得之前我们试图用volatile来解决同步问题吗?代码在这里:

Java内存屏障底层 java内存屏障详解_Java内存屏障底层_03

但是当在多个线程中执行同一个seller对象的时候,发现ticket并不是按照我们想象的一个个递增的,那么问题出在哪里?

上面讲到volatile只解决内存的可见性,并不能解决内存的原子性,看如下流程:

1,ticket初始值为0;

2,线程A调用increment将ticket的值更新为1,并且刷回主内存;

3,线程B和线程C同时调用increment方法,这个时候先从主内存读取ticket的值,两个线程都读到了1,并且自增之后刷回主内存,这个时候主内存ticket的值是多少?没错,是2!

上面流程里面3个线程调用了3次ticket自增,但是最后ticket的值不是3而是2。

这里想要得到正确的值,还需要其他技术,比如同步、锁机制,我们后面跟大家分享。

这里简单总结一下volatile不适用场景:volatile修饰的变量的值的更改不能依赖于前值。比如onWork的状态更改不依赖之前到底是true还是false,但是ticket的更改因为是自增所以要依赖前值。

五、结语

今天跟大家分享了内存屏障的概念和volatile的用法,volatile可以作为轻量级的同步机制使用,但是大家一定注意其不适用的场景。关于两个线程重量级的同步机制,这个也是面试之中常见的考点,希望大家能掌握volatile关键字的作用,可以私我,解答疑问,相互提升。

三人行必有我师焉!