面试官:讲一讲你对 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 小时前
初探 Flask
后端·python·flask·html
星栈独行6 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Lei活在当下6 小时前
先用起来,再理解,关于协程Coroutine应该知道的事
android·java·jvm
Java爱好狂.7 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易7 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶7 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl8 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel8 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记9 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
IT_陈寒10 小时前
Java的Optional差点让我掉坑里,这几个坑你别踩
前端·人工智能·后端