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

相关推荐
罗政5 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师6 小时前
spring获取当前request
java·后端·spring
Java小白笔记8 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
Joeysoda9 小时前
Java数据结构 时间复杂度和空间复杂度
java·开发语言·jvm·数据结构·学习·算法
18你磊哥9 小时前
java重点学习-JVM组成
java·开发语言·jvm
JOJO___10 小时前
Spring IoC 配置类 总结
java·后端·spring·java-ee
白总Server11 小时前
MySQL在大数据场景应用
大数据·开发语言·数据库·后端·mysql·golang·php
蜜桃小阿雯11 小时前
JAVA开源项目 校园美食分享平台 计算机毕业设计
java·jvm·spring boot·spring cloud·intellij-idea·美食
longlongqin11 小时前
JIT(即时编译)技术
jvm
Lingbug12 小时前
.Net日志组件之NLog的使用和配置
后端·c#·.net·.netcore