前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你了解的可见性可能是错的!

你了解的可见性可能是错的!

作者头像
用户5397975
发布2019-10-13 22:56:48
5580
发布2019-10-13 22:56:48
举报
文章被收录于专栏:咖啡拿铁

背景

这篇文章最开始再我的群里面有讨论过,当时想写的这篇文章的,但是因为一些时间的关系所以便没有写。最近阅读微信文章的时候发现了一篇零度写的一篇文章《分享一道阿里Java并发面试题》,对于有关Java并发性技术的文章我一般还是挺感兴趣的,于是阅读了一下,整体来说还是挺不错的,但是其中犯了一个验证可见性的问题。由于微信文章回复不方便讨论,于是我便把之前一些和群友的讨论在这里写出来。

如何测试可见性问题

因为在群里面我们习惯的有每周一问,也就由我或者群友发现一些由意思的问题然后提问给大家,让大家参与讨论,当时我提出了一个如何测试vlolatile可见性的问题,首先在Effective Java给出了一个测试volatile可见性的例子:

代码语言:javascript
复制
import java.util.concurrent.*;  

public class Test {  
    private static /*volatile*/ boolean stop = false;  
    public static void main(String[] args) throws Exception {  
        Thread t = new Thread(new Runnable() {  
            public void run() {  
                int i = 0;  
                while (!stop) {  
                    i++;  
//                    System.out.println("hello");  
                }  
            }  
        });  
        t.start();  

        Thread.sleep(1000);  
        TimeUnit.SECONDS.sleep(1);  
        System.out.println("Stop Thread");  
        stop = true;  
    }  
}  

这里大家可以复制上面的代码,你会发现这里程序永远不会结束,在零度的那篇文章中也给出了一个测试可见性的例子:

代码语言:javascript
复制
public class ThreadSafeCache {
    int result;

    public int getResult() {
        return result;
    }

    public synchronized void setResult(int result) {
        this.result = result;
    }

