线程安全之可见性

可见性的概念

当一个线程修改了共享变量的值,其他线程能够看到修改的值。从定义上可以发现可见性更多的体现在线程的读操作上,在一个线程修改了共享变量的前提下,怎样保证让其它的线程都知道数据发送了变化。

示例

public class VisibilityDemo {
    private boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        VisibilityDemo demo1 = new VisibilityDemo();
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (demo1.flag) { 
                    i++;
                }
                System.out.println(i);
            }
        });
        thread1.start();

        TimeUnit.SECONDS.sleep(2);
        // 设置is为false,使上面的线程结束while循环
        demo1.flag = false;
        System.out.println("被置为false了.");
    }
}

在以上的代码中,我们在main方法中new了另一个线程,在这个线程中它不断的对i变量进行自增操作。
然后,主线程在休眠2s后,将flag为false了。那么thread1线程能否立即知晓主线程将flag置为false了呢?
事情并不简单
“神经大条”童鞋站起来了,他自信的说:thread1是能立即知道主线程更改了flag的值的,理由很简单,因为主线程和thread1线程都是用的同一个flag变量。
“眉头一皱”童鞋发现事情并不简单,他认为thread1并不能立即知道主线程更改了flag的值的,因为thread1会读取CPU缓存中的flag变量,这个过程是非实时的。

那么结果到底是怎样呢?Linux 的创始人 Linus Torvalds大佬说过"Talk is cheap. Show me the code",知道这句话的你,知道该怎么做了吧。不妨跑跑这段代码吧。
最终有结果打印的,但是似乎并不能验证以上的问题。那么,有没有其它办法能验证呢?

试试将运行模式设置为 -server,再run一下代码,看看结果会是怎样。
似乎,代码陷入了死循环…这又是什么原因导致的呢?
问号脸
为了验证这个问题,我们需要借助一个工具——jitwatch(能看到java编译后的汇编的工具)

  1. 下载 https://github.com/AdoptOpenJDK/jitwatch
  2. 解压 通过maven运行
    mvn clean compile exec:java
  3. 页面选择 config, 配置要调试的项目src源码路径,和class编译路径
    打开jit.log
    点击start
  4. 在分析的结果中,选中指定的类,再选择右侧的具体方法,则弹出jit编译结果

最终界面如下:
在这里插入图片描述
通过jitwatch工具,我们可以发现,以下代码实际被"修改"
在这里插入图片描述

 Thread thread1 = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (demo1.flag) { 
                    i++;
                }
                System.out.println(i);
            }
        });

实际运行代码是如下这样。在server模式下。编译器会对代码进行优化——指令重排序,而client模式下不会进行优化。

 Thread thread1 = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                // 指令重排序后
                while (true) { 
                    i++;
                }
                System.out.println(i);
            }
        });

产生以上问题的原因是什么呢?

可见性问题原因总结

  1. java内存结构模型
    当线程1将flag的值由true改为false后,线程2读取的仍然读取的可能是CPU缓存中flag=false的值,使得线程2对flag不可见(见下图)。java的内存模型结构使得一个线程修改了共享变量的值时,存在着可见性问题。
    在这里插入图片描述

  2. 指令重排序
    Java编程语言的语义允许编译器和微处理器执行优化,这些优化可以与不正确的同步代码交互,从而产生看似矛盾的行为。
    简单来说,java编译器在编译代码时并不是“老老实实”地把java代码一行一行的编译,它会偷偷对代码进行一下优化,在单线程情况下这种优化不会产生任何问题。但是多线程情况下的重排序,则可能会产生一些“意外惊喜”。

例如:

thread-1thread-2
1: r2 = A;3: r1 = B;
2: B = 1;4: A = 2;

期望结果:r1 = 1,r2 = 2

优化后:

thread-1thread-2
B = 1;r1 = B;
r2 = A;A = 2;

可能产生结果:r1 = 0,r2 = 0

重排序后的结果,可能并不是我们所期望产生的结果,在多线程情况下的重排序,可能使得java编译器“好心办坏事”。(以上案例来源于oracle官方,传送门

那么,当线程对一个共享变量进行更改后,怎么让其它线程能立即知道变量进行了更改呢?Java引入了一个关键字——volatile。

volatile关键字

volatile这个英文单词的释义:挥发性的;不稳定的。它可以用来修饰变量,如下所示。

private volatile boolean flag = true;

当一个变量被volatile修饰时,编译器则会"小心翼翼"的对待这个 变量。试着在原先的代码上对flag变量加上volatile,看看最终结果是否会有所不同。
是不是感觉一切又恢复了本该有的模样。

在oracle的文档中,关于volatile有以下描述。传送门
在这里插入图片描述
使用volatile变量可以降低内存一致性错误的风险,因为对volatile变量的任何写入都会建立与之后读取同一变量的happens-before关系。这意味着对volatile变量的更改始终对其他线程可见。

  1. 它强制性的要求,当一个线程对共享变量(volatile修饰的)进行更改后,必须立即写入主内存。同时当线程读取一个共享变量时,必须从主内存中读取。
    在这里插入图片描述
  2. 同时也不允许编译器对代码进行指令重排序

综上所述,volatile关键字可以解决线程之间的可见性问题。看起来是那么美好,似乎线程之间关于共享变量的矛盾烟消云散了。但是,volatile关键字是否真的有那么强大,强大到能够解决线程之间对共享变量的操作么?事实上并非如此。回到java内存模型那张图,线程将data更改为2,看起来似乎是一步简简单单的操作。其实不然。
在这里插入图片描述
实际的场景分为了以下3个步骤。尽管volatile保证了从内存中读取和写入,但是,其他线程可能步骤1,2,3中的某个步骤将data改为了3。此时的结果依旧是不准确的。只有保证步骤1,2,3是不可分割的才行,此时又引入了线程并发的另一个特性——原子性。敬请期待,下回分解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值