浅谈volatile关键字

news2024/11/28 4:42:49

文章目录

    • 1.保证内存可见性
    • 2.可见性验证
    • 3.原子性验证
    • 4.原子性问题解决
    • 5.禁止指令重排序
    • 6.JMM谈谈你的理解
      • 6.1.基本概念
      • 6.2.JMM同步规定
        • 6.2.1.可见性
        • 6.2.2.原子性
        • 6.2.3.有序性
      • 6.3.Volatile针对指令重排做了啥
    • 7.你在哪些地方用过Volatile?

volatile是Java提供的轻量级的同步机制,主要有三个特性:

1.保证内存可见性
2.不保证原子性
3.禁止指令重排序

1.保证内存可见性

volatile是Java提供的轻量级的同步机制,保证了可见性,不保证原子性。了解volatile工作机制,首先要对Java内存模型(JMM)有初步的认识:

  • 每个线程创建时,JVM会为其创建一份私有的工作内存(栈空间),不同线程的工作内存之间不能直接互相访问。JMM规定所有的变量都存在主内存,主内存是共享内存区域,所有线程都可以访问
  • 线程对变量进行读写,会从主内存拷贝一份副本到自己的工作内存,操作完毕后刷新到主内存。所以,线程间的通信要通过主内存来实现。

volatile的作用是:线程对副本变量进行修改后,其他线程能够立刻同步刷新最新的数值。这个就是可见性

在这里插入图片描述

2.可见性验证

我们来看一个例子:

package com.bruceliu.demo15;

import java.util.concurrent.TimeUnit;

/**
 * @BelongsProject: Thread0509
 * @BelongsPackage: com.bruceliu.demo15
 * @Author: bruceliu
 * @QQ:1241488705
 * @CreateTime: 2020-05-13 23:16
 * @Description: TODO
 */
public class VolatileDemo {

    int x = 0;
    //注意:这里的b没有被volatile修饰
    boolean b = false;

    /**
     * 写操作
     */
    private void write() {
        x = 5;
        b = true;
        System.out.println("x=>" + x);
        System.out.println("b =>" + b);
    }

    /**
     * 读操作
     */
    private void read() {
        //如果b=false的话,就会无限循环,直到b=true才会执行结束,会打印出x的值
        while (!b) {

        }
        System.out.println("x=" + x);
    }

    public static void main(String[] args) throws Exception {
        final VolatileDemo volatileDemo = new VolatileDemo();

        //线程1执行写操作
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                volatileDemo.write();
            }
        });

        //线程2执行读操作
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                volatileDemo.read();
            }
        });

        //我们让线程2的读操作先执行
        thread2.start();

        //睡1毫秒,为了保证线程2比线程1先执行
        TimeUnit.MILLISECONDS.sleep(1);

        //再让线程1的写操作执行
        thread1.start();

        thread1.join();
        thread2.join();

        //等待线程1和线程2全部结束后,打印执行结束
        System.out.println("执行结束");
    }
}

注意我们的b没有用volatile修饰,我们先启动了线程2的读操作,后启动了线程1的写操作,由于线程1和线程2会保存x和b的副本到自己的工作内存中,线程2执行后,由于他副本b=false,所以会进入到无限循环中,线程1执行后修改的也是自己副本中的b=true,然而线程2无法立即察觉到,所以执行上面代码后,不会打印“执行结束”,因为线程2一直在执行!
在这里插入图片描述

运行之后会一直出于运行状态,并且没有打印“执行结束”

在这里插入图片描述
此时的流程会是这样子

在这里插入图片描述

给b加了volatile关键字修饰后,线程1对b做了修改,然后会立即更新内存中的值,线程2通过嗅探发现自己的副本已经过期了,然后重新从内存中拿到b=true的值,然后跳出while循环,执行结束!

我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用.

3.原子性验证

看下面一段代码,number变量加了volatile修饰。创建了10个子线程,每个线程循环1000次执行number++。

package com.bruce.demo8;

import java.util.concurrent.TimeUnit;

/**
 * @BelongsProject: SingleTon-2020
 * @BelongsPackage: com.bruce.demo8
 * @CreateTime: 2020-12-10 23:08
 * @Description: TODO
 */
public class Demo8 {

    static class MyTest {
        public volatile int number = 0;
        public void incr(){
            number++;
        }
    }


    public static void main(String[] args) {
        MyTest myTest = new MyTest();

        for (int i = 1; i <= 10; i++){

            new Thread(() -> {
                for (int j = 1; j <= 1000; j++){
                    myTest.incr();
                }
            }, "Thread"+String.valueOf(i)).start();
        }

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //等线程执行结束了,输出number值
        System.out.println("当前number:" + myTest.number);
    }
}

