juc多线程:jmm内存模型与volatile内存语义(代码片段)

张维鹏 张维鹏     2023-01-12     599

关键词:

一、JMM 内存模型:

1、什么是 JMM 内存模型:

        Java 内存模型是 Java 虚拟机定义的一种多线程访问 Java 内存各个变量的访问规范,主要围绕如何解决并发过程中的原子性、可见性、有序性这三个问题来解决线程的安全问题。

        Java 内存模型将内存分为了主内存和工作内存(也称为栈空间)。主内存存放所有的共享变量,所有线程都可以访问。每个线程都有自己的工作内存,存储了该线程使用到的变量的副本,线程对变量的所有操作都必须在自己的工作内存中完成,不能直接操作主存中的变量。操作时,首先将变量从主内存拷贝到自己的工作内存中,然后在自己的工作内存中对变量进行操作,操作完成后再将变量写回主存。不同的线程间也无法直接访问对方的工作内存的变量,线程间的变量值的传递必须通过主内存来完成。

(1)原子性:原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。

(2)可见性:可见性指的是,当一个线程修改了某个共享变量的值,其他线程能够马上得知这个修改的值。

串行程序不存在可见性问题,因为在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个修改过的变量值。但在多线程环境中可就不一定了,因为线程对共享变量的操作都是拷贝到各自的工作内存中进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题

(3)有序性:对于多线程环境,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,有可能出现乱序现象

指令重排序:计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,在单线程条件下,指令重排序可以保证执行结果的一致性,但是在多线程条件下,这些重排优化可能会导致程序出现内存可见性问题,不能保证多线程间语义一致性

2、原子性、可见性、有序性问题的解决措施:

(1)原子性问题:除了 JVM 自身提供对基本数据类型读写操作的原子性外,对于方法级别或者代码级别的原子性操作,可以使用 synchronized 关键字或者重入锁 ReentrantLock 保证程序执行的原子性。

(2)可见性问题:工作内存与主内存同步延迟现象导致的可见性问题,可以使用 synchronized 关键字、Lock 或者 volatile 关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

(3)有序性问题:可以利用 volatile、synchronized 关键字解决。

3、JMM 的 as-if-serial 规则和 happens-before 规则:

(1)as-if-serial:无论编译器和处理器如何进行重排序,单线程程序的执行结果不会改变。

(2)happens-before:在多线程程序开发中,如果仅靠 synchronized 和 volatile 关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,所以 Java 内存模型中提供了内置的 happens-before 规则来辅助处理前面提到的问题,它是判断数据是否存在竞争、线程是否安全的依据,从而保证线程安全。一个操作 happens-before 另一个操作,表示第一个的操作结果对第二个操作可见,并且第一个操作的执行顺序也在第二个操作之前。但这并不意味着 JVM 必须按照这个顺序来执行程序。如果重排序后的执行结果与按 happens-before 关系执行的结果一致,JVM 也允许重排序的发生。happens-before 原则内容如下:

  • 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

  • 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

  • volatile 规则:volatile 变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile 变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

  • 线程启动规则:线程的 start() 方法先于它的每一个动作,即如果线程A在执行线程B的 start() 方法之前修改了共享变量的值,那么当线程B执行 start() 方法时线程A对共享变量的修改对线程B可见。

  • 传递性:A先于B ,B先于C,那么A必然先于C

  • 线程终止规则:线程的所有操作先于线程的终结,Thread.join() 方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程从线程B的 join() 方法成功返回后,线程B对共享变量的修改将对线程A可见。

  • 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测线程是否中断。

  • 对象终结规则:对象的构造函数执行,结束先于 finalize() 方法。

二、volatile 内存语义:

1、volatile 的作用:

        volatile 是 Java 虚拟机提供的轻量级同步机制,是线程不安全的,volatile 跟可见性和有序性有关,被 volatile 修饰的共享变量,具有以下两个作用:

(1)保证不同线程对该变量操作的内存可见性:当变量被 volatile 修饰时,那么对它的修改会立刻刷新到主存,同时其他线程操作 volatile 变量时,JMM 会把该线程对应的工作内存置为无效,那么该线程就只能从主存中重新读取共享变量,保证读取到最新值

通过 synchronized 和 Lock 也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是相比于 volatile,synchronized 和 Lock 的开销都更大。

(2)禁止指令重排序

