使用volatile对其他线程实时可见
背景:
今天继续做白老师布置的作业,今天来设计一个小场景来演示用volatile修饰的变量对其他线程的可见性。
设计场景:
- 设计两个线程,第一个线程往已经定义好的list里面不断添加元素。
- 第二个线程不断读取这个list,当发现size等于10的时候,就输出日志并终止循环。
我们看这个list在有volatile修饰和没volatile修饰的区别。
代码:
先随便定义一个Bean:
package top.usjava.learn.javaarchitecturelearn.vo;
public class Cat {
String name;
int age;
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
接下来是测试类(list变量没有添加volatile修饰):
package top.usjava.learn.javaarchitecturelearn.unitlearn;
import top.usjava.learn.javaarchitecturelearn.vo.Cat;
import java.util.ArrayList;
import java.util.List;
public class TestVolatile {
List<Cat> list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
TestVolatile testVolatile = new TestVolatile();
Thread t1 = new Thread(()->{
for(int i =0;i<100;i++){
testVolatile.list.add(new Cat("a"+i,i));
}
System.out.println("已经添加完100个cat了");
});
Thread t2 = new Thread(()->{
while (true){
if (testVolatile.list.size()==10){
System.out.println("检测到list的size已经到10了");
break;
}
}
});
t2.start();
Thread.sleep(1000L);
t1.start();
}
}
结果如图:
我们从图中看到,list都已经添加完100个元素了,第二个线程都还没检测出来,还在不断循环检测,现在就变成了死循环了。因为list现在size已经是100了。(注意:线程1在跑之前,线程2已经在循环监听了。因为main方法里先让t2线程跑了,休眠1秒再让t1跑)
接下来把list加上volatile修饰看看:
volatile List<Cat> list = new ArrayList<>();
运行结果:
可以看到,加了volatile修饰后,线程2是可以检测到出来的。
原因:
这里简单地进行说明一下原因,深层次的分析可以查看其他博客或者相关书籍。
先看下图:
首先,说一下内存的一点点概念,就是程序运行时,有两个内存空间,一个是主内存,一个是工作内存。主内存里的变量是对各个线程都是可见的,而工作内存是每个线程自己的独占的内存空间,其他线程是看不到的。
知道这个概念后,再来描述下一般程序在没有用volatile时是如何对变量进行修改的:
1. 首先,线程1会从主内存读取和加载变量。
2. 然后,线程1会把该变量copy一份放到自己的工作内存里。
3. 接着,线程1对自己工作内存的该变量进行修改,修改完后,再写入主内存,更新主内存的值,这样,其他线程就可以通过主内存拿到最新的值了。但是注意:不是修改完工作内存的值后马上就写入主内存,这个主要看jvm和操作系统的调度了。
因此,没有用volatile时,如果其他线程在线程1写入主内存前读取该变量,那得到的值肯定是旧值。所以,上面代码的第一次运行结果就是这个原因。就是线程1在修改list时,不一定是在size等于10的时候写入主内存,所以线程2一直检测不到10的出现。
如果用volatile修饰,对比不用的情况,区别就是在修改变量后,线程马上把新值更新到主内存,然后其他线程再读取这个变量时,就拿到了最新的值了。所以上面代码第二次运行的结果是可以检测到的。
注意:
上面我代码的例子其实就算用volatile关键字修饰,也不是每次都可以检测成功的。因为虽然说是实时更新到主内存,但是由于线程2不一定是在size等于10的时候取值,所以有时也会检测不出来,这个时候就要可以用CountDownLatch这种方案来解决了。