JavaSE-多线程(3)- volatile
volatile 解释
volatile 字面意思为 易变的,解释具体作用前可先看下以下例子:
VolatileTest类中有一个running 变量控制run方法执行,t1 线程启动后 run 方法一直执行,主线程在等待2秒后改变running 值,试图让 t1 线程结束运行,但通过运行结果发现效果并没有达到
例1)
package com.hs.example.base.multithread.day01;
public class VolatileTest {
private /*volatile*/ boolean running = true;
public void run(){
System.out.println("start...");
while (running){
}
System.out.println("end...");
}
public static void main(String[] args) {
VolatileTest volatileTest = new VolatileTest();
Thread t1 = new Thread(volatileTest::run);
t1.start();
/*new Thread(new Runnable() {
@Override
public void run() {
volatileTest.run();
}
}).start();*/
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileTest.running = false;
}
}
但是如果在变量running 前加上 volatile关键字,情况就会和预期的一样,通过在主线程改变running值,t1 线程可以停止运行
为什么出现以上情况
VolatileTest对象实例存放于堆内存中,而 main 和 t1 线程有自己的工作空间,他们访问的 running 变量实际是存放于自己工作空间的副本,当main线程改变自己工作空间 running 变量的值时,堆内存无法及时更新,t1 线程无法拿到最新的 running 值,所以导致线程一直运行。
例2)
下例是一个“懒汉式”的单例模式
package com.hs.example.base.multithread.day01;
public class VolatileTest2 {
private static /*volatile*/ VolatileTest2 instance = null;
private int a = 4;
private VolatileTest2() {
}
public static VolatileTest2 getInstance() {
//双重空值检查
//步骤 1 当instance实例化后可过滤大部分情况,直接返回实例
if (instance == null) {
//步骤 2
synchronized (VolatileTest2.class) {
/**
* 步骤 3 第二次空值判断,
* 假设两个线程 t1 , t2 同时到达步骤1,两者都判断实例为空,那么t1 获得锁后实例化了一个对象,等到t2 获得锁后又会实例化一个对象
* 在这里再加一层判断后,t1实例化对象,t2 获得锁,再判断一次,就不会再实例化了
*/
if (instance == null) {
instance = new VolatileTest2();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(VolatileTest2.getInstance());
}).start();
}
}
}
上面例子看似没什么问题,但是可能存在多线程访问时,VolatileTest2 实例属性值不一致问题,究其原因可以了解下对象创建过程。
对象创建过程:
- ①给对象分配内存
- ②给对象属性赋默认值(以上a的默认值为0)
- ③给对象属性赋初始值(a = 4)
- ④将对象赋值给引用 instance
假设对象创建时,还没有赋初始值就将对象赋值给了引用,那么这时候 instance 拿到的变量值就不是正确的(此时对象初始化还没完成),如果此时有线程进入,判断 instance 不为空,那么使用的 instance 属性值就是有误的,使用 volatile 关键字可以避免这种情况发生
volatile 作用
- 保证线程可见性 (例1)
- 防止指令重排序 (例2)
volatile 为什么能保证线程可见性
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量,其内存语义实现则是通过内存屏障。
内存屏障有两个指令: Load Barrier(读屏障),Store Barrier(写屏障)
内存屏障的作用:
- 阻止屏障两侧的指令重排序:(后面不往屏障前重排,前面不往屏障后重排)
- 强制把写缓冲区、高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
- 对于 Load Barrier 来说,在指令前插入 Load Barrier,强制重新从主内存加载数据,可以让高速缓存中的数据失效(不去读缓存中的数据,而是去主内存中重新加载)
- 对于 Store Barrier 来说,在指令后插入 Store Barrier, 能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
四种内存屏障:
LoadLoad屏障:禁止读和读的重排序
StoreStore屏障:禁止写和谐的重排序
LoadStore屏障:禁止读和写的重排序
StoreLoad屏障:禁止写和读的重排序
可以通过以下文章了解详细内容:
https://baijiahao.baidu.com/s?id=1709086005694976168&wfr=spider&for=pc
https://blog.csdn.net/weixin_43093006/article/details/111351514