面试官:讲一讲你对 volatile 的理解

张有志的回答,巴拉巴拉

volatile 是 Java 虚拟机提供的轻量级的同步机制,有三大特点:保证可见性;不保证原子性;禁止指令重排

保证可见性

当多个线程操作共享数据时,彼此是不可见的。由此提出 JMM (java 内存模型)

JMM (java 内存模型) :是一种抽象的概念,并不真实存在,它描述的一组规则或者规范。通过这些规则、规范定义了程序中各个变量的访问方式。

在每个线程创建时,JVM 都会为其创建一个工作内存 ,工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。但线程对变量的操作(读取、赋值)必须在工作内存中进行,首先将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量更新到主内存,也就是说,每个线程操作的实际上是变量的副本,他们只操作了自己复制的那一份,别的线程如何操作的不知道。

加上 volatile修饰之后,会强制将修改的值立即写入主内存。注意的是 volatile也不能用太多,会导致总线风暴

不保证原子性

csharp 复制代码
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 双重校验锁

csharp 复制代码
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() 并不是一个原子操作,而是分为三步

ini 复制代码
1. memory = allocate();    // 1.分配对象内存空间
2. instance(memory);        // 2.初始化对象
3. instance = memory;        // 3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤2和步骤3不存在依赖关系,操作系统会进行指令重排序。也就是说步骤3可能会先执行

ini 复制代码
memory=allocate();        // 1.分配对象内存空间
instance=memory;        // 3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);        // 2.初始化对象

通过步骤3已经 != null 了,但是此时还没初始化完成,所以上面的 第二个 if (instance == null)要加,防止进来多个线程,实例化多次。

相关推荐
披着羊皮不是狼6 分钟前
Spring Boot——从零开始写一个接口:项目构建 + 分层实战
java·spring boot·后端·分层
Tony Bai43 分钟前
Go GUI 开发的“绝境”与“破局”:2025 年现状与展望
开发语言·后端·golang
Tony Bai44 分钟前
【Go模块构建与依赖管理】08 深入 Go Module Proxy 协议
开发语言·后端·golang
kpli902 小时前
Java开发性能优化
java·jvm
码事漫谈2 小时前
从一个问题深入解析C++字符串处理中的栈损坏
后端
码事漫谈2 小时前
C++ 核心基石:深入理解 RAII 思想,告别资源泄露的噩梦
后端
Mos_x2 小时前
使用Docker构建Node.js应用的详细指南
java·后端
LucianaiB2 小时前
【CodeBuddy + GLM-4.6】超强联合打造一个梦幻搭子Agent
后端
wei_shuo2 小时前
openEuler 集群部署Nova计算服务:控制节点与计算节点实战操作
后端
Spirit_NKlaus3 小时前
Springboot自定义配置解密处理器
java·spring boot·后端