6.volatile:可见性

什么是可见性?

可见性就是,多线程环境中,对共享变量的修改对于其他线程是否立即可见的问题。
举个例子:

  • 子线程,1s之后,将flag修改为true
public class TestVolatile extends Thread{
    boolean flag = false;
    
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("flag:"+isFlag());
    }
    
    public boolean isFlag() {
        return flag;
    }
}
  • 主线程,不断判断flag的值,如果被修改,则打印并终止
public class Main {
    public static void main(String[] args) throws InterruptedException {
        TestVolatile thread = new TestVolatile();
        thread.start();

        while (true) {
            if (thread.isFlag()) {
                System.out.println("flag被改了");
                break;
            }
        }
    }
}

如果一切正常的话,1s后,主线程将会打印"flag被改了",但是真实情况确是子线程真的把flag改了,但是主线程一直没有感知到flag的变化,进入了死循环。这就是没有满足可见性所带来的问题。

为什么多线程之间存在不可见的问题?

要回答这个问题,就需要来看看Java内存模型:


6.volatile:可见性_第1张图片
Java内存模型

所有变量都被定义在主内存中,每个线程有自己的工作内存,线程需要先将主存中的变量拷贝到工作内存中,操作完成后,再同步到主内存。为什么会有工作内存呢?因为工作内存中的变量会放入CPU的高速缓存中,而主内存的变量就在物理内存中,这样可以提高速度。
对上面的例子做一个分析:

  1. 子线程启动后,主存变量flag=false
  2. 主线程进入while(true)循环,从主存中拷贝flag到主线程的工作内存,因为flag为false,所以一直循环
  3. 1s后,子线程从主存拷贝flag到子线程的工作内存,并对其修改,但并没有立即将该修改同步到主存
  4. 主线程工作内存中的值一直是false,将会一直循环下去

但是这里有一个问题,子线程是没有立即将flag同步到主存,但迟早会同步过去啊,那么主线程总有某一时刻会发现主存的flag被改了,然后从主存刷新flag的新值到工作内存,while循环也会停止才对啊,为啥就停不下来了呢?
问题的关键就在while(true)中,根据尚硅谷的NIO教程,while(true)是一个执行效率极高的操作,它的速度太快了,根本不给从主存刷新值的机会,所以才会停不下来。我们可以给while(true)代码加入Thread.sleep(1);,这时候,会发现主线程可以从主存刷新值了,代码可以正常结束了。可是问题是,谁写代码老加个sleep(),另外,我主线程当然想第一时间就感知到flag的修改,为啥让我休眠1ms。
这时候,就需要volatile闪亮登场了,我们给flag加上volatile修饰,再次运行,发现即使不sleep(),主线程也能感知到flag的修改了。

为啥volatile能保证可见性?

volatile规则:修改了volatile变量,必须立即同步到主存,同时使其他线程工作内存中的值变为无效;使用volatile变量前,必须先从主存刷新,以此来保证可见性
同样以上面的代码为例:子线程将flag修改为true,同步到主存,同时使主线程的工作内存中的flag失效,主线程下次使用flag时if (thread.isFlag()),发现工作内存中的flag已经失效,而且由于volatile的影响,在使用flag前本来也会强制从主存刷新,将会得到最新的值true

有其他办法能保证可见性吗?

在参考的资料中均有提到,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性。
但是我给子线程的flag=true;这一行加上synchronized同步代码块儿或者可重入锁,发现都未能实现可见性啊,可能是因为它只保证了修改flag立即同步到主存,却没能让主线程工作空间中的flag失效,也没有要求在使用flag之前必须从主存刷新吧。

最佳实践

由此例可以看出,volatile对于单纯的set/get场景是非常好用的

你可能感兴趣的:(6.volatile:可见性)