03.04 Volatile、线程可见性、指令重排; 这三者到底有什么样的关系?

volatile这个关键字在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。

volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。

Volatile、线程可见性、指令重排; 这三者到底有什么样的关系?

被volatile修饰的变量,将具备两种特性:

1)保证此变量对所有的线程的可见性。

这里的“可见性”,指的是当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,保证其他线程得到的变量是最新的值。普通变量是做不到这点的。

如果要理解这一点需要对程序执行的过程有所了解。

当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

如下面的这段代码:

<code>a = a + 1;/<code>

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。

在多核CPU中,每条线程可能运行于不同的CPU中。如果有两个线程同时执行以上代码,线程1执行 a =a+1这句时,会先把a的初始值加载到CPU1的高速缓存中,然后加1。同样线程2执行 a =a+1这句时,也会先把a的初始值加载到CPU2的高速缓存中,然后加1。这样可能最总a的值只增加了1,并没有增加了我们预想中的2。

volatile修饰的变量,会保证修改的值会立即被更新到主存,当有其他线程需要读取时,会去主内存中读取新值。就避免了以上的问题。

Volatile、线程可见性、指令重排; 这三者到底有什么样的关系?

2)禁止指令重排优化。

指令重排指的是处理器为了提高程序运行效率,可能会对输入代码进行优化,重新排序代码的执行顺序,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

原理是,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

例如代码在线程1执行:

<code>loadconfig();       //语句1/<code>

以上语句因为是没有数据依赖性的,所以CPU可能会对他们的执行顺序进行重新排序。可能会先执行语句2然后执行语句1。

而线程2:

<code>While(!isInit){/<code>

以上代码,会导致因为线程1先执行语句2,isInit的值为true,线程2则不再等待配置初始化往下运行。导致程序出现处理逻辑错误。

值得注意的一点,volatile虽然能够保证线程的可见性 但是并不能保证原子性,所以不能替代synchronized。


分享到:


相關文章: