#多线程# 线程安全之可见性

#多线程# 线程安全之可见性

单线程,看山时山,看水是水
多线程,看山可能不是山,看水可能不是水

定义

JVM运行时数据区中,每个线程都有两块可操作区,一是自己独占的工作区,二是所有线程共享的内存区。可见性即是单个线程对共享区数据的更新对其他线程可见。

问题

然而,在多线程中,线程的可见性往往是会出问题的。
比如下面这一段代码:

public static boolean variable = true;
public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
        public void run() {
            System.out.println("start thread1!");
            while(variable){

            }
            System.out.println("end thread1!");
        }
    }).start();

    Thread.sleep(1000L);
    System.out.println("change variable to false!");
    variable = false;
}

这段代码实现的是,主线程通过对共享变量variable的修改来完成子线程while循环的终止。
期望结果:

start thread1!
change variable to false!
end thread1!

实际却是:

start thread1!
change variable to false!

看结果,上面一段代码最终没能完成我们想要完成的目标。经过追踪不难找到原因,子线程一直没有读到由主线程所更新的variable的值。

主线程更新了共享变量,子线程却读不到,从而程序失败,这便是可见性所引发的线程安全问题。

根源

程序失败是因为可见性问题,那么可见性问题的出现又是因为什么?
是因为缓存?指令重排?
不不不,根源只有一个,性能优化!其余只是手段。

CPU觉得读写内存太慢,它要要做优化,所以有了一级二级三级高速缓存。
CPU觉得程序不够优秀,执行浪费了时间,它要做优化,所以有了CPU指令重排。

无独有偶,JVM也有同样的想法。
单次执行某段代码的时候JVM可以容许代码不够优秀,以解释执行的方式一行一行地执行。但是在执行重复代码的时候(1、多次调用某个方法,2、多次循环),JVM便不会再一行行地执行,它会由解释执行上升为编译执行,它会将整段代码进行编译,然后再执行,这便是JIT编译。而在编译期间,它便顺道做一下优化,做一些指令重排。

没有任何事情是不会付出代价的,在单线程里毫无问题的缓存,指令重排等操作,在多线程中便造成了可见性问题。
比如缓存,缓存与内存同步总有一个过程,不做管理的话,理论上在此期间有可能出现脏读。
比如上面的例子,while循环便在JIT编译期间做了指令重排。

//源代码
while(variable){
}
//重排后,类似如下
if(variable){
	while(true){
	}
}

毫无疑问,在多线程环境中,这样的指令重排,这样的优化就不应该存在。

解决方案

找到了问题的根源,那如何解决?
显然地,一切问题早有解决方案。针对多线程对共享数据的读写操作,Java语言规范早已提出一系列的准则规范,称之为Java内存模型。具体实现由JVM去完成。

通过Java内存模型happens-before规则,我们一开始遇到的可见性问题便可以得以解决。代码中的循环也不再是死循环。

具体方法如下:
方法一:循环体加内容

  1. System.out.println("");
public static boolean variable = true;
public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
        public void run() {
            System.out.println("start thread1!");
            while(variable){
				System.out.println("");
            }
            System.out.println("end thread1!");
        }
    }).start();

    Thread.sleep(1000L);
    System.out.println("change variable to false!");
    variable = false;
}
  1. Thread.yield();
public static boolean variable = true;
public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
        public void run() {
            System.out.println("start thread1!");
            while(variable){
				Thread.yield();
            }
            System.out.println("end thread1!");
        }
    }).start();

    Thread.sleep(1000L);
    System.out.println("change variable to false!");
    variable = false;
}
  1. Thread.sleep();
  2. 。。。

是不是随便加什么内容都可以?显然加上i++肯定不行。
为什么简简单单的打印可以?
因为println()的源码如下:

public void println(boolean x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

通过对Java内存模型的理解可知,线程锁的获取与释放对其他线程同步。当while循环包含了synchronized,那么对while的指令重排必然失效。

为什么Thread.sleep()可以?因为它改变的当前线程的状态。
通过对Java内存模型的理解可知,线程状态的改变对其他线程同步。

方法二:volatile关键字

public static volatile boolean variable = true;
public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
        public void run() {
            System.out.println("start thread1!");
            while(variable){
            }
            System.out.println("end thread1!");
        }
    }).start();

    Thread.sleep(1000L);
    System.out.println("change variable to false!");
    variable = false;
}

volatile在JVM中的具体语义为cannot be cached,即由volatile所修饰的变量写不可缓存,读也不可缓存。
JIT指令重排,依赖于缓存下variable的值。当variable不可被缓存,指令重排自然被禁止。

总结

多线程里,简则以volatile修饰,繁则深入理解Java内存模型
到时看山还是山,看水还是水!

ps:如有失误,请多多指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值