JUC - 多线程之JMM;volatile(七)

news/2024/7/7 19:38:37

一、JMM

Java Memory Model(JMM)Java内存模型,区别与java内存结构。JMM定义了一套在多线程读写共享数据(变量、数组)时,对数据的可见性、有序性和原子性的规则和保障

(一)JMM结构规范

JMM规定了所有的变量都存储在主内存(Main Memory)中

每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)

不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成

在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响

(二)主内存和本地内存结构

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。本地内存它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化之后的一个数据存放位置

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类 型的变量来说,load、store、read和write操作在某些平台上允许例外)

1、lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态

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

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

4、load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中

5、use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机 遇到一个需要使用到变量的值,就会使用到这个指令

6、assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变 量副本中

7、store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中, 以便后续的write使用

8、write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内 存的变量中

JMM对这八种指令的使用,制定了如下规则

1、不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须 write

2、不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

3、不允许一个线程将没有assign的数据从工作内存同步回主内存

4、一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量 实施use、store操作之前,必须经过assign和load操作

5、一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解 锁

6、如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前, 必须重新load或assign操作初始化变量的值

7、如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

8、对一个变量进行unlock操作之前,必须把此变量同步回主内存

(三)JMM三个特征

Java内存模型保证了并发编程中数据的原子性、可见性、有序性

1、原子性

原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰

多线程情况下,对同一个对象进行操作时,会导致字节码指令交错执行,从而产生原子性问题,可以通过synchronize关键字解决

原子性操作指相应的操作是单一不可分割的操作。在我们学化学这门课程的时候,对于里面讲到的原子性相信大家都非常明白,原子是微观世界中最小的不可再进行分割的单元,原子是最小的粒子。java里面的原子性操作也是如此,它代表着一个操作不能再进行分割是最小的执行单元。

原子性类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态

i = 0;       //1
j = i ;      //2
i++;         //3
i = j + 1;   //4

上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是 

1 在Java中,对基本数据类型的变量和赋值操作都是原子性操作; 
2 中包含了两个操作:读取i,将i值赋值给j 
3 中包含了三个操作:读取i值、i + 1 、将+1结果赋值给i; 
4 中同三一样

在Java中,对基本数据类型的变量和赋值操作都是原子性操作 

i = 0;

2、可见性

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改

3、有序性(指令重排)

如果在本线程内观察,所有的操作都是有序的;(线程内表现为串行的语义)如果在一个线程中观察另外一个线程,所有的操作都是无序的

多线程情况下,JVM会进行指令重排,会影响有序性

有序性最终表述的现象是CPU是否按照既定代码顺序执行依次执行指令。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行,在单线程的情况下只要保证最终执行结果正确即可

int i = 0;            //语句1  
boolean flag = false; //语句2
i = 1;                //语句3  
flag = true;          //语句4

上面代码最终执行结果是i=1、flag=true,在不影响这个结果的情况下语句2可能比语句1先执行,语句4可能比语句3先执行

JMM提供了内置解决方案(happen-before 原则)及其外部可使用的同步手段(synchronized/volatile 等),确保了程序执行在并发编程中的 原子性可视性及其有序性

(四)happen-before原则

happen-before是在JMM中用来实现并发编程中的有序性的。主要包括了以下八个规则:

1、程序顺序性原则:应该线程按照代码的顺序执行

2、锁原则:如果一个对象已经加锁,那么后续的再对其加锁,一定发生在解锁之后

3、对象终结原则:对象的构造函数一定发生在对象终结之前

4、volatile变量规则:被volatile修改的变量写操作,Happens-Before于任意后续对这个变量操作的读

跟线程相关的4个原则

1、线程启动原则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作

2、线程中断原则:线程中断发生在线程中断检查之前

3、线程的结束原则:如果线程A中执行了 ThreadB.join(),那么线程B的所有操作都发生在线程A的ThreadB.join()之后的操作

4、线程传递性原则:A happen-before B, B happen-before C ,那么A 一定 happen-before C
 

