【Java 并发编程】一文详解 Java volatile关键字

news2024/11/19 21:23:15

一文详解 Java volatile关键字

  • 1. JMM(Java Memory Model)
    • 1.1 现代计算机的内存模型
    • 1.2 JMM 简介
    • 1.3 JMM 的三大特性
    • 1.4 指令重排
    • 1.5 happens-before
      • 1.5.1 happens-before 规则
      • 1.5.2 总结
    • 1.6 as-if-serial
  • 2. volatile 关键字
    • 2.1 volatile 的内存语义
    • 2.2 volatile 的三大特性
      • 2.2.1 保证可见性
      • 2.2.2 不保证原子性
      • 2.2.3 禁止指令重排(保证有序性)
        • volatile 有关禁重排的行为:
        • 内存屏障四大指令插入情况
    • 2.3 volatile 适用场景
      • (1)单一赋值可以,但是含复合运算赋值不可以(i++ 之类)
      • (2)状态标志,判断业务是否结束
      • (3)开销较低的读,写锁策略
      • (4)DCL 双重检查锁

先一起看一段代码:

/**
 * @author pointer
 * @date 2023-04-15 15:16:47
 */
public class NewThread extends Thread {

    private boolean flag = false;

    public boolean isFlag() {
        return flag;
    }

    @Override
    public void run() {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        flag = true;
        System.out.println("flag = " + flag);
    }

    public static void main(String[] args) {
        NewThread thread = new NewThread();
        thread.start();

        while (true) {
            if (thread.flag) {
                System.out.println("我已经是 true 了");
            }
        }
    }
}

你会发现,永远都不会输出 “我已经是 true 了”,讲道理线程改了flag变量为 true,主线程是可以访问的到的!

为会出现这个情况呢?那就不能不说一下JMM(Java内存模型)

1. JMM(Java Memory Model)

1.1 现代计算机的内存模型

其实早期计算机中CPU和内存的速度是差不多的,但在现代计算机中,CPU的指令速度远超内存的存取速度。由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以现代计算机系统不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(CPU Cache Memory)来作为内存与处理器之间的缓冲。

将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence)

在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)
在这里插入图片描述

1.2 JMM 简介

Java 内存模型(Java Memory Model,JMM),是一种抽象的模型,并不真实存在。

Java 内存模型是一种规范,定义了线程和主内存之间的抽象关系:

  • 所有的共享变量都存储在主内存(Main Memory)中,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。

  • 线程对变量的所有操作(读/写)都必须在本地内存中进行,而不能直接读写主内存。

  • 不同的线程之间无法直接访问对方本地内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

Java 内存模型的抽象图:

在这里插入图片描述
为了支持 JMM,Java 定义了8种原子操作,用来控制主内存与工作内存之间的交互:

  1. lock - 锁定:作用于主内存的变量,把一个变量标识为一条线程独占状态。

  2. unlock - 解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  3. read - 读取:作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

  4. load - 载入:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

  5. use - 使用:作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

  6. assign - 赋值:作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  7. store - 存储:作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

  8. write - 写入:作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

1.3 JMM 的三大特性

原子性、有序性、可见性是并发编程中非常重要的基础概念,JMM的很多技术都是围绕着这三大特性展开。

  • 原子性:原子性指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。

  • 可见性:可见性指的是一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改。

  • 有序性:有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排

原子性、可见性、有序性都应该怎么保证呢?

  • 原子性:JMM只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用 synchronized

  • 可见性:Java 是利用 volatile 关键字来保证可见性的,除此之外,finalsynchronized 也能保证可见性。

  • 有序性:synchronized 或者 volatile 都可以保证多线程之间操作的有序性。

1.4 指令重排

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。

  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 Java 源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如图:

在这里插入图片描述
指令重排也是有一些限制的,有两个规则 happens-beforeas-if-serial 来约束。

1.5 happens-before

JMM 向程序员做出保证:一个操作 happens-before 于另一个操作,那么第一个操作的执行结果将对第二个执行结果可见,而且第一个操作的执行顺序排在第二个顺序之前。

