前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于线程可见性一个“诡异”的问题

关于线程可见性一个“诡异”的问题

作者头像
我是攻城师
发布2018-07-23 10:51:51
4020
发布2018-07-23 10:51:51
举报
文章被收录于专栏:我是攻城师我是攻城师

我在之前的文章中提到过一个关于线程可见性例子:

代码语言:javascript
复制
static boolean keepRunning=true;

    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{

            while (keepRunning){

               System.out.println();
            }
        }).start();

        Thread.sleep(1000);
        keepRunning=false;

如果执行上面的代码,大多人可能觉得会死循环,因为这里没有任何的同步策略,比如synchronized,Lock,atomic,volatile等关键字,也就是说没有任何同步策略保证,也就没有任何可见性,所以在主线程里面修改的变量,在另外一个线程里面可能看见也可能看不见,所以结果是不确定的,但实际上它总是停止的,不会陷入死循环,至于为什么,这个先不着急,我们接着再看下面的一段代码:

代码语言:javascript
复制
private  static boolean flag=true; // main thread will call flag=false

    private final static Object lock=new Object(); // lock condition

    public static void thread1(){

        while (flag){

            synchronized (lock){
                // some work
            }

        }

    }


    public static void main(String[] args) throws Exception {

        Thread t1=new Thread(()->{
            thread1();
        });
        t1.start();
        Thread.sleep(1000);
        flag=false;

        // The program can stop normally

    }

上面的这段程序其实跟我发的第一段代码类似,这里仅仅有一个同步块,但是程序也可以正常停止,看起来是非常诡异的,因为在JMM内存模型里面,没有volatile修饰的变量是不保证线程可见性的,此外我们发现这个变量也不在synchronized同步块里面,也就是说也不保证可见性,但程序为什么会终止呢?因为程序一旦终止,就意味着这个变量是具有可见性的,那么究竟是怎么回事?

其实这里是受happens-before关系的影响,看下面的一个例子:

代码语言:javascript
复制
public class Shared {
    public int a;
    public int b;
    public volatile int c;
}

然后接着,我们在线程A里面给上面的变量赋值:

代码语言:javascript
复制
shared.a = 1;
shared.b = 2;
shared.c = 3;

然后我们在B线程里面我们访问这些值:

代码语言:javascript
复制
display(c);
display(b);
display(a);

如果c的值打印3,那么即使a和b没有volatile修饰,那么线程B里面也可以访问到其最新的变化分别是2和1,因为根据happens-before关系,如果线程A的写操作发生在线程B的读操作之前,那么写操作之前的所有的数据都会同步到内存,然后在屏障后的读操作会从主内存读取所有的最新的数据,所以a和b的值也会被另外一个线程可见,这其实一定程度上增强了volatile关键字的作用。

在java里面,我们都知道synchronized关键字拥有volatile关键字所有的功能,那么他们有一样的影响,接着我们分析上一个例子,因为jit的优化,上面的循环语句:

代码语言:javascript
复制
while (flag){

        synchronized (lock){
            // some work
        }

    }

会被优化成:

代码语言:javascript
复制
if(flag){

        while (true){

            synchronized (lock){
                //some work
            }

        }

    }

这样一来flag变量和synchronized同步块就具有happens-before关系了,首先被禁用重排序,其次当第一次同步块执行完毕之后,会被flush到主内存里面,接着在同步块之后再访问这个变量,就会从主内存加载,这样以来相当于有了可见性,即使是这里没有volatile关键字,所以我们的结果才可以正常停止,同理第一个例子里面println语句在JDK源码里面也是同步的:

代码语言:javascript
复制
public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

所以就不难理解为什么都可以正常停止。到这里我们已经揭开这诡异问题的真面目。这里需要注意的是即使上面的代码结果是正确的,但这种编写代码的方式是不正确的,我们要避免这样做,因为它们看起来非常迷惑,所以如果我们需要可见性我们可以通过合理的同步来达到目的,例如使用volatile,synchronized,atomic等并发包里面的一些工具类,一定避免使用上面的方式。

最后关于synchronized同步块的条件,建议大家不要字符串做为锁,这里有几个弊端:

(1)字符串如果没有被final修饰,那么它的引用是可变的,这意味着这个锁可能会变成多个对象

(2)如果第三方的依赖包里面也有同样的锁字符串,那么就会冲突,这样来有可能导致莫名奇妙的问题。

所以这里推荐使用final修饰的Object对象的实例做为锁的条件。

总结:

本文通过两个诡异的案例,给大家展示了可能会遇到的一个奇怪的case,通过分析类比我们知道真正的原因是由于happen-before的关系,尽管从理论分析的通,但实际上它不是正确的使用方式,这一点大家一定要记住。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档