【并发】深入理解JMM并发三大特性(一)

news2024/12/25 23:43:14

【并发】深入理解JMM&并发三大特性(一)

今天是2022.11.16,在此之前我已经学习完了图灵课堂MySQL的课程,也是想这篇文章一样用CSDN博客的形式来记录这些知识点。

在并发中,JMM在大多数人眼中,它是整个Java并发编程中最难的部分也是最重要的部分!Guide上面有相应的描述:

JMM(Java 内存模型)详解 (javaguide.cn)https://javaguide.cn/java/concurrent/jmm.html

下面我讲详细介绍该内容~~~  

一、并发和并行

1. 并行(parallel)

指在同一时刻,有多条指令多个处理器同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

2. 并发(concurrency)

指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

并行在处理器系统中存在。而并发可以在处理器和处理器系统中存在。

并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行) 

3. 并发三大特性

可见性、原子性和有序性!!!

这就是并发的3个特性,它们同样也是并发编程Bug的源头!!!在并发编程中出现BUG,十有八九是这3个特性处理不当所导致的

(1)可见性

一个线程修改了共享变量的值其他线程能够看到修改的值。

Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值。

这种依赖主内存作为传递媒介的方法来实现可见性的。

如何保证有序性?

  1. 通过 volatile 关键字保证可见性。
  2. 通过 内存屏障保证可见性。
  3. 通过 synchronized 关键字保证可见性。
  4. 通过 Lock保证可见性。(JUC下面的)
  5. 通过 final 关键字保证可见性

我们可以先来看一个例子:

我们这里开了A、B两个线程,A线程调了 load(),B线程调了refresh()

换句话说就是,A会进入一个 while(flag) 的死循环,B试图修改flag,让其退出循环!

public  class VisibilityTest {
    private boolean flag = true;
    private int count = 0;

    public void refresh() {
        flag = false;
        System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
    }

    public void load() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        while (flag) {
            //TODO 业务逻辑
            count++;
        }
        System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityTest test = new VisibilityTest();

        Thread threadA = new Thread(() -> test.load(), "threadA");
        threadA.start();

        Thread.sleep(1000);
        Thread threadB = new Thread(() -> test.refresh(), "threadB");
        threadB.start();
    }
}

运行结果: 

但是,结果却和我们预想的不一样!这是因为 flag这个变量,在上述的环境中并不满足可见性!即线程B修改flag,对线程A来说,是不可见的! 

如何解决??? 

我们可以使用 volatile关键字去修饰 flag 或 count;

可以在业务逻辑那一块调用 storeFence() 方法,开启内存屏障

可以在业务逻辑中使用synchronized关键字

也可以在业务中使用 System.out.println() 语句(里面有调synchronized);

还可以在业务中调用 Thread.yield() ,让其发生上下文切换......

具体这是为什么呢?我会在下文中解释清楚!因为这一块涉及到JMM内存模型、JVM的内存指令,我们只有先搞清楚这些东西,才能将这可见性问题讲明白!

(2)原子性(有后续)

一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行

在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。

如何保证原子性?

  • 通过 synchronized 关键字保证原子性。
  • 通过 Lock保证原子性。
  • 通过 CAS保证原子性。

(3)有序性(有后续)

即程序执行的顺序按照代码的先后顺序执行。

JVM 存在指令重排,所以存在有序性问题。(后面再讲!)

如何保证有序性?

  • 通过 volatile 关键字保证可见性。
  • 通过 内存屏障保证可见性。
  • 通过 synchronized 关键字保证有序性。
  • 通过 Lock 保证有序性。

二、Java内存模型(JMM)

1. 什么是JMM? 

Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果

JMM描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性、有序性、可见性展开的。

在JMM中规定了,一个线程 如何和何时 可以看到由其他线程修改过后的共享变量的值?以及如何同步的访问共享变量?

在JMM模型中,线程A 和 线程B 它们在逻辑上都有各自独立的内存空间,里面存放的共享变量是不能直接通信的,必须要经过“主内存”!

2. JMM与硬件内存架构的关系

Java内存模型与硬件架构之间存在差异

对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。

如下图所示,Java内存模型和计算机硬件内存架构是一个交叉关系 

所以,我们上面讲的主内存、本地内存,都是一个逻辑概念! 

3. 内存交互操作

关于主内存工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

4. JMM的内存可见性保证 

程序类型,Java程序的内存可见性保证可以分为下列3类

(1)单线程程序。单线程程序不会出现内存可见性问题。编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。

(2)正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。

(3)未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。 JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。

三、深入解析volatile

1. volatile的特性

  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性(基于这点,我们通过会认为volatile不具备原子性)。volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。
  • 有序性:对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性。

2. volatile写-读的内存语义

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

3. volatile可见性实现原理

JMM内存交互层面实现

volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量操作对多线程的可见性。

硬件层面实现

通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

4. volatile在JVM(hotspot)的实现

字节码解释器实现

