[Java并发-1]变量可见性问题

变量可见性问题

1.什么是可见性?

  一个操作的结果,对于另一个操作是可见的。从Java程序的角度来说,是跨线程操作之间的可见性。举个例子,A线程存在一个写a变量的操作,B线程存在一个读a变量的操作,那么可见性就是指,写操作的结果,对于读操作是可见的。

我们用一段代码,来模拟可见性问题的场景

import java.util.concurrent.TimeUnit;


public class TestVolatile {

    private boolean flag = true;
    
    public static void main(String[] args) throws InterruptedException {
        TestVolatile testVolatile = new TestVolatile();
        Thread th1 = new Thread(new Runnable() {
            int i = 0;
            @Override
            public void run() {
                while(testVolatile.flag == true){
                    i = i + 1;
                }
                System.out.println("退出while循环,i=" + i);
            }
        });
        th1.start();
        TimeUnit.SECONDS.sleep(1);
        testVolatile.flag = false;
        System.out.println("flag设置为false了");
        th1.join();
        System.out.println("主线程结束");
    }

}

  事实上,这段代码在我的电脑上运行,是不会退出while循环的,这就出现了可见性问题:主线程对变量flag作出的修改操作,子线程th1看不到。理由有2点,第一是因为在多核CPU的硬件环境下,不同线程可能占用不同的CPU资源,每个CPU有自己的缓存,主线程对flag的修改结果是先写入主线程所在的CPU缓存,再写入JVM堆内存的,从写入缓存->写入主内存->th1刷新新的数据到缓存,这期间数据不一致,因此子线程th1可能暂时看不到最新的flag变量值(缓存一致性协议保证最终能看到);第二是因为出现了指令重排序(导致无限循环的罪魁祸首)。P.S.可见性问题是综合性问题,可能是CPU缓存导致的,也可能是硬件优化导致的(有些内存读写可能不会立刻触发,而是进入一个硬件队列),还可能是指令重排序、编译器优化导致的。

图片描述
  HotSpot虚拟机有2种编译形式,一种是解释执行,逐条将字节码编译成机器码并执行,一种是即时编译(Just In Time Compilation,JIT),将一个方法中所有的字节码翻译成机器码并执行。前者的优势在于,不需要等待编译,后者的优势在于,实际执行速度更快。HotSpot虚拟机默认采取混合模式,先解释执行字节码,然后对热点代码,以方法为单位进行即时编译。
  实际上,百分之20的代码占据了80%的计算资源,对于大部分不常用的代码,无需耗费时间即时编译,而是采取解释执行的方式运行。对于小部分热点代码(例如循环),我们可以将其编译为机器码,提高效率。HotSpot内置多个即时编译器:C1、C2和Graal。Graal是Java10正式引入的,这里不讲。C1又称为Client编译器,针对的是对启动性能有要求的客户端GUI程序,优化手段相对简单,编译时间较短。C2又称为Server编译器,针对的是对峰值性能有要求的服务端程序,优化手段相对复杂,编译时间较长。从Java7开始,HotSpot默认采取分层编译的方式:热点方法被C1编译,热点中的热点,被C2编译。即时编译是放在额外的线程中进行的,HotSpot会根据CPU的数量设置编译线程的数目,按照1:2的比例配置给C1和C2编译器。在计算资源充足的情况下,字节码的解释执行和即时编译可以同时进行,编译完成后的机器码,会在下次调用该方法时生效,替换原来的解释执行。

  回到例子,在示例代码的while循环处,因为i = i + 1执行次数很多,属于热代码,C2编译器会进行进一步的优化,将指令重排序,变成下面的样子。(如果运行参数为Client编译模式,在这里就不会出现重排序)

    if(flag){
        while(true){
            i = i + 1;
        }
    }

2.怎么解决上述问题?

  关闭重排序优化(JIT优化),告诉编译器,我不需要重排序。
[Java并发-1]变量可见性问题_第1张图片

  也可以利用volatile关键字。根据Java内存模型的规定,对一个volatile变量的写操作,happens before后续对这个volatile变量的读操作,我们可以给flag变量加上volatile关键字,这样只要某个线程更新了flag,其他线程立马能够看到最新的值。P.S.为了满足Java内存模型的规定,实现volatile关键字的功能,需要做到两点:1.volatile变量禁止缓存,任何线程对volatile变量的读,都得保证是最新的值。2.禁止重排序,volatile变量不做任何重排序的操作。

  运行结果:
[Java并发-1]变量可见性问题_第2张图片

3.可见性是由什么决定的

  可见性是由Java内存模型决定的。内存模型决定了程序在某个点,能够读取到什么值。

你可能感兴趣的:(java并发)