两个操作之间存在 happens-before 规则,Java 平台并不一定按照规则定义的顺序来执行。 这么做的原因是因为,我们程序员并不关心两个操作是否被重排序,只要保证程序执行时语义不能改变就好了。

happens-before 这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

1.5.1 happens-before 规则

  1. 程序顺序规则:一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。

  2. 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。

  3. volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

  5. start() 规则:这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

  6. join() 规则:如果线程A执行操作 ThreadB.join() 并成功返回,那么线程B中的任意操作 happens-before 于线程A从 ThreadB.join() 操作成功返回。

1.5.2 总结

在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。

JMM 的设计分为两部分,一部分是面向我们程序员提供的,也就是 happens-before 规则,它通俗易懂的向我们程序员阐述了一个强内存模型,我们只要理解 happens-before 规则,就可以编写并发安全的程序了。 另一部分是针对 JVM 实现的,为了尽可能少的对编译器和处理器做约束,从而提高性能,JMM 在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。 我们只需要关注前者就好了,也就是理解 happens-before 规则。毕竟我们是做程序员的,术业有专攻,能写出安全的并发程序就好了。

1.6 as-if-serial

as-if-serial 语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守 as-if-serial 语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例:

double pi = 3.14;   // A
double r = 1.0;   // B 
double area = pi * r * r;   // C

编译器可以把 A、B 的执行顺序颠倒,但不可把 C 的执行优先级提到 A 和 B 之上。

2. volatile 关键字

2.1 volatile 的内存语义

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。

当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。

所以 volatile 的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

2.2 volatile 的三大特性

2.2.1 保证可见性

相比 synchronized 的加锁方式来解决共享变量的内存可见性问题,volatile 就是更轻量的选择,它没有上下文切换的额外开销成本。

volatile 可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存,当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。

例如,我们声明一个 volatile 变量 volatile int x = 0,线程A修改 x = 1,修改完之后就会把新的值刷新回主内存,线程B读取 x 的时候,就会清空本地内存变量,然后再从主内存获取最新值。

2.2.2 不保证原子性

原子性指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。

在这里插入图片描述

多线程环境下,“数据计算” 和 “数据赋值” 操作可能多次出现,即操作非原子。若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致。

对于 volatile 变量,JVM 只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。由此可见 volatile 解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步

i++ 为例,不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上 1,分3步完成。如果第二个线程在第一个线程读取旧值和写回新值期间(上图所指三步期间)读取 i 的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于 add 方法必须使用 synchronized 修饰,以便保证线程安全。

2.2.3 禁止指令重排(保证有序性)

重排序:是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序。不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序。但重排后的指令绝对不能改变原有的串行语义。

数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。

具体解释见本文 1.4 节。

