JUC并发编程(一):Java内存模型(JMM)及三大特性:可见性、有序性、原子性

news2024/9/22 7:35:42

1.简介

在当今高流量、高并发的互联网业务场景下,并发编程技术显得尤为重要,不管是哪一门编程语言,掌握并发编程技术是个人进阶的必经之路。时隔一个半月没有写技术博客文章,有点生疏了。。。闲话少叙,接下来我将围绕并发编程知识点进行总结讲解,这里从并发编程入门开始,讲述Java内存模型和并发的三大特性。

2.Java内存模型(JMM)

Java 内存模型(简称 JMM):定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。
其和内存区域是不一样的东西。内存区域是指 JVM 运行时将数据分区域存储,强调对内存空间的划分,即运行时数据区(Runtime Data Area)。

在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致

  • 主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
  • 本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。

Java内存模型图如下所示:

注意:Java内存模型和内存区域不要混淆,Java内存区域 JVM 运行时将数据分区域存储,强调对内存空间的划分,即运行时数据区(Runtime Data Area),如下图所示

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨群:Shepherd_126

3.并发三大特性:可见性、有序性、原子性

这些年,我们的 CPU、内存、I/O 设备都在不断迭代,不断朝着更快的方向努力。但是,在这个快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差,cpu速度>>内存速度>>磁盘设备速度。为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为:

  1. CPU 增加了缓存,以均衡与内存的速度差异;
  2. 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
  3. 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

当然Java内存模型的存在也是为了平衡上面三者处理速度差异。引入以上功能确实提升了系统的整体处理性能,但是凡事都有双面性,给我们平时并发编程过程中引入了以下问题

3.1 可见性

可见性就是一个线程对共享变量的修改,另外一个线程能够立刻看到。但是基于上面的Java内存模型可知每个线程都有自己的本地内存存储共享变量的副本,这可能会导致一个线程对共享变量的修改对另一个线程是不可见的问题。这里引入经典的i++是线程安全的吗问题?

    private static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        // 线程1
        Thread t1 = new Thread(() -> {
            for (int k = 0; k < 10000; k++) {
                i++;
            }
        });
        // 线程2
        Thread t2  = new Thread(() -> {
            for (int k = 0; k < 10000; k++) {
                i++;
            }
        });
        // 启动线程
        t1.start();
        t2.start();
        // 等到两个线程执行完成
        t1.join();
        t2.join();
        System.out.println("i="+ i);
    }

这里我们的预期结果是20000,但是执行发现结果再10000~20000之间,说明对共享变量i进行i++不是线程安全的

我们假设线程 A 和线程 B 同时开始执行,那么第一次都会将 i=0 读到各自的 CPU 缓存(线程本地缓存)里,执行完 i++ 之后,各自 CPU 缓存里的值都是 1,同时写入内存后,我们会发现内存中是 1,而不是我们期望的 2。之后由于各自的 CPU 缓存里都有了 i 的值,两个线程都是基于 CPU 缓存里的 i 值来计算,所以导致最终 i 的值都是小于 20000 的。这就是缓存的可见性问题volatile关键字主要用于解决变量在多个线程之间的可见性

3.2 原子性

volatile关键字主要用于解决变量在多个线程之间的可见性,所以我在上面案例中使用volatile关键字修饰变量i,即:

private volatile static int i = 0;

再次执行程序,发现执行结果还是在10000~20000之间,这就很奇怪了,不是说volatile关键字可以解决共享变量在多个线程之间的可见性吗,为啥执行结果还是不对?volatile确实能保证共享变量的可见性,这是毋庸置疑的。这时候我们需要分享一下i++这条语句在执行需要分成几条指令?至少需要三条 CPU 指令。

  • 指令 1:首先,需要把变量 i 从内存加载到 CPU 的寄存器;这时候volatile保证了从内存的值一定最新修改之后的值
  • 指令 2:之后,在寄存器中执行 +1 操作;
  • 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)

基于多线程执行任务,操作系统会进行多线程任务切换,可以发生在任何一条CPU 指令执行完,是的,是 CPU 指令,而不是高级语言里的一条语句。对于上面的三条指令来说,我们假设 i=0,如果线程 t1 在指令 1 执行完后做线程切换,此时线程t1的本地内存中i=0,接下来换线程t2执行执行任务,这时候线程t2执行指令1从内存加载共享变量i到本地内存i=0,再完成后续指令,然后cpu再任务切换回线程t1执行时,线程t1的本地内存变量i=0,接下来执行完后续指令,这样一来二去两个线程都是基于i=0进行了i++, 得到的结果不是我们期望的 2,而是 1。

