Java 多线程 --- 线程同步 volatile关键字
volatile keyword
- Volatile是Java虚拟机提供的轻量级的同步机制
- Volatile可以保证可见性, 禁止指令重排, 但是不保证原子性
- volatile利用MESI协议和snooping保证可见性
- volatile不保证原子性, 比如
num++
这个操作实际上分为三步, 拿到num值, 对num值加一, 放回num值.- volatile关键字保证了拿到number的值是正确的,但是在执行对num值加一, 放回num值这些指令的时候,其他线程可能已经把number的值改变了,而操作栈顶的值就变成了过期的数据,所以就可能把较小的number值同步回主内存之中.
- 如果要实现原子性, 可以使用synchronized关键字, 或者使用 Java并发包(JUC)中的AtomicInterger等类
- 可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改
- 通过之前对synchronzed内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具有可见性。
- 同样的在volatile分析中,会通过在指令中添加lock指令,以实现内存可见性。因此, volatile具有可见性
使用volatile保证可见性
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
public class Solution {
static class ShareData {
int number = 0;
public void setNumberTo100() {
this.number = 100;
}
}
public static void main(String[] args) {
// 资源类
ShareData shareData = new ShareData();
// 子线程 实现了Runnable接口的,lambda表达式
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 线程睡眠3秒,假设在进行运算
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改number的值
shareData.setNumberTo100();
// 输出修改后的值
System.out.println(Thread.currentThread().getName() + "\t update number value:" + shareData.number);
}, "child Thread: ").start();
while(shareData.number == 0) {
//System.out.println(Thread.currentThread().getName() + "等待number更新为100");
}
//这句话输出不出来, 因为子线程更改number值后, main线程没有感知到
System.out.println(Thread.currentThread().getName() + "\t 主线程感知到了 number 不等于 0");
}
}
- 最后线程没有停止,没有输出"主线程知道了 number 不等于0"这句话,说明没有用volatile修饰的变量,变量的更新是不可见的
- 将number 声明为 volatile:
volatile int number = 0;
使用volatile禁止指令重排
- 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
- 有三种指令重排:
- .编译器优化重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- .指令级的并行重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- .内存系统的重排:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
Example: 双重检测锁定的单例模式
package com.jackson0714.passjava.threads;
/**
演示volatile 单例模式应用(双边检测)
* @author: 悟空聊架构
* @create: 2020-08-17
*/
class VolatileSingleton {
private static VolatileSingleton instance = null;
private VolatileSingleton() {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static VolatileSingleton getInstance() {
// 第一重检测
if(instance == null) {
// 锁定代码块
synchronized (VolatileSingleton.class) {
// 第二重检测
if(instance == null) {
// 实例化对象
instance = new VolatileSingleton();
}
}
}
return instance;
}
}
- 代码看起来没有问题,但是 instance = new VolatileSingleton();其实可以看作三条伪代码:
memory = allocate(); // 1、分配对象内存空间
instance(memory); // 2、初始化对象
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
- 步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); // 1、分配对象内存空间
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
instance(memory); // 2、初始化对象
可以使用volatile:
private static volatile VolatileSingleton instance = null;
注意:当且仅当满足以下所有条件时,才应该用volatile变量
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他的状态一起纳入不变性条件中。
- 在访问变量时不需要加锁。