    public static void main(String[] args) {
        ThreadSafeCache threadSafeCache = new ThreadSafeCache();

        for (int i = 0; i < 8; i++) {
            new Thread(() -> {
                int x = 0;
                while (threadSafeCache.getResult() < 100) {
                    x++;
                }
                System.out.println(x);
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        threadSafeCache.setResult(200);
    }
}

这里大家也可以运行一下这里是不会结束的。

然而这两个例子真的是测试可见性的?我们先不着急下定论,首先我们来看看何为可见性,这里为了防止自己的一些片面之词,查阅了一些资料可以发现可见性的定义总体来说可以定义为:

当一个线程修改了共享变量后,其他线程能够立即得知这个修改。

可见性的定义比较简单,那怎么去实现呢?一般来说可见性会通过缓存一致性协议来完成,这里有篇文章讲CPU缓存一致性协议讲得不错:https://www.cnblogs.com/yanlong300/p/8986041.html,我这里直接借用他的图片,

  • CPU A 计算完成后发指令需要修改x.
  • CPU A 将x设置为M状态(修改状态)并通知缓存了x的CPU B, CPU B将本地cache b中的x设置为I状态(无效状态)
  • CPU A 对x进行赋值
  • CPU B 发现x是失效的这个时候会进行回刷操作

可以看见我们的一致性协议会有一定的时间延迟,但是我们的可见性的目的是立即读到最新的,所以我们这里会将无效状态通知到其他拥有该缓存数据的CPU缓存中,并且等待确认,我们vlolatile也是采用这种方式达到可见性的,当然更多的细节你可以直接阅读上面推荐的文章。

我们又回到我们的测试用例,可以发现我们的while循环是一个死循环,但是我们的缓存一致性协议是一定时间延迟,虽然这个一定时间并不保证,但是在现代的电脑系统上尤其是你自己的机器上,刷新一个缓存这点小时间还是有的吧。

并且我们验证可见性的时候似乎违背了我们初衷,可见性的定义是立即读到最新的,但是我们却在强调我们的测试程序会出现死循环,那我们不就是验证的是永远都读不到最新的吗?

通过上面的种种论述我们发现我们可见性的验证似乎出了一点问题。

推翻验证程序

我们这里只需要一行代码就可以推翻我们上面的验证程序,我们用第二个验证程序:

只添加了一句打印我们的结果值,我们的程序却停止了:

这个结果证明我们的其他线程是能获取到我们的更新后的结果值的,所以这里一定是有其他原因。

真相大白

我们上面添加了一句话,并没有影响我们的逻辑,但是却产生了截然不同的结果,这个到底是怎么回事呢?首先我们能想到的是编译器优化,看看添加代码前和添加代码后,编译器编译之后的代码是什么,由于我们用的是idea直接打开idea的class文件会帮助我们做反编译。

添加代码前:

添加代码后:

这里可以看见编译器已经将我们的while循环优化成for循环,在循环内部添加了一个输出语句,这里可以看见逻辑并没有太大的变化,可以看见不是我们的编译器作怪的问题,这种优化代码的问题还有一个元凶那就是JIT,由于我们的循环有很多次肯定会触发JIT编译优化。

由于JIT编译优化有多个层级,这里我们只看最终的C2优化后的汇编代码,看JIT的汇编代码可以利用hsdis+JITWatch查看,这里我只用了hsdis打印在控制台上查看即可。这里需要添加一下JVM启动参数-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly, 启动之后一大堆汇编代码,为了看这个查询了好多汇编指令终于是把它理顺了。

代码语言:javascript
复制
  0x0000000112f81ce8: cmp    $0x64,%r10d
  0x0000000112f81cec: jge    0x0000000112f81cfc  ;*goto
                                                ; - ThreadSafeCache::lambda$main$0@14 (line 29)

  0x0000000112f81cee: inc    %ebx               ; OopMap{rbp=Oop off=80}
                                                ;*goto
                                                ; - ThreadSafeCache::lambda$main$0@14 (line 29)

  0x0000000112f81cf0: test   %eax,-0xb001cf6(%rip)        # 0x0000000107f80000
                                                ;*goto
                                                ; - ThreadSafeCache::lambda$main$0@14 (line 29)
                                                ;   {poll}
  0x0000000112f81cf6: jmp    0x0000000112f81cee

上面的这么多行代码都是我们下面:这段代码的翻译:

代码语言:javascript
复制
                while (threadSafeCache.getResult() < 100) {
                    x++;
                }

解释一下汇编的代码:

  • Step 1:比较threadSafeCache.getResult() 和100的大小
  • Step 2: threadSafeCache.getResult()如果大于等于100,跳转至0x0000000112f81cfc,也就是循环外的代码。
  • Step 3: 如果小于,那么执行x++操作。
  • Step 4: 检查安全点checkpoint,这里不是逻辑代码不需要太关注。
  • Step 5: 跳转至我们的Step3处。

可以看见我们上面的代码Step3-5之间形成了死循环,其实我们的代码翻译过来可以看作下面的代码:

代码语言:javascript
复制
if(threadSafeCache.getResult() < 100){
    while(true){
        x++;
    }
}

可以看见我们的整段代码只执行了这一次get逻辑,有可能get的时候我们主线程还没有执行set。 为什么里面加了一段打印之后就不会有这样的效果呢?我的猜测是如果在我们print中有sync加锁操作,jit会取消这种激进的优化,当然我们的变量如果是volatile也会有这样的效果,我们添加volatile的jit的汇编代码如下:

可以发现这里没有做激进的优化而是每次都会获取新的值,来进行比较。

总结

到最后,我也没有提及,如何去测试可见性,因为这个东西理论上来说无法去测试,因为有一个很重要的一点我们没法确定线程的执行顺序,当然也有确定的方式,那就是加一个同步器,可以是锁,可以是信号量,让我们的读取操作,在我们写操作之后,还有读操作一定是一次,不能使用循环,我尝试着按照这个思路去写:

代码语言:javascript
复制
public class Test {

    private static /*volatile*/ boolean stop = false;

    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        Thread t = new Thread(new Runnable() {

            public void run() {
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(stop);
            }
        });
        t.start();

        Thread.sleep(1000);
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Stop Thread");
        stop = true;
        countDownLatch.countDown();
    }
}

上面这个程序没有加volatile,那么输出结果是有一定可能是false的但是发现,所有结果是true,其实这种方式没法去测试,因为我们外加了同步器而我们的同步器会带来读写屏障的加入,如果是读屏障那么会告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令,也就是会执行失效,回刷缓存。

所以验证可见性的确没有一个很好的例子,我们只需要知道如果没有其他保障(读写屏障等),有可能不能获取到最新的数据,但是其最终会获取到更新的数据,这个也很像我们分布式一致性中的最终一致性。

最后大家也可以看看零度的这篇文章:https://mp.weixin.qq.com/s/i9ES7u5MPWCv1n8jYU_q_w,其中的对内存屏障和happens-before也有一定的讲解。

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

本文分享自 咖啡拿铁 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 如何测试可见性问题
  • 推翻验证程序
  • 真相大白
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档