二、volatile

volatile关键字是Java虚拟机提供的的最轻量级的同步机制,它作为一个修饰符,用来修饰变量

volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性

(一)保证可见性

如下,我们有2条线程 t1 和 main主线程,num在主线程中改为1,但是分支线程t1 并不知道num已经变为1 ,还在根据 num == 0进行循环,程序一直在运行

此时,我们将全局变量 num 加上volatile修饰,t1线程立马结束循环

/**
 * volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
 * 1、volatile保证可见性
 */
public class VolatileTest {
    // 不加 volatile 程序就会死循环!
    // 加 volatile 可以保证可见性
    private volatile static int num = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            while (num == 0){ // 线程 t1 对主内存主线程 num = 1 的变化不知道

            }
        },"t1").start();

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

        // 主线程中修改 num = 1
        num = 1;
        System.out.println(num);
    }
}

(二)不保证原子性

public class VolatileTest1 {
    private volatile static int num = 0;

    public static void main(String[] args) {
        // 开启10条线程,每条线程执行1000次循环+1,理论结果执行完成 num=10000
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){ // Java中默认开启了2条线程 main  gc
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " num = " + num);
    }

    public static void add(){
        num++;
    }
}

不管执行多少次,会发现 num 都不是 10000 

如果不使用 Lock 和 Synchronized  ,解决 volatile不保证原子性问题;使用 java.util.concurrent.atomic 包下的原子类操作

此时妥妥的稳稳的输出 num = 10000 

/**
 * volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
 * 2、volatile 不保证原子性
 *
 * 解决 volatile不保证原子性问题;使用java.util.concurrent.atomic 包下的原子类操作(不使用Lock和Synchronized)
 */
public class VolatileTest2Atomic {
    // volatile 不保证原子性
    //private volatile static int num = 0;
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void main(String[] args) {
        // 开启10条线程,每条线程执行1000次循环+1,理论结果执行完成 num=10000
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int i1 = 0; i1 < 1000; i1++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){ // Java中默认开启了2条线程 main  gc
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " num = " + num);
    }

    public static void add(){
        // 不是一个原子性操作
        //num++;

        // AtomicInteger + 1 方法, CAS
        num.getAndIncrement();
    }
}

(三)禁止指令重排

有序性最终表述的现象是CPU是否按照既定代码顺序执行依次执行指令。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行,在单线程的情况下只要保证最终执行结果正确即可

int i = 0;            //语句1  
boolean flag = false; //语句2
i = 1;                //语句3  
flag = true;          //语句4

上面代码最终执行结果是i=1、flag=true,在不影响这个结果的情况下语句2可能比语句1先执行,语句4可能比语句3先执行

/**
 * volatile关键字保证变量对所有线程可见性,禁止指令重排,但是不保证原子性
 * 3、volatile 禁止指令重排
 */
public class VolatileTest3 {
    private static VolatileTest3 volatileTest3;
    private static boolean isInit = false;

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            volatileTest3 = null;
            isInit = false;

            new Thread(() -> {
                volatileTest3 = new VolatileTest3();    //语句1
                isInit = true;                          //语句2
            }).start();

            new Thread(() -> {
                if(isInit){
                    volatileTest3.doSomething();
                }
            }).start();
        }
    }

    public void doSomething() {
        System.out.println("doSomething");
    }
}

我们所期望的结果应该是每次都会打印doSOmething,可是这里会报空指针异常,出现这种情况的原因就是因为指令重排导致,上面语句1和语句2最终执行顺序可能会变为语句2先执行,语句1还未执行,此时刚有有一个线程独到了isInit的值为true,此时通过对象取调用方法就报空指针,因为此时SerialTest对象还未被实例化

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确

volatile关键字修饰时编译后会多出一个lock前缀指令 

lock指令相当于一个内存屏障:重排序时不能把后面的指令重排序到内存屏障之前的位置

