我在之前的文章中提到过一个关于线程可见性例子:
如果执行上面的代码,大多人可能觉得会死循环,因为这里没有任何的同步策略,比如synchronized,Lock,atomic,volatile等关键字,也就是说没有任何同步策略保证,也就没有任何可见性,所以在主线程里面修改的变量,在另外一个线程里面可能看见也可能看不见,所以结果是不确定的,但实际上它总是停止的,不会陷入死循环,至于为什么,这个先不着急,我们接着再看下面的一段代码:
上面的这段程序其实跟我发的第一段代码类似,这里仅仅有一个同步块,但是程序也可以正常停止,看起来是非常诡异的,因为在JMM内存模型里面,没有volatile修饰的变量是不保证线程可见性的,此外我们发现这个变量也不在synchronized同步块里面,也就是说也不保证可见性,但程序为什么会终止呢?因为程序一旦终止,就意味着这个变量是具有可见性的,那么究竟是怎么回事?
其实这里是受happens-before关系的影响,看下面的一个例子:
然后接着,我们在线程A里面给上面的变量赋值:
然后我们在B线程里面我们访问这些值:
如果c的值打印3,那么即使a和b没有volatile修饰,那么线程B里面也可以访问到其最新的变化分别是2和1,因为根据happens-before关系,如果线程A的写操作发生在线程B的读操作之前,那么写操作之前的所有的数据都会同步到内存,然后在屏障后的读操作会从主内存读取所有的最新的数据,所以a和b的值也会被另外一个线程可见,这其实一定程度上增强了volatile关键字的作用。
在java里面,我们都知道synchronized关键字拥有volatile关键字所有的功能,那么他们有一样的影响,接着我们分析上一个例子,因为jit的优化,上面的循环语句:
会被优化成:
这样一来flag变量和synchronized同步块就具有happens-before关系了,首先被禁用重排序,其次当第一次同步块执行完毕之后,会被flush到主内存里面,接着在同步块之后再访问这个变量,就会从主内存加载,这样以来相当于有了可见性,即使是这里没有volatile关键字,所以我们的结果才可以正常停止,同理第一个例子里面println语句在JDK源码里面也是同步的:
所以就不难理解为什么都可以正常停止。到这里我们已经揭开这诡异问题的真面目。这里需要注意的是即使上面的代码结果是正确的,但这种编写代码的方式是不正确的,我们要避免这样做,因为它们看起来非常迷惑,所以如果我们需要可见性我们可以通过合理的同步来达到目的,例如使用volatile,synchronized,atomic等并发包里面的一些工具类,一定避免使用上面的方式。
最后关于synchronized同步块的条件,建议大家不要字符串做为锁,这里有几个弊端:
(1)字符串如果没有被final修饰,那么它的引用是可变的,这意味着这个锁可能会变成多个对象
(2)如果第三方的依赖包里面也有同样的锁字符串,那么就会冲突,这样来有可能导致莫名奇妙的问题。
所以这里推荐使用final修饰的Object对象的实例做为锁的条件。
总结:
本文通过两个诡异的案例,给大家展示了可能会遇到的一个奇怪的case,通过分析类比我们知道真正的原因是由于happen-before的关系,尽管从理论分析的通,但实际上它不是正确的使用方式,这一点大家一定要记住。
领取专属 10元无门槛券
私享最新 技术干货