2、内存屏障:

        volatile 在内存中的语义是通过内存屏障实现,即可见性和禁止重排优化。把加入 volatile 关键字的代码和未加入 volatile 关键字的代码都生成汇编代码,会发现加入 volatile 关键字的代码会多出一个内存屏障指令,它是一个 CPU 指令。内存屏障提供了以下功能:

  • 告诉编译器和处理器,重排序时不能把后面的指令重排序到内存屏障之前的位置,从而避免多线程环境下出现乱序执行现象

  • 保存某些变量的内存可见性,利用该特性实现 volatile 的内存可见性

3、volatile 的原子性:

        volatile 的两点内存语义能保证可见性和有序性,但是不能保证原子性:对单个 volatile 变量的读/写具有原子性,但是对于类似 volatile++ 这样的复合操作就无能为力了,要想保证原子性,只能借助于 synchronized、Lock 或者并发包下的 atomic 的原子操作类了。

4、volatile使用场景:

(1)状态量标记:这种对变量的读写操作,标记为 volatile 可以保证修改对线程立刻可见,效率也比 synchronized、Lock 有一定的提升。

public class VolatileSafe 
​
    volatile boolean close;
​
    public void close()
        close=true;
    
​
    public void doWork()
        while (!close)
            System.out.println("safe....");
        
    

        对于 boolean 变量 close 值的修改属于原子性操作,因此可以通过使用 volatile 修饰变量 close,使用该变量对其他线程立即可见,从而达到线程安全的目的。

(2)单例模式的实现,典型的双重检查锁定(DCL):

        这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给 instance 加上了 volatile。指令重排序会导致,当一条线程访问的 instance 不为 null 时,但是实际上 instance 实例未必已初始化完成,也就造成了线程安全问题。

public class DoubleCheckLock 
    //禁止指令重排优化
    private volatile static DoubleCheckLock instance;
​
    private DoubleCheckLock()
    public static DoubleCheckLock getInstance()
​
        //第一次检测
        if (instance==null)
            //同步
            synchronized (DoubleCheckLock.class)
                if (instance == null)
                    //多线程环境下可能会出现问题的地方
                    instance = new DoubleCheckLock();
                
            
        
        return instance;
    

单例模式的线程安全性:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法:

  • (1)饿汉式单例模式的写法:线程安全

  • (2)懒汉式单例模式的写法:非线程安全

  • (3)双检锁单例模式的写法:线程安全

推荐文章:https://blog.csdn.net/javazejian/article/details/72772461

java并发编程-jmm内存模型与volatile关键字

