volatile
是 Java 虚拟机提供的轻量级的同步机制,有三大特点:保证可见性;不保证原子性;禁止指令重排
保证可见性
当多个线程操作共享数据时,彼此是不可见的。由此提出 JMM (java 内存模型)
JMM (java 内存模型) :是一种抽象的概念,并不真实存在,它描述的一组规则或者规范。通过这些规则、规范定义了程序中各个变量的访问方式。
在每个线程创建时,JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。但线程对变量的操作(读取、赋值)必须在工作内存中进行,首先将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量更新到主内存,也就是说,每个线程操作的实际上是变量的副本,他们只操作了自己复制的那一份,别的线程如何操作的不知道。
加上 volatile
修饰之后,会强制将修改的值立即写入主内存。注意的是 volatile
也不能用太多,会导致总线风暴
不保证原子性
public class JucTest {
public static void main(String[] args) {
AddData addData = new AddData();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
addData.add();
}
}, String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println("num = " + addData.num);
}
}
class AddData{
public volatile int num = 0;
public void add(){
num++;
}
}
输出结果为:
ini
复制代码
num = 17886
循环了20000次,为什么结果却不是20000呢?
因为 num++
不是一个原子操作,在多线程下是非线程安全的。
理想的情况是:
线程1在自己的工作内存中,将num改为1,写回主内存,由于内存可见性,通知线程2 num 已经改为1,线程2将num复制到自己的工作内存,将num++,改为2,写回主内存,通知线程3,以此类推。
但是在多线程的环境下,竞争调度,线程1刚刚要写入1的时候线程被挂起,2号线程将1 写入主内存,此时应该通知其他线程,主内存的值已经改为了1 了,由于线程操作极快,还未来及通知其他线程,刚才挂起的线程1将 num = 1 又写入了主内存,主内存的值被覆盖,出现了丢失写值
禁止指令重排
DCL 双重校验锁
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
System.out.println("实例化 Singleton");
instance = new Singleton();
}
}
}
return instance;
}
}
instance = new Singleton() 并不是一个原子操作,而是分为三步
1. memory = allocate(); // 1.分配对象内存空间
2. instance(memory); // 2.初始化对象
3. instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null
由于步骤2和步骤3不存在依赖关系,操作系统会进行指令重排序。也就是说步骤3可能会先执行
memory=allocate(); // 1.分配对象内存空间
instance=memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory); // 2.初始化对象
通过步骤3已经 != null 了,但是此时还没初始化完成,所以上面的 第二个 if (instance == null)
要加,防止进来多个线程,实例化多次。