11.一个诡异的可见性问题
Posted 纵横千里,捭阖四方
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了11.一个诡异的可见性问题相关的知识,希望对你有一定的参考价值。
我们说synchronized能解决的三个问题是:原子性、可见性和有序性。那是否所有场景都需要同时解决这三个问题呢?不一定!看个例子:
public class VolatileExample
public static boolean stop = false;
public static void main(String[] args) throws InterruptedException
Thread t1 = new Thread(() ->
int i = 0;
while (!stop)
i++;
);
t1.start();
System.out.println("begin start thread");
Thread.sleep(1000);
stop = true;
这段代码的逻辑很简单,首先t1线程通过stop变量来判断是否结束循环,然后在main线程中通过修改stop变量的值来破坏t1线程的循环条件从而退出循环。但是实际情况是t1并没有按照期望输出。
注意如果你的JDK是client版本可能看不到效果。
我们while这里可以给i++加锁,也就是将上面t1的代码改成如下的样子,再运行就会让程序停下来
Thread t1 = new Thread(() ->
int i = 0;
while (!stop)
synchronized (VolatileExample.class)
i++;
);
或者我们添加一个打印和或者Thread.sleep(0),也就是将上面t1的代码改成如下的样子,再运行也会让程序停下来。
Thread t1 = new Thread(() ->
int i = 0;
while (!stop)
System.out.println("thread t1");
i++;
);
还有一个更牛的方式,增加一个创建文件的操作,此时也会让程序停下来:
Thread t1 = new Thread(() ->
int i = 0;
while (!stop)
new File("");
i++;
);
这是什么道理呢?很明显此时必然与new File()或者加锁等的底层机制有关系。
我们先看一下println的实现:
public void println(String x)
synchronized (this)
print(x);
newLine();
可以看到这里的println里加锁了,而且是类锁System。这里加了synchronized这个同步关键字,会防止循环期间对于stop值的缓存。因为println有加锁的操作,所以当其完成任务要释放锁的时候,会强制性的把工作内存中涉及到的写操作同步到主内存。 从IO角度来说,print本质上是一个IO的操作,我们知道磁盘IO的效率一定要比CPU的计算效率慢得多,所以IO可以使得CPU有时间去做内存刷新的事情,从而导致这个现象。比如我们可以在里面定义一个new File()。同样会达到效果。 Thread.sleep(0)生效的原因是导致线程切换,线程切换会导致缓存失效从而读取到了新的值。
在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。但是在多线程环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最新的值,这就是可见性。
为什么多线程环境下会存在可见性问题呢?
这主要是指令执行过程中存在重排序导致的,Server版本的编译器是面向服务器的,会做大量的优化,例如勿用代码消除,循环展开、消除公共子表达式等等。而本节最开始的代码之所以不能停止,就是因为代码被编译器优化了,我们直接在stop的定义前添加volatile关键字即可:
public volatile static boolean stop = false;
重排序问题具体咋回事?是不是只有重排序会带来可见性问题?不是的。我们后面继续讨论。
以上是关于11.一个诡异的可见性问题的主要内容,如果未能解决你的问题,请参考以下文章