按理说number最终应该是10000,但是这边执行后,结果如下:
在这里插入图片描述

4.原子性问题解决

方法一:使用 synchronized 关键字

//给函数增加synchronized修饰,相当于加锁了
 public synchronized void incr(){
     number++;
 }

结果如下:
在这里插入图片描述
方法二:使用AtomicInteger

public class Demo8 {

    static class MyTest {

        public volatile AtomicInteger number = new AtomicInteger();
        
        public void incr(){
            number.getAndIncrement();
        }
    }


    public static void main(String[] args) {
        MyTest myTest = new MyTest();

        for (int i = 1; i <= 10; i++){

            new Thread(() -> {
                for (int j = 1; j <= 1000; j++){
                    myTest.incr();
                }
            }, "Thread"+String.valueOf(i)).start();
        }

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //等线程执行结束了,输出number值
        System.out.println("当前number:" + myTest.number);
    }
}

5.禁止指令重排序

体现了JMM的有序性

6.JMM谈谈你的理解

6.1.基本概念

JMM 本身是一种抽象的概念并不是真实存在,它描述的是一组规定或则规范,通过这组规范定义了程序中的访问方式。

6.2.JMM同步规定

6.2.1.可见性

可见性:一个线程对某一共享变量修改之后,另一个线程要立即获取到修改后的结果。

  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存
  • 加锁解锁是同一把锁

由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量的储存在主内存,主内存是共享内存区域,所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须都工作内存进行看。

首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

内存模型图
在这里插入图片描述

6.2.2.原子性

原子性: 不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要具体完成,要么同时成功,要么同时失败。

数据库也经常提到事务具备原子性!
在这里插入图片描述

在putfield时,其他线程挂起,没有及时得到主内存的值改变消息

n++在多线程下是非线程安全的,如何不加synchronized解决?

  • 使用原子类(java.util.concurrent.atomic)
  • 为什么使用了原子类可以解决?原理是什么?CAS

6.2.3.有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分一下3种:

源代码->编译器优化的重排->指令并行的重排->内存系统的重排->最终执行的指令

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须考虑指令之间的数据依赖性。

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

指令重排 - example 1

public void mySort() {
    
	int x = 11;   // 1
    int y = 12;   // 2
    
	x = x + 5;  // 3
     y = x * x;  // 4
	
}

按照正常单线程环境,执行顺序是 1 2 3 4
但是在多线程环境下,可能出现以下的顺序:

2 1 3 4

1 3 2 4

上述的过程就可以当做是指令的重排,即内部执行顺序,和我们的代码顺序不一样。但是指令重排也是有限制的,即不会出现下面的顺序

4 3 2 1

因为处理器在进行重排时候,必须考虑到指令之间的数据依赖性

因为步骤 4:需要依赖于 y的申明,以及x的申明,故因为存在数据依赖,无法首先执行

例子
int a,b,x,y = 0

线程1线程2
x = a;y = b;
b = 1;a = 2;
x = 0; y = 0

因为上面的代码,不存在数据的依赖性,因此编译器可能对数据进行重排

线程1线程2
b = 1;a = 2;
x = a;y = b;
x = 2; y = 1

这样造成的结果,和最开始的就不一致了,这就是导致重排后,结果和最开始的不一样,因此为了防止这种结果出现,volatile就规定禁止指令重排,为了保证数据的一致性

指令重排 - example 2
比如下面这段代码

public class ResortSeqDemo {
    int a= 0;
    boolean flag = false;

    public void method01() {
        a = 1;
        flag = true;
    }

    public void method02() {
        if(flag) {
            a = a + 5;
            System.out.println("reValue:" + a);
        }
    }
}

我们按照正常的顺序,分别调用method01() 和 method02() 那么,最终输出就是 a = 6

但是如果在多线程环境下,因为方法1 和 方法2,他们之间不能存在数据依赖的问题,因此原先的顺序可能是

a = 1;
flag = true;

a = a + 5;
System.out.println("reValue:" + a);

但是在经过编译器,指令,或者内存的重排后,可能会出现这样的情况

flag = true;

a = a + 5;
System.out.println("reValue:" + a);

a = 1;

也就是先执行 flag = true后,另外一个线程马上调用方法2,满足 flag的判断,最终让a + 5,结果为5,这样同样出现了数据不一致的问题

为什么会出现这个结果:多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

这样就需要通过volatile来修饰,来禁止指令重排序保证线程安全性

6.3.Volatile针对指令重排做了啥

Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象
首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  • 保证特定操作的顺序
  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 内存屏障另外一个作用是刷新出各种CPU的缓存数,因此任何CPU上的线程都能读取到这些数据的最新版本。