1Java内存模型-JMM内存模型? Java内存模型(JavaMemoryModel简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。JVM... 查看详情

玩命死磕java内存模型(jmm)与volatile关键字底层原理(代码片段)

点击上方关注“终端研发部”设为“星标”,和你一起掌握更多数据库知识引言本篇文章结合我个人对Java内存模型的理解以及相关书籍资料为前提全面剖析JMM内存模型,本文的书写思路先阐述JVM内存模型、硬件与OS(... 查看详情

玩命死磕java内存模型(jmm)与volatile关键字底层原理(代码片段)

点击上方关注“终端研发部”设为“星标”,和你一起掌握更多数据库知识引言本篇文章结合我个人对Java内存模型的理解以及相关书籍资料为前提全面剖析JMM内存模型,本文的书写思路先阐述JVM内存模型、硬件与OS(... 查看详情

java多线程和并发,jmm(java内存模型)

...主内存和工作内存3.JMM如何解决可见性问题-指令重排序4.Volatile十、JMM(Java内存模型)(暂时没有理解)1.什么是JMM 2.JMM的主内存和工作内存(1)主内存 (2)工作内存 (3)主内存和工作内存数据存储类型以及操作方... 查看详情

juc学习之共享模型之内存(代码片段)

...可见性退不出的循环解决方法两阶段终止模式interrupt实现volatile实现同步模式之Balking定义实现指令重排有序性指令级并行原理名词ClockCycleTimeCPIIPC鱼罐头的故事指令重排序优化支持流水线的处理器SuperScalar处理器CPU缓存结构原理1.... 查看详情

java——聊聊juc中的java内存模型(jmm)(代码片段)

...三大特性3.1原子性3.2可见性3.3有序性4.JMM规范下,多线程对变量的读写过程5.JMM规范下,多线程先行发生原则之happens-before5.18条规则5.2小案例1.CPU缓存模型我们先来看一下任务管理器性能这一块的东西。(那张图上传 查看详情

java——聊聊juc中的java内存模型(jmm)(代码片段)

...三大特性3.1原子性3.2可见性3.3有序性4.JMM规范下,多线程对变量的读写过程5.JMM规范下,多线程先行发生原则之happens-before5.18条规则5.2小案例1.CPU缓存模型我们先来看一下任务管理器性能这一块的东西。(那张图上传 查看详情

java——聊聊juc中的java内存模型(jmm)(代码片段)

...三大特性3.1原子性3.2可见性3.3有序性4.JMM规范下,多线程对变量的读写过程5.JMM规范下,多线程先行发生原则之happens-before5.18条规则5.2小案例1.CPU缓存模型我们先来看一下任务管理器性能这一块的东西。(那张图上传 查看详情

volatile可见性分析(代码片段)

...il.concurrentjava.util.concurrent.atomicjava.util.concurrent.locks谈谈对Volatile的理解Volatile在日常的单线程环境是应用不到的Volatile是Java虚拟机提供的轻量级的同步机制(三大特性)保证可见性不保证原子性禁止指令重排JMM是什么JMM是Java内存... 查看详情

java——聊聊juc中的volatile与内存屏障(代码片段)

文章目录:1.volatile内存语义2.内存屏障2.1粗分两种:写屏障2.2粗分两种:读屏障2.3细分四种3.volatile可见性介绍 4.volatile无原子性介绍5.volatile有序性介绍6.如何正确使用volatile?6.1 单一赋值可以,但是含有符合... 查看详情

全面理解java内存模型(jmm)及volatile关键字(代码片段)

本篇主要结合博主个人对Java内存模型的理解以及相关书籍内容的分析作为前提,对JMM进行较为全面的分析,本篇的写作思路是先阐明Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具体关系,... 查看详情

jvm细谈java内存模型jmm(代码片段)

...,因此JMM需要提供原子性、可见性、有序性的保证。volatile关键字volatile关键字的特性是Java内存模型规定的1.保证被此关键字修饰的变量对所有线程的可见性2.禁止指令重排序编译器重排序–>JVM重排序–>内存重排序内存... 查看详情

并发编程-java内存模型和volatile

...模型2.1、JMM和JVM2.2、Java内存模型(JMM)2.2.1、案例2.2.2、volatile作用2.3、重排序2.3.1、什么是重排序2.3.2、重排序如何影响线程安全2.4、总结1、内存模型概念我们都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行... 查看详情

volatile语义

volatile在Java内存模型(JMM)中,保证共享变量对所有线程可见,但不保证原子性。volatile语义是同步,通过共享变量的方式,完成线程间的通信。为什么需要volatileJava内存模型中抽象、简化了计算机物理设备,分成工作内存和主... 查看详情

重点知识学习(8.2)--[jmm(java内存模型),并发编程的可见性原子性有序性,volatile关键字,保持原子性,cas思想](代码片段)

...发编程的可见性3.并发编程的有序性4.并发编程的原子性5.volatile关键字6.保持原子性:加锁,JUC原子类加锁JUC原子类7.CAS(Compare-And-Swap)1.JMM(JavaMemoryModel)Java内存模型[JavaMemoryModel]速度排序:CPU>内存>I/O设备CPU增加了缓存,以... 查看详情

jmm简介

...性、有序性这三个特性而建立的模型。可见性:JMM提供了volatile变量定义、final、synchronized块来保证可见性。例如:线程a在将共享变量x=1写入主内存的时候,如何保证线程b读取共享变量x的值为1,这就是JMM做的事情。JMM通过控制... 查看详情

jmm与并发三大特性

...程的修改操作,因此Java提供三种方式保障可见性:使用volatile关键字,该关键字实现基础为MESI缓存一致性协议通过synchornized关键字通过JUC提供的显示锁3、JMM与有序性Java内存模型中 查看详情

了解并发内存模型(jmm)和volatile(代码片段)

上一篇:了解JVM中的GC我们都知道,多个线程同时操作一个数据会有并发问题,那为什么会出现并发问题呢,产生并发问题的原因是什么呢?产生并发问题的原因一般产生并发问题无外乎都跟以下三种特性相关原子性可见性有序性... 查看详情