面试官:讲一讲你对 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)要加,防止进来多个线程,实例化多次。

相关推荐
你的人类朋友9 小时前
先用js快速开发,后续引入ts是否是一个好的实践?
前端·javascript·后端
码事漫谈9 小时前
医疗设备控制系统中同步与异步通信的架构设计
后端
码事漫谈9 小时前
C++ 中 rfind 方法详解
后端
AAA修煤气灶刘哥10 小时前
服务器指标多到“洪水泛滥”?试试InfluxDB?
数据库·后端·面试
uzong10 小时前
技术面试,时间不足15分钟,面试官就挂掉了电话,原因竟然是……
后端·面试
Roye_ack10 小时前
【项目实战 Day12】springboot + vue 苍穹外卖系统(Apache POI + 工作台模块 + Excel表格导出 完结)
java·spring boot·后端·excel·苍穹外卖
kobe_OKOK_11 小时前
Django ORM 字段查询表达式(Field lookup expressions)
后端·python·django
qq_54702617911 小时前
SpringBoot+Redis实现电商秒杀方案
spring boot·redis·后端
Code blocks12 小时前
SpringBoot自定义请求前缀
java·spring boot·后端