在这里插入图片描述
也就是过在Volatile的写 和 读的时候,加入屏障,防止出现指令重排的

7.你在哪些地方用过Volatile?

工作内存与主内存同步延迟现象导致的可见性问题

  • 可通过synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其它线程可见,对于指令重排导致的可见性问题和有序性问题
  • 可以使用volatile关键字解决,因为volatile关键字的另一个作用就是禁止重排序优化

举例:

public class LazySafe {
    private LazySafe(){
    }
    //对象加上了volatile关键字是为了保证变量的可见性,防止指令重排序
    //第二个线程拿到的可能是半实列化的对象,所以要加volatile防止指令重排序
    private volatile static LazySafe lazySafe;

    public static  LazySafe getInstance(){
        if(lazySafe==null){
            //双重判定
            synchronized (LazySafe.class){
                if(lazySafe==null){
                    lazySafe=new LazySafe(); //不是原子性的!
                }
            }
        }
        return lazySafe;
    }
}

DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排

原因是在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。因为 instance = new SingletonDemo();可以分为以下三步进行完成:

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

但是我们通过上面的三个步骤,能够发现,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

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

这样就会造成什么问题呢?

也就是当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例

指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题

所以需要引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/364356.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【消费战略】解读100个食品品牌丨元气森林 6年百亿的饮品黑马成功之道

元气森林成立于2016年&#xff0c;短短六年时间取得了近百亿营收的奇迹&#xff0c;成为让可口可乐、百事、娃哈哈、农夫山泉等消费巨头都无法忽视的对手。六年的成长堪比行业前辈20多年的积累&#xff0c;从这个角度而言&#xff0c;塔望咨询认为元气森林是成功的&#xff0c;…

电影《蚁人与黄蜂女:量子狂潮》观后感

上周看了电影《蚁人与黄蜂女&#xff1a;量子狂潮》&#xff0c;整体有一种像是打开了一座新世界的大门&#xff0c;探索新知的感觉&#xff0c;也许是电影3D效果或者现场原因&#xff0c;给人展示量子世界的时候&#xff0c;总是看不清楚画面细节&#xff0c;稍微有些模糊&…

一文读懂Linux中的进程、线程

一文读懂Linux中的进程、线程 大家好&#xff0c;我是木荣君&#xff0c;这次我们来聊一聊Linux中进程和线程。进程和线程的概念非常重要&#xff0c;本篇来详细介绍下这两者的概念。我们在实际开发中&#xff0c;经常会听到这两个词&#xff0c;如果我们不了解这些词背后的概念…

深入浅出C++ ——二叉搜索树

文章目录一、二叉搜索树概念二、二叉搜索树操作1. 二叉搜索树的查找2. 二叉搜索树的插入3. 二叉搜索树的删除三、二叉搜索树的实现四、二叉搜索树的性能分析一、二叉搜索树概念 二叉搜索树又称二叉排序树/二次查找树&#xff0c;它是一棵空树或者是每颗子树都具有以下性质的二叉…

Spring Boot 框架 集成 Knife4j(内含源代码)

Spring Boot 框架 集成 Knife4j&#xff08;内含源代码&#xff09; 源代码下载链接地址&#xff1a;https://download.csdn.net/download/weixin_46411355/87480176 目录Spring Boot 框架 集成 Knife4j&#xff08;内含源代码&#xff09;源代码下载链接地址&#xff1a;[htt…

Bmp图片格式介绍

Bmp图片格式介绍 介绍 BMP是英文Bitmap&#xff08;位图&#xff09;的简写&#xff0c;它是Windows操作系统中的标准图像文件格式&#xff0c;能够被多种Windows应用程序所支持。随着Windows操作系统的流行与丰富的Windows应用程序的开发&#xff0c;BMP位图格式理所当然地被…

JetPack板块—Android X解析

Android Jetpack简述 AndroidX 是Android团队用于在Jetpack中开发&#xff0c;测试&#xff0c;打包&#xff0c;发布和版本管理的开源项目。相比于原来的Android Support库,AndroidX 可以称得上是一次重大的升级改进。 和Support库一样&#xff0c;AndroidX与Android 操作系…

投出1000份简历,苦于软件测试没有项目经验,全部石沉大海,辞职5个月,我失业了......

想要找一份高薪的软件测试工作&#xff0c;简历项目必不可少&#xff08;即使是应届生&#xff0c;你也要写上实习项目&#xff09;。所以很多自学的朋友找工作时会碰到一个令人颇感绝望的拦路虎&#xff1a;个人并没有实际的项目工作经验怎么办&#xff1f; 怎么办&#xff1f…

