内存可见性

2.内存可见性

在介绍volatile关键字之前,先来看看什么是内存可见性

那什么是内存可见性呢?

通过一个神奇的程序来引出内存可见性

例子很简单,一个线程根据循环条件一直循环,另一个线程一段时间后更改其循环条件使其变为false,然后观察程序运行情况。

首先创建一个线程并重写run()方法:

内存可见性

 

然后,在run()方法里面根据条件循环,这里我们采用while循环:

内存可见性

 

接着,循环条件是判断变量isStop的值(当前循环是否停止),所以我们需要定义一个boolean变量来记录循环是否需要停止:

内存可见性

 

run()方法书写完毕。

然后,启动线程:

内存可见性

 

接着,将isStop变量设置为true,即希望线程里面的while循环读到isStop变量为true时停止循环:

内存可见性

 

当然了,isStop不是在thread线程启动后立即设置,而是在thread线程启动后1秒去设置:

内存可见性

 

例子书写完毕。

在这里,大家可以猜一下运行结果是什么,肯定出乎你的意料。

这里就不卖关子了。

运行程序,执行结果:

内存可见性

 

从运行结果来看,跟我们想的不太一样,程序看似停着不动了。

请问这符合预期吗?

这是符合预期的,程序看似停着不动,实际上是while循环在不停地运行着没有结束。

明明isStop变量在经过1秒钟之后被主线程设置为true了,怎么while循环还没有结束呢?

这里我们还是把isStop变量在启动thread线程前后值的变化输出给大家看一下比较有说服力。

改写例子,在启动thread线程前输出isStop值:

内存可见性

 

在设置完isStop值后输出isStop值:

内存可见性

 

例子改写完毕。

运行程序,执行结果:

内存可见性

 

从运行结果来看,符合预期。

那么就有一个问题:明明isStop的值被改为true,thread线程里面的while循环为什么没有停下来?

这里就涉及到一个内存可见性问题。

主线程和thread线程读取isStop过程

首先说说主线程和thread线程读取isStop过程:

内存可见性

 

简单小结一下就是:

主线程先去缓存中取isStop,发现缓存中没有isStop,于是就去主内存中取isStop。

同理,thread线程也是先去缓存中取isStop,发现缓存中也没有isStop,于是就去主内存中取isStop。

现在主线程和thread线程都取得isStop=false。

thread线程执行while循环,主线程设置isStop=true过程

再来说说thread线程执行while循环,主线程设置isStop=true过程:

内存可见性

 

简单小结一下就是:

thread线程执行while循环,while循环条件中需要用到isStop,于是thread线程就去取isStop,优先去缓存中取isStop,发现缓存中有isStop,于是就取到了isStop=false,while循环条件(!isStop)成立。

过了一段时间,主线程执行isStop=true,由于这个操作,缓存和主内存中的isStop都变为了true。

但是,thread线程并不知道isStop已被主线程所修改,因为主线程在修改isStop值的时候并没有通知到各个已拥有isStop的缓存。

于是,thread线程就继续执行着while(!isStop)无法停止。

解决办法

综上所述,内存可见性问题就是有多个线程同时读取同一变量,当其中任意一个线程修改其变量的值时,其他线程都无法及时得到最新值。

内存可见性中的“内存”指的是主内存,“可见性”有两种,一种是可见,还有一种是不可见。

可见:多个线程共享变量时,其中一个线程修改其变量的值,其他线程及时得到最新值。

不可见:多个线程共享变量时,其中一个线程修改其变量的值,其他线程无法及时得到最新值。

例如,上述问题中主线程和thread线程就是多个线程,isStop就是它们读取的同一变量,主线程去修改了它的值,thread线程无法及时得到最新值。

这也说明了:主线程在某一时刻是执行写入操作的线程,thread线程在某一时刻是执行读取操作的线程。

解决这个问题就在于只要让执行写入操作的线程将数据写完之后能够通知到其他执行读取操作的线程即可。

请问这像什么?像不像同步?

同步还真能解决我们的问题。同步我们知道有隐式锁synchronized和显式锁Lock,这两种锁都能解决我们的问题,下面来看看。

Java多线程基础不好的同学可以前去阅读《“全栈2019”53篇Java多线程学习资料及总结》一章查阅相关Java多线程基础学习资料。

隐式锁synchronized

改写例子,在thread线程中的while循环里面加上同步代码块或调用同步方法:

内存可见性

 

例子改写完毕。

运行程序,执行结果:

内存可见性

 

从运行结果来看,符合预期。程序在1秒钟之后停下来了,问题得到了解决。

接下来把显式锁演示完了,我们再来说同步为什么能解决内存可见性问题。

显式锁Lock

改写例子,将while循环里面的同步代码块移除掉:

内存可见性

 

然后,创建出显式锁Lock:

内存可见性

 

接着,在while循环里面加锁:

内存可见性

 

例子改写完毕。

运行程序,执行结果:

内存可见性

 

从运行结果来看,符合预期。程序在1秒钟之后停下来了,问题也得到了解决。

同步为什么能够解决内存可见性问题?

因为无论缓存中有没有变量,同步都会使线程去主线程获取变量,而不是在缓存中获取,所以线程每次取得的变量都是最新的。

当然了,为了一个变量我们就用上同步,这代价未免也太大了吧?

为此,Java为我们提供了解决办法:volatile关键字

如果你只是解决内存可见性问题,volatile关键字足以。

volatile关键字怎么用?

请往下看。

3.volatile关键字

volatile关键字含义:

表明变量必须同步地发生变化。

volatile关键字用于修饰变量。

下面我们就来试试volatile关键字。

还是上一小节例子,首先将显式锁Lock对象移除掉:

内存可见性

 

然后,将显式锁同步代码移除掉:

内存可见性

 

接着,我们使用volatile关键字修饰isStop变量:

内存可见性

 

例子改写完毕。

运行程序,执行结果:

内存可见性

 

从运行结果来看,符合预期。程序在1秒钟之后停下来了,问题同样得到了解决。

下面,我们来通过动画感受一下加了volatile关键字之后的程序运行过程:

内存可见性

 

这里简单说一下过程:

当isStop被volatile关键字所修饰之后,每当线程需要去获取isStop变量时,都要去主内存中获取,所以在主线程修改isStop值为true时,thread线程及时读到了,于是while循环条件(!isStop)不成立,程序结束。

本系列是Java原子操作系列,volatile关键字和下一章要介绍的比较并交换CAS技术都是Java原子操作预备知识,大家应该好好理解与体会。

最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。

祝大家编码愉快!

GitHub

本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/volatile

总结

  • 内存可见性问题就是有多个线程同时读取同一变量,当其中任意一个线程修改其变量的值时,其他线程都无法及时得到最新值。
  • 内存可见性中的“内存”指的是主内存,“可见性”有两种,一种是可见,还有一种是不可见。
  • 可见:多个线程共享变量时,其中一个线程修改其变量的值,其他线程及时得到最新值。
  • 不可见:多个线程共享变量时,其中一个线程修改其变量的值,其他线程无法及时得到最新值。
  • 因为无论缓存中有没有变量,同步都会使线程去主线程获取变量,而不是在缓存中获取,所以线程每次取得的变量都是最新的。
  • volatile关键字含义:表明变量必须同步地发生变化。
  • volatile关键字用于修饰变量。
  • 如果你只是解决内存可见性问题,volatile关键字足以。