我们潜意识里面觉得 i++ 这个操作是一个不可分割的整体,就像一个原子一样,线程的切换可以发生在 count+=1 之前,也可以发生在 i++ 之后,但就是不会发生在中间。我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。因此,很多时候我们需要在高级语言层面保证操作的原子性。那怎么保证原子性呢?volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证

代码逻辑修改如下:

private static int i = 0;
// 使用synchronized
synchronized static void add() {
    i++;
}
public static void main(String[] args) throws InterruptedException {
    // 线程1
    Thread t1 = new Thread(() -> {
        for (int k = 0; k < 10000; k++) {
           add();
        }
    });
    // 线程2
    Thread t2  = new Thread(() -> {
        for (int k = 0; k < 10000; k++) {
            add();
        }
    });
    // 启动线程
    t1.start();
    t2.start();
    // 等到两个线程执行完成
    t1.join();
    t2.join();
    System.out.println("i="+ i);
}

在 32 位的机器上对 long 型变量进行加减操作存在并发隐患原因是long类型64位,所以在32位的机器上,对long类型的数据操作通常需要多条指令组合出来,无法保证原子性,所以并发的时候会出问题

3.3 有序性

有序性。顾名思义,有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:a=1;b=2,编译器优化后可能变成b=2;a=1,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。经典问题:单例模式的双重检测锁实现方式。实现代码如下:

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

uniqueInstance 采用 volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化,如果我们这个时候访问 uniqueInstance 的成员变量就可能触发空指针异常

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行

4.Happens-Before 规则

首先先看下面代码:

    int x = 0;
    volatile boolean v = false;
    public void write() {
        x = 10;
        v = true;
    }
    public void read() {
        if (v == true) {
            System.out.println("x=" + x);
        }
    }
    public static void main(String[] args) {
        VolatileDemo demo = new VolatileDemo();
        // 线程1写数据x
        new Thread(() ->{
            demo.write();
        }).start();

        // 线程2读数据x
        new Thread(() ->{
            demo.read();
        }).start();
    }

假设线程 1 执行 write() 方法,按照 volatile 语义,会把变量 “v=true” 写入内存;假设线程 B 执行 read() 方法,同样按照 volatile 语义,线程 B 会从内存中读取变量 v,如果线程 B 看到 “v == true” 时,那么线程 B 看到的变量 x 是多少呢?

直觉上看,应该是 10,那实际应该是多少呢?这个要看 Java 的版本,如果在低于 1.5 版本上运行,x 可能是 10,也有可能是 0;如果在 1.5 以上的版本上运行,x 就是等于 10。

分析一下,为什么 1.5 以前的版本会出现 x = 0 的情况呢?我相信你一定想到了,变量 x 可能被 CPU 缓存而导致可见性问题。这个问题在 1.5 版本已经被圆满解决了。Java 内存模型在 1.5 版本对 volatile 语义进行了增强。怎么增强的呢?答案是一项 Happens-Before 规则。

Happens-Before 规则:前面一个操作的结果对后续操作是可见的

4.1 程序的顺序性规则

这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。这还是比较容易理解的,比如刚才那段示例代码,按照程序的顺序,代码 “x = 10;” Happens-Before 于后一行代码 “v = true;”,这就是规则 1 的内容,也比较符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。

4.2 volatile 变量规则

这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。

这个就有点费解了,对一个 volatile 变量的写操作相对于后续对这个 volatile 变量的读操作可见,这怎么看都是禁用缓存的意思啊,貌似和 1.5 版本以前的语义没有变化啊?如果单看这个规则,的确是这样,但是如果我们关联一下规则 3,就有点不一样的感觉了。

4.3 传递性

这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C

  1. “x=10” Happens-Before 写变量 “v=true” ,这是规则 1 的内容;
  2. 写变量“v=true” Happens-Before 读变量 “v=true”,这是规则 2 的内容 。
  3. 再根据这个传递性规则,我们得到结果:“x=10” Happens-Before 读变量“v=true”。

4.4 管程中锁的规则

这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。

要理解这个规则,就首先要了解“管程指的是什么”。管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。

管程中的锁在 Java 里是隐式实现的,例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。

synchronized (this) { // 此处自动加锁
  // x 是共享变量, 初始值 =10
  if (this.x < 12) {
    this.x = 12; 
  }  
} // 此处自动解锁