1、在每个 volatile 写操作的前面插入一个 StoreStore 屏障:禁止上面的普通写和下面的 volatile 写重排序

2、在每个 volatile 写操作的后面插入一个 StoreLoad 屏障:防止上面的 volatile 写与下面可能有的 volatile读/写重排序

3、在每个 volatile 读操作的后面插入一个 LoadLoad 屏障:禁止下面所有的普通读操作和上面的 volatile 读重排序

4、在每个 volatile 读操作的后面插入一个 LoadStore 屏障:禁止下面所有的普通写操作和上面的 volatile 读重排序

(四)volatile和synchronized区别 

1、volatile是线程同步的轻量级实现,性能比synchronize好

2、volatile只能修饰变量,而synchronize可以修饰方法、代码块和变量

3、volatile多线程时不会发生阻塞,而synchronize会阻塞线程

4、volatile可以保证可见性和有序性(禁止指令重排),无法保证原子性,而synchronize都可以保证

volatile就是保证变量对其他线程的可见性和防止指令重排序
而synchronize解决多个线程访问资源的同步性


http://lihuaxi.xjx100.cn/news/101268.html

相关文章

Java中List集合详解

目录 一&#xff1a;Connection接口: 二&#xff1a;Map接口 三&#xff1a;Iterator迭代器&#xff0c;增强for List , Set, Map都是接口&#xff0c;前两个继承至Collection接口&#xff0c;Map为独立接口Iterator接口也是Java集合中的一员&#xff0c;但它与Collection、…

C++复习第二天:类与对象

1. 什么是面向过程&#xff1f;什么是面向对象&#xff1f; C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出解题过程的步骤&#xff0c;调用函数来实现。 C是基于面向对象的&#xff0c;关注的是对象&#xff0c;将一件事物划分成不同的对象&#xff0c;通过不…

【车间调度】基于全球邻域和爬坡来优化模糊柔性作业车间调度问题(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑…

python数学建模--线性规划问题案例及求解

目录数学问题&#xff1a;线性规划问题程序设计结果分析实际应用1&#xff1a;加工厂的生产计划设置未知数建立数学模型程序设计结果分析实际应用2&#xff1a;油料加工厂的采购和加工计划设置未知数建立数学模型程序设计结果分析遗留的问题钢管加工用料问题分析scipy.optimize…

全网最全学习攻略【尚硅谷电影推荐系统】附视频代码链接

简述 因为设计任务是开发一款图书推荐系统&#xff0c;但是没有现成的系统开发讲解&#xff0c;于是从网上找到了尚硅谷电影推荐系统的开发教程。 从配置虚拟机到开发各种推荐功能共耗时一个月左右&#xff0c;小破站里的视频教程很多但是有的是武老师少录了&#xff0c;有的是…

拓端tecdat|R语言实现 Copula 算法建模相依性案例分析报告

原文链接&#xff1a;http://tecdat.cn/?p6193 原文出处&#xff1a;拓端数据部落公众号 copula是将多变量分布函数与其边缘分布函数耦合的函数&#xff0c;通常称为边缘。Copula是建模和模拟相关随机变量的绝佳工具。Copula的主要吸引力在于&#xff0c;通过使用它们&#x…

计算机网络——4.1

作业4.1 题量: 38 满分: 138 作答时间:10-18 16:51至10-24 23:55 智能分析 127分 一. 单选题&#xff08;共26题&#xff0c;78分&#xff09; 1. (单选题, 3分)下列IP地址中作为环回地址用于本地软件环回测试的是&#xff08;&#xff09; A. 128.0.0.0B. 192.0.0.0C. 12…

StringBoot 入门初始

目录1、简介1.1、什么是StringBoot ?1.2、为什么使用springboot2、构建一个 SpringBoot 项目&#xff08;helloWorld)2.1、项目创建2.1.1 页面创建2.1.2 IDEA创建2.2、启动项目并访问2.2.1 创建一个HelloController.java2.2.2 启动项目2.3、自定义banner图3、Spring Boot启动器…