volatile 有关禁重排的行为:

  1. 当第一个操作为 volatile 读时,不论第二个操作是什么,都不能重排序。这个操作保证了 volatile 读之后的操作不会被重排到 volatile 读之前。(volatile 读之后的操作,都禁止重排序到 volatile 之前

  2. 当第二个操作为 volatile 写时,不论第一个操作是什么,都不能重排序。这个操作保证了 volatile 写之前的操作不会被重排到 volatile 写之后 。 (volatile 写之前的操作,都禁止重排序到 volatile 之后

  3. 当第一个操作为 volatile 写时,第二个操作为 volatile 读时,不能重排。(volatile 写之后 volatile 读,禁止重排序的

内存屏障四大指令插入情况

  1. 在每个 volatile 写操作的前面插入一个 StoreStore 屏障,保证在 volatile 写之前,其前面的所有普通写操作都已经刷新到主内存中。

  2. 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障,避免 volatile 写与后面可能有的 volatile 读/写操作重排序。

在这里插入图片描述

  1. 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障,禁止处理器把上面的 volatile 读与下面的普通读重排序。

  2. 在每个 volatile 读操作的后面插入一个 LoadStore 屏障,禁止处理器把上面的 volatile 读与下面的普通写重排序。
    在这里插入图片描述

来源:https://blog.csdn.net/weixin_43899792/article/details/124492448

2.3 volatile 适用场景

(1)单一赋值可以,但是含复合运算赋值不可以(i++ 之类)

volatile int a = 10;

(2)状态标志,判断业务是否结束

使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束。

理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换。

例子:判断业务是否结束 volatile boolean flag = false

(3)开销较低的读,写锁策略

使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销。

理由:利用 volatile 保证读取操作的可见性;利用 synchronized 保证复合操作的原子性。

 // 当读远多于写,结合使用内部锁和 volatile 变量来成少同步的开销
 public class Counter{
     private volatile int value;
     public int getValue(){
         // 利用 volatile 保证读取操作的可见性
         return value;
     }
     public synchronized int increment(){
     	 // 利用 synchronized 保证复合操作的原子性
         return value++; 
     }
 }

(4)DCL 双重检查锁

隐患: 多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取。

class SafeDoubleCheckSingleton{

    private static SafeDoubleCheckSingleton singleton;
    
    // 私有化构造方法
    private SafeDoubleCheckSingleton(){}
    
    // 双锁设计
    public static SafeDoubleCheckSingleton getSingleton(){
        if(singleton == null){
            // 1. 多线程并非创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if(singleton == null){
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        
        // 2. 对象创建完毕后,执行 getInstance() 将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

修正方法1:加 volatile(也即正确的DCL)

原理:利用 volatile禁止 “初始化对象” 和 “和设置singleton指向内存空间” 的重排序

// 通过 volatile 声明,实现线程安全的延迟初始化
private volatile static SafeDoubleCheckSingleton singleton;

修正方法2:静态内部类

// 静态内部类
class SingletonDemo{
    private SingletonDemo(){
 
    }
    // 私有的静态内部类,独有一份
    private static class SingletonDemoHandler{
        private static SingletonDemo instance = new SingletonDemo();
    }
    // 外部暴露
    public static SingletonDemo getInstance(){
        return SingletonDemoHandler.instance;
    }
}

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

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

相关文章

mssql 中msdb 权限说明

msdb MSDB是Microsoft SQL Server中一个系统数据库,它存储了SQL Server代理作业,备份和还原,数据库维护计划,邮件,日志和数据库相关的其他信息。MSDB数据库包含了许多系统表和视图,例如sysjobs、sysjobhis…

什么是网络——计算机网络发展史

个人简介:云计算网络运维专业人员,了解运维知识,掌握TCP/IP协议,每天分享网络运维知识与技能。座右铭:海不辞水,故能成其大;山不辞石,故能成其高。个人主页:小李会科技的…

面了个4年的测试,自动化测试都不会,真是醉了...

最近面试了一个 4 年经验的软件测试工程师,简历和个人介绍都提到了精通自动化测试和性能测试,于是我就问了几个自动化测试方面的问题: 你使用过哪些自动化测试工具?它们之间有什么区别? 请解释一下 TestNG 框架&#…

分享丨从事设计10年后转行产品经理,总结6种工作差异

作者从设计转行到产品管理,在获得关于产品策略方面更大的话语权同时,也失去了专注于工作本身的自由。经历了角色转变这一过程后,分享了她对于这两个角色在日常生活中差异的些许反思,同时也鼓励大家在转变角色前,先深入…

增强型语言模型——走向通用智能的道路?!?

增强型语言模型——走向通用智能的道路?!? FesianXu 20230325 at Baidu Search Team 前言 继AlphaGo在2016年横扫了人类棋手之后,2022年末的chatGPT横空出世,再次引爆了全球对人工智能技术的热情。基于语言模型的chat…

【react从入门到精通】React父子组件通信方式详解(有示例)

文章目录 前言React技能树通过 props 实现父子组件通信通过 state 实现父子组件通信通过回调函数实现父子组件通信使用 React Context 实现组件通信总结写在最后 前言 在上一篇文章《JSX详解》中我们了解了什么是jsx以及jsx的语法规则。 本文中我们将详细了解React父子组件通信…

详解SpringBoot外部化配置

SpringBoot外部化配置(基于2.4.0以后) Spring Boot可以让你将配置外部化,这样你就可以在不同的环境中使用相同的应用程序代码。 你可以使用各种外部配置源,包括Java properties 文件、YAML文件、环境变量和命令行参数。 属性值可…

【刷题笔记】二维数组地址计算+算法分析+进制转换

目录 一、二维数组地址计算 题目: 分析: 解答: 二、算法分析举例 题目: 分析: 解答: 三、进制转换 题目: 分析: 解答: 一、二维数组地址计算 题目&#xff…

3.19 makefile用法及变量(自定义变量、自动变量、隐含变量)

目录 makefile概述 makefile创建变量的目的 自定义变量 makefile变量的赋值 自动变量 makefile隐含变量 makefile概述 makefile重要性 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力 make是一个命令工具,是一个解释makefil…

Docker高频使用命令总结(镜像与容器命令)

目录 一.Docker常用命令总结 1.镜像命令管理 2.容器命令管理 二.Docker镜像操作命令 1.docker search:搜索镜像 2.docker pull:下载镜像 3.docker push:上传镜像 4.docker images:查看本地镜像 5.docker inspect &#x…

全球最大蒸馏体量干邑集团,邀请酣客老王讲授产品设计大师课

酒作为文化符号,寄托着全人类的精神追求,与历史、艺术为伍,充当着国际间友好交流的使者。为了弘扬中国白酒文化,把品质卓越的中国酱酒带到全世界,也为了给中国的烈酒爱好者讲清中外烈酒之间的工艺与文化差异,酣客君丰发起了“中国酱酒世界行”,不远万里探访欧洲各国名酒产区,一…

Flutter学习之旅 -网格布局

GridView列表三种形式 可以通过GridView.count实现网格布局 /* 格式: GridView.count(crossAxisCount: 一行显示数量,children: [component(),...],) */ class MyHomePage extends StatelessWidget {const MyHomePage({Key? key}) : super(key: key);overrideWidget build(B…

如何利用问卷工具助力活动开展,实现高效数据收集?

问卷调查是一种常用的活动开展方式,它可以帮助我们更好地了解参与者的需求和意见,为活动的开展提供有力的参考和依据。 1、了解期望和需求:在活动中,我们可以事先通过问卷调查了解参与者的需求、意见、对活动的需求和期望&#x…

PCB制板之前的DFM分析

PCB制板之前的DFM分析 1.华秋DFM分析2.AD18输出IPC文件3.华秋DFM分析开短路 1.华秋DFM分析 1.打开华秋DFM软件,目前先用这软件做DFM分析,主要是简单容易上手操作,并且分析完成之后可以一键下单。 2.将输出的gerber文件导入华秋DFM这个软件…

ubuntu18.04 + 3060 安装测试

分区 在这里,我们进行手动分区,假设你留出的空闲分区为 80G,点击空闲盘符,点击""进行分区,如下: 1)efi:如果是单硬盘,在唯一的一个空闲分区上添加&#xff0…

为项目创建Qframework!

liangxiegame/QFramework: Unity3D System Design Architecture (github.com) 下载这个download 在unity中打开! 就可以啦 ———— 这里总结一下UI和自动脚本两个非常常用的功能: UI https://www.bilibili.com/video/BV1QK411m7g4/?spm_id_from…

美格智能发布基于高通QCS8550处理器的高算力AI模组SNM970,定义未来终端新体验

近日,全球领先的无线通信模组及解决方案提供商美格智能发布了高算力AI模组SNM970。该产品是行业首批基于高通QCS8550处理器开发的AI模组产品,并凭借卓越的8核高通Kryo™ CPU、综合AI算力高达48Tops、支持Wi-Fi 7等特性,助力将运算效能和灵活性…

【Go Web开发】Web初识、RESTful架构和RESTful API详解、Gin框架的安装和简单使用

博主简介:努力学习的大一在校计算机专业学生,热爱学习和创作。目前在学习和分享:数据结构、Go,Java等相关知识。博主主页: 是瑶瑶子啦所属专栏: Go语言核心编程近期目标:写好专栏的每一篇文章 文章目录 一、…

YonLinker连接集成平台构建新一代产业互联根基

近日,由用友公司主办的“2023用友BIP技术大会“在用友产业园(北京)盛大召开,用友介绍了更懂企业业务的用友BIP-iuap平台,并发布了全面数智化能力体系,助力企业升级数智化底座,加强加速数智化推进…