JVM中的字节码解释器(bytecodeInterpreter),用C++实现了JVM指令,其优点是实现相对简单且容易理解,缺点是执行慢。

bytecodeInterpreter.cpp

在linux系统x86中的实现 

inline void OrderAccess::storeload()  { fence(); }
inline void OrderAccess::fence() { 
  if (os::is_MP()) {
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }

x86处理器中利用lock前缀指令实现类似内存屏障的效果。 

lock前缀指令的作用

(1)确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销

(2)LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。

(3)LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效。

【遗留问题】之前的程序什么时候会跳出循环?线程什么时候会刷主存?

我们现在可以说了,程序什么时候会条出这个循环?之前提到了JMM内存模型,当一个线程中的本地内存里的变量“过期”的时候,那么线程将会重新到“主内存”里面去 read/load 新的值!

看下面的程序,我们通过调 shortWait(1000000) 来模拟业务执行时间。

while (flag) {
    // TODO 业务逻辑
    count++;
    
    // 模拟业务执行多久
    shortWait(1000000);  // 1ms
}
......

public static void shortWait(long interval) {
    long start = System.nanoTime();
    long end;
    do {
        end = System.nanoTime();
    } while (start + interval >= end);
}

当循环里面执行时间比较长时,缓存中的变量可能过期,就会重新到“主内存”里面去 read/load

本地内存(缓存)被淘汰之后,可见性就可以得到保证!(淘汰了,就需要从主内存中重新加载)所以,我们解决可见性问题核心,就是想办法将本地内存中的值淘汰,让其从主内存中重新加载!!!

Thread.yield();

释放时间片!会导致上下文切换,之后就要重新加载上下文,JMM就会对比主内存与本地内存中的值是否一致,不一致就会从主内存中重新加载

private volatile boolean flag = true;

private volatile int count = 0;

volatile的底层会去调storeload()fence() 里面有用到 lock前缀指令

lock前缀指令不是内存屏障指令,但是它具有一样的功能,它可以使缓存失效

UnsafeFactory.getUnsafe().storeFence();  

这个是开启内存屏障,它和上述的volatile类似,都会调到storeload()fence() 

System.out.println(count);

public void println(int x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

 在 synchronized 里面要保证一些可见性,所以它里面也会调内存屏障!所以原因和上述的一样!

private Integer count = 0;

在Integer里面的这个value用了final修饰! 

private final int value;

public Integer(int value) {
    this.value = value;
}

用了final,也会从主内存中去重新加载!

总结:Java中的可见性如何保证?

最本质的其实只有两种!

(1)通过调storeLoad,用lock前缀指令,开启内存屏障

(2)Thread.yield() 上下文切换

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

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

相关文章

Zookeeper 2 Zookeeper 安装与配置 2.1 Zookeeper 安装与配置

Zookeeper 【黑马程序员Zookeeper视频教程,快速入门zookeeper技术】 文章目录Zookeeper2 Zookeeper 安装与配置2.1 Zookeeper 安装与配置2.1.1 Zookeeper 下载安装2 Zookeeper 安装与配置 2.1 Zookeeper 安装与配置 2.1.1 Zookeeper 下载安装 【其实这块内容 学…

搜狗趁势而来,输入法江湖风云再起

在2022腾讯数字生态大会上,搜狗输入法正式推出面向B端的开放平台,这是自去年9月完成退市、并入腾讯以来,这家输入法C端王者第一个大动作,宣告着其进军B端的野心。几乎与此同时,如今已是自家兄弟的微信,悄然…

vue调用百度api时跨域的解决方案

今天在开发一个项目中发现vue前端调用百度ocr识别接口出现了跨域问题! 百度api 的接口: https://aip.baidubce.com/oauth/2.0/token 和百度开发工程师沟通一个多小时,未找到解决方案,忽然想到了是不是nginx配置的问题&#xff0…

TCManager——中药房管理系统大作业

简介 由于最近一个月世界变化有点大,所以一直在同步自己的大脑,没有写博客。 上个月花了5天(3天后端2天前端)写了个经典的springbootvue2的中药房管理系统大作业——TCManager。项目已在gitee上(校园网差,…

vscode自动添加头注释和函数注释

下载koroFileHeader插件 然后点它的wiki文档 会跳到它的github 配置字段 OBKoro1/koro1FileHeader Wiki GitHub 先找到vscdoe的setting文件,两种方法: 1, 然后点这里: 或者去搜索框搜索:FileHeader 出现如下&…

你的团队是王者还是青铜(下)

我们接着上篇继续聊。 问题4:谁动了团队的时间?如果重来一个迭代,你有7*40个小时的投资,你要如何决策团队的工作安排? “小溪,一会约开卡;小溪,我这有个问题;小溪&#…

SpringBoot+Vue物流仓储管理系统

项目背景 在信息化的时代,效率和速度就变得尤为重要了,具有高效率和速度就具有更好的竞争力,更受客户欢迎。与此同时,网购与人们的生活息息相关,顾客在网上购买的商品需要通过物流公司对这些商品进行管理和配送&#x…

十五、Docker 网络

1、概述 Docker 容器和服务如此强大的原因之一是您可以将它们连接在一起,或将它们连接到非 Docker 工作负载。Docker 容器和服务甚至不需要知道它们部署在 Docker 上,或者它们的对等体是否也是 Docker 工作负载。无论您的 Docker 主机运行 Linux、Window…

Doo Prime 为泰国 SOS 儿童村送温暖,公益有起点爱心无疆界

一年一度的圣诞节即将来临,在这欢乐的时刻, Doo Prime 荣幸地宣布 ,向泰国 SOS 儿童村捐赠了 35 万泰铢 ( 约合 1.23 万美元 ),作为泰国南部城市合艾府 SOS 儿童村的房屋翻修费用。 Doo Prime 希望 SOS 儿童村的孩子们都能在温馨…

【小程序】网络数据请求

目录 1. 小程序中网络数据请求的限制 2. 配置 request 合法域名 3. 发起 GET 请求 4. 发起 POST 请求 5. 在页面刚加载时请求数据 5. 跳过 request 合法域名校验 6. 关于跨域和 Ajax 的说明 1. 小程序中网络数据请求的限制 出于安全性方面的考虑,小程序官方…

【目标检测】Objects as Points

目录概述细节anchor-base vs anchor free网络结构标注损失函数学习资料概述 本文是一个anchor-free的目标检测算法。 【2019】【CenterNet】 研究的问题: 如何更好地将目标检测问题建模为关键点检测问题 提出的方法: 一个简单高效的目标检测方法Cent…

抖音年货节增长秘籍:横向做阵地,纵向定节奏,提前深种草

抖音好物年货节即将到来,对于品牌来说,这将是新一年首次生意爆发的机会。为了更好的融入抖音电商生态,做好数字化经营主阵地,品牌面临多个新挑战: 大促Bigday流量费用高,品牌怎样更高效获取流量&#xff1f…

数据库设计说明书(GB8567——88)基于协同的在线表格forture-sheet

数据库设计说明书(GB8567——88) 1引言 1.1编写目的 数据库的设计是为了以后编码、测试以及维护阶段的后台数据的存储做准备。应用于系统开发前期,为后期数据库设计指引方向。 预期的读者:系统开发人员、系统测试人员和系统维…

瑞昱rtl819x-SDK-v3.4.14b的watchdog分析

watchdog 看门狗,又叫watchdog timer,是一个定时器电路,一般有一个输入,叫喂狗或踢狗;一个输出到MCU 的 RST 端,MCU 正常工作的时候,每隔一段时间输出一个信号到喂狗端,给 WDT 清零…

SQL注入渗透与攻防(八)之延时注入

目录 基于时间的SQL盲注 - 延时注入 案列演示 基于时间的SQL盲注 - 延时注入 关于延时注入的使用场景一般在我们测试的注入点没有报错信息又没有回显点的情况下进行的。通过对于时间的延时判断来猜解数据。在实战过程中并不推荐大家通过延时注入去判断数据库,因为…

转行做程序员,难吗

在互联网急速发展的这几年里,程序员这个职业,成为了很多人的心之所向。虽然高薪背后往往伴随着高强度的工作节奏和压力,但是也不妨碍大量的人在编程培训或者自学编程的道路上前仆后继。 那么转行程序员真的容易吗?下面就跟大家分享…

无需编程即可将chatgpt接入自己的微信公众号

ChatGpt是openai推出的GPT3文本生成机器人。该机器人主要完成文本生成相关任务。机器人可以自主进行写作、翻译、修改语法、角色扮演的,甚至编写程序。网上有很多接入方法,大部份需要编程,并且需要注册openai账号获取appkey。本文所介绍的方法…

datagridview如何根据不同的按钮显示不同的表

以我设计的为例: 双击按钮,会自动创建三个事件 以第一个按钮为例:在其中添加如下代码 SqlConnection sqlcon new SqlConnection(); sqlcon.ConnectionString "";//双引号内填入你的数据库连接字符 sqlcon.Open(); string sql &…

UG NX二次开发(C#)-获取模型文件的预览图片

文章目录 1.前言2. 模型文件预览图介绍3.采用二次开发的方法获取模型的预览图4.验证1.前言 UG NX保持的prt文件中保存了模型的预览位图,但是采用UG NX二次开发时不能直接提取出来,本文讲解一下将prt文件的预览位图提取出来并单独保存。 2. 模型文件预览图介绍 如果我们在UG…

【字体】写代码编程字体展示推荐

写代码编程字体展示推荐 零、适合写代码编程的字体 什么样的字体适合写代码用?一般来讲,写代码的字体要求字母的宽度一致、轮廓清晰美观、l1i,0oO易于分辨、清晰易分辨的标点等,甚至有强迫症的还要中文与英文严格2:1对齐。下面收…