• 每日一面系列之volatile 的理解


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

    保证可见性

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

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

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

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

    不保证原子性

    1. public class JucTest {
    2.    public static void main(String[] args) {
    3.        AddData addData = new AddData();
    4.        for (int i = 1; i <= 20; i++) {
    5.            new Thread(() -> {
    6.                for (int j = 1; j <= 1000; j++) {
    7.                    addData.add();
    8.               }
    9.           }, String.valueOf(i)).start();
    10.       }
    11.        while (Thread.activeCount() > 2){
    12.            Thread.yield();
    13.       }
    14.        System.out.println("num = " + addData.num);
    15.   }
    16. }
    17. class AddData{
    18.    public volatile int num = 0;
    19.    public void add(){
    20.        num++;
    21.   }
    22. }

    输出结果为:

     
    

    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 又写入了主内存,主内存的值被覆盖,出现了丢失写值

    image.png

    禁止指令重排

    DCL 双重校验锁

    1. public class Singleton { ​  
    2.  
    3. private volatile static Singleton instance; ​  
    4.  public static Singleton getInstance() {      
    5.   if (instance == null) {            
    6. synchronized (Singleton.class) {  
    7.              if (instance == null) {  
    8.                  System.out.println("实例化 Singleton");                  
    9.   instance = new Singleton();              
    10. }          
    11. }      
    12. }        
    13. return instance;  
    14. } ​
    15. }

    instance = new Singleton() 并不是一个原子操作,而是分为三步

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

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

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

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

  • 相关阅读:
    A股风格因子看板 (2023.10 第03期)
    ctfshow web入门 php特性 web131-web135
    指针进阶(1)
    pytorch安装错误
    使用FastAPI部署Ultralytics YOLOv5模型
    【机器学习】07. 决策树模型DecisionTreeClassifier(代码注释,思路推导)
    数据持久化技术——MP
    SpringAMQP
    pytorch的F.cross_entropy交叉熵函数和标签平滑函数
    Android问题笔记 - kotlin中使用Java接口,报错Parameter specified as non-null is null 快速解决
  • 原文地址:https://blog.csdn.net/mclongyi/article/details/133066581