6.1 反馈的基本概念及判断方法

一、反馈的基本概念 1、什么是反馈 反馈也称为 “回授”。在电子电路中&#xff0c;将输出量&#xff08;输出电压或输出电流&#xff09;的一部分或全部通过一定的电路形式作用到输入回路&#xff0c;用来影响其输入量&#xff08;放大电路的输入电压或输入电流&#xff09;…

【软件测试】接口自动化测试你真的会做吗?资深测试工程师的总结......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 目的&#xff1f; 通…

离线环境拷贝迁移 conda envs 环境(蛮力方法,3行命令)

前言 最近要使用 GPU 服务器做实验&#xff0c;可惜的是&#xff0c;有网络连接的服务器显卡旧&#xff0c;算力不够&#xff1b;显卡较新的机器没有联网。于是有需求将旧机器上配置好的 conda 环境迁移至新机器。网上给的默认方法生成 yaml 文件迁移等 需要联网&#xff0c;只…

孪生生产线:法兰工厂数据驱动的颠覆性创新

2018 年&#xff0c;世界经济论坛(WEF)携手麦肯锡公司共同倡议并正式启动了全球“灯塔工厂网络项目”(Lighthouse Network)&#xff0c;共同遴选率先应用工业革命 4.0 技术实现企业盈利和持续发展的创新者与示范者。这就使得工厂系统需要对各流水线及生产运行成本方面进行多角度…

Neuron Selectivity Transfer 原理与代码解析

paper&#xff1a;Like What You Like: Knowledge Distill via Neuron Selectivity Transfercode&#xff1a;https://github.com/megvii-research/mdistiller/blob/master/mdistiller/distillers/NST.py本文的创新点本文探索了一种新型的知识 - 神经元的选择性知识&#xff0c…

整型在内存中的存储(详细剖析大小端)——“C”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容是整型在内存中的存储噢&#xff0c;现在&#xff0c;就让我们进入整型在内存中的存储的世界吧 数据类型详细介绍 整型在内存中的存储&#xff1a;原码、反码、补码 大小端字节序介绍及判断 数据类型介绍 前面我们已经学…

【扫盲】数字货币科普对于完全不了解啥叫比特币的小伙伴需要的聊天谈资

很多人并不清楚&#xff0c;我们时常听说的比特币&#xff0c;以太坊币&#xff0c;等等这些东西到底是一场骗局还是一场货币革命&#xff1f; 下面就围绕这数字货币的历史以及一些应用场景开始分析这个问题。 一、 开端 一切从2008年中本聪&#xff08;Satoshi Nakamoto&…

shiro反序列化漏洞与无依赖CB链分析

环境搭建 git clone https://github.com/apache/shiro cd shiro git checkout shiro-root-1.2.4将 shiro/samples/web/pom.xml 中的jstl依赖改为1.2: <dependency><groupId>javax.servlet</groupId><artifactId>jstl</artifactId><version&g…

【数据结构与算法】3.(单向、无向、带权)图,广度、深度优先搜索,贪心算法

文章目录1.图简介2.图的存储方式2.1.邻接矩阵存储方法2.2.邻接表存储方法3.有向、无向图和查询算法3.1.数据结构3.2.广度优先算法BFS3.3.深度优先算法DFS3.3.1.DFS查询单条路径3.3.2.DFS查询所有路径4.带权图和贪心算法4.1.贪心算法4.2.基于带权无向图使用贪心算法查询最优路径…

混合精度训练,FP16加速训练,降低内存消耗

计算机中的浮点数表示&#xff0c;按照IEEE754可以分为三种&#xff0c;分别是半精度浮点数、单精度浮点数和双精度浮点数。三种格式的浮点数因占用的存储位数不同&#xff0c;能够表示的数据精度也不同。 Signed bit用于控制浮点数的正负&#xff0c;0表示正数&#xff0c;1表…

MAC地址IP地址 端口

网络结构&#xff1a; 服务器-客户机&#xff08;C/S&#xff09;Client-Server结构&#xff0c;如QQ,LOL都拥有客户端 优点&#xff1a;响应速度快&#xff0c;形式多样&#xff0c;安全新较高缺点&#xff1a;安装软件和维护&#xff0c;不能跨平台LINUX/windows/MAC浏览器-…

C语言——柔性数组

目录0. 前言1. 思维导图2. 柔性数组的特点3. 柔性数组的使用4. 柔性数组的优势5. 结语0. 前言 柔性数组是在C99标准时引入&#xff1a; 结构中的最后一个元素允许是未知大小的数组&#xff0c;这就叫柔性数组成员。 代码示例&#xff1a; typedef struct flexible_arr {int a…