所以结合规则 4——管程中锁的规则,可以这样理解:假设 x 的初始值是 10,线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁),线程 B 进入代码块时,能够看到线程 A 对 x 的写操作,也就是线程 B 能够看到 x==12。这个也是符合我们直觉的,应该不难理解。

4.5 线程 start() 规则

这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

换句话说就是,如果线程 A 调用线程 B 的 start() 方法(即在线程 A 中启动线程 B),那么该 start() 操作 Happens-Before 于线程 B 中的任意操作。具体可参考下面示例代码。

Thread B = new Thread(()->{
  // 主线程调用 B.start() 之前
  // 所有对共享变量的修改,此处皆可见
  // 此例中,var==77
});
// 此处对共享变量 var 修改
var = 77;
// 主线程启动子线程
B.start();

4.6 线程 join() 规则

这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。

换句话说就是,如果在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回。具体可参考下面示例代码。

Thread B = new Thread(()->{
  // 此处对共享变量 var 修改
  var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程 B 可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用 B.join() 之后皆可见
// 此例中,var==66

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

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

相关文章

在使用nohup命令后台训练pytorch模型时,关闭ssh窗口导致的训练任务失败解决方法

下班前使用终端通过SSH登陆服务器&#xff0c;用nohup命令后台训练了一个pytorch模型。第二天来公司上班发现模型训练终止&#xff0c;报如下问题。 WARNING:torch.distributed.elastic.agent.server.api:Received 1 death signal, shutting down workers WARNING:torch.distr…

2023年陕西省安全员B证证考试题库及陕西省安全员B证试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年陕西省安全员B证证考试题库及陕西省安全员B证试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特种设备作业人员上岗证考试大…

2023年危险化学品经营单位主要负责人证考试题库及危险化学品经营单位主要负责人试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年危险化学品经营单位主要负责人证考试题库及危险化学品经营单位主要负责人试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特…

联想携中国移动打造车路协同方案 助力重庆实现32类车联网场景

10月11日&#xff0c;联想集团在中国移动全球合作伙伴大会上首次分享了与中国移动等合作伙伴共同打造的5G车路协同案例——重庆两江协同创新区车路协同应用。联想利用基于5G智能算力技术&#xff0c;在总里程55公里路段实现了32类车联网场景。 据了解&#xff0c;重庆两江协同创…

Spark任务优化分析

一、背景 首先需要掌握 Spark DAG、stage、task的相关概念 Spark的job、stage和task的机制论述 - 知乎 task数量和rdd 分区数相关 二、任务慢的原因分析 找到运行时间比较长的stage 再进去看里面的task 可以看到某个task 读取的数据量明显比其他task 较大。 如果是sql 任…

Python爬虫提高排名

在如今竞争激烈的互联网时代&#xff0c;网站的SEO优化变得尤为重要。而Python爬虫作为一种强大的工具&#xff0c;可以帮助网站主们提升搜索排名&#xff0c;吸引更多的流量和用户。本文将为您揭秘如何利用Python爬虫来改善您的SEO优化&#xff0c;并帮助您提升搜索排名。无论…

线性代数 --- 矩阵的QR分解,A=QR

矩阵的QR分解&#xff0c;格拉姆施密特过程的矩阵表示 首先先简单的回顾一下Gram-Schmidt正交化过程的核心思想&#xff0c;如何把一组线性无关的向量构造成一组标准正交向量&#xff0c;即&#xff0c;如何把矩阵A变成矩阵Q的过程。 给定一组线性无关的向量a,b,c&#xff0c;我…

2023年【危险化学品经营单位主要负责人】模拟考试及危险化学品经营单位主要负责人作业考试题库

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品经营单位主要负责人模拟考试是安全生产模拟考试一点通总题库中生成的一套危险化学品经营单位主要负责人作业考试题库&#xff0c;安全生产模拟考试一点通上危险化学品经营单位主要负责人作业手机同步练习。…

支持在线状态检查的仪表板miniboard

什么是 miniboard &#xff1f; miniboard 是带有选项卡和在线状态检查的轻量级仪表板。可以通过 GUI 或 yaml 文件进行配置。 采用 shoutrrr 通知。 什么是 Shoutrrr &#xff1f; Shoutrrr 是一个类似 caronc/apprise 的通知库&#xff0c;支持多种通知服务。 安装 在群晖上…

矿物鉴定VR实践教学平台:打造全新的沉浸式学习体验

在科技的帮助下&#xff0c;我们的学习和培训方式正在发生着深刻的变化。其中&#xff0c;虚拟现实&#xff08;VR&#xff09;技术带来的沉浸式学习体验&#xff0c;为我们提供了一种全新的学习和实践方式。本文将详细介绍一款使用VR技术的教学工具——矿物鉴定VR实践教学平台…

国外无人机蜂群作战样式进展及反蜂群策略研究

源自&#xff1a;现代防御技术 作者&#xff1a;王瑞杰, 王得朝, 丰璐, 赵正党, 陈浙梁 摘 要 科技进步和军事需求的联合推动下&#xff0c;无人机蜂群作战成为一种新兴的并能够改变战争规则的颠覆性作战样式&#xff0c;各军事强国围绕“蜂群技术和战术”展开了激烈的竞争…

[GWCTF 2019]你的名字 - SSTI注入(waf绕过)

[GWCTF 2019]你的名字 1 解题流程1.1 分析1.2 解题 2 思考总结 1 解题流程 1.1 分析 1、页面只有一个输入框&#xff0c;输入什么回显什么 2、根据特性应该是SSTI注入 1.2 解题 fuzz&#xff1a;过滤则长度1512 过滤&#xff1a;{{}}、class、mro、builtins、file、func_gl…

Android Studio展示Activty生命周期

前言 本文章以及之后文章的程序版本使用Android Studio 2022.3.1 Patch 1 版本编辑&#xff0c;使用语言为java&#xff0c;最低支持API 27 Android 8.1&#xff0c;构建工具版本如下&#xff1a; 本文章主要是介绍Activty跳转和删除&#xff0c;以备后续使用&#xff0c;所以就…

百度SEO优化的特点(方式及排名诀窍详解)

百度SEO优化的特点介绍&#xff1a; 百度SEO优化是指对网站进行优化&#xff0c;使其在百度搜索引擎中获得更好的排名&#xff0c;进而获取更多的流量和用户。百度SEO优化的特点是综合性强、效果持久、成本低廉、投资回报高。百度的搜索算法不断更新&#xff0c;所以长期稳定的…

酷开会员 | 探索火星奥秘,跟着酷开系统揭开火星神秘面纱!

酷开会员 | 探索火星奥秘&#xff0c;跟着酷开系统揭开火星神秘面纱&#xff01; 地球&#xff0c;是我们的母星。人类脚踏这颗星球坚实的大地&#xff0c;倚靠她的供给繁衍生息。然而从整个太阳系中看去&#xff0c;这个人类唯一的家园&#xff0c;也不过只是一粒悬浮在阳光中…

3.3 使用广播信道的数据链路层

思维导图&#xff1a; 3.3.1 局域网的数据链路层 ### 3.3 使用广播信道的数据链路层 #### 简介 - 广播信道支持一对多通信。 - 局域网技术在20世纪70年代末兴起&#xff0c;现在在计算机网络中占有主导地位。 #### 3.3.1 局域网的数据链路层 **局域网的特点&#xff1a;** 1…

RFID技术在锂电池生产线自动化应用

随着电动汽车和能源储存系统市场的不断扩大&#xff0c;锂离子电池作为其核心部件&#xff0c;以其高能量密度、长寿命等优点成为了主流选择。而对于锂电池智能化、高效化生产有着更高的要求&#xff0c;RFID技术的使用&#xff0c;将大幅度提高锂电池的生产产能&#xff0c;从…

高防CDN之所以强大的原因

高防CDN&#xff0c;这不仅仅是一个网络安全解决方案&#xff0c;它是您的在线堡垒&#xff0c;守护您的网站免受网络不法分子的侵袭。它究竟有何神奇之处&#xff0c;使它如此引人瞩目呢&#xff1f; 网络安全铠甲&#xff1a; 高防CDN是您的磅礴网络安全铠甲。它能够抵御多种…

MYSQL的日志管理

MySQL中有几种类型的日志记录&#xff0c;分别用于记录不同的操作和事件。以下是MySQL中常见的日志类型 错误日志 错误日志是 MySQL 中最重要的日志之一&#xff0c;它记录了当 mysqld 启动和停止时&#xff0c;以及服务器在运行过程中发生任何严重错误时的相关信息。当数据…

Unity头发飘动效果

Unity头发飘动 介绍动作做头发飘动头发骨骼绑定模拟物理组件 UnityChan插件下载UnityChan具体用法确定人物是否绑定好骨骼节点&#xff08;要做的部位比如头发等&#xff09;给人物添加SpringManager骨骼管理器给骨骼节点添加SpringBone这里给每个头发骨骼都添加上SpringBone。…