【JAVA多线程】JMM,成体系聊一下JAVA线程安全问题

目录

1.什么是JMM

2.现代计算机架构

3.线程安全理论

3.1.造成线程安全问题的原因

3.1.1.内存不可见

3.1.2.重排序

3.2.解决思路

3.2.1.as-if-serial

3.2.2.happen-before

4.线程安全实现

4.1.Synchronized

4.2.volatile


1.什么是JMM

JMM,全称为Java Memory Model,是Java内存模型的简称。它是一种规范,定义了Java程序中多线程环境下的内存交互规则,确保了在不同的硬件和操作系统平台上的内存访问行为具有一致性。说白了就是JAVA的一种保证,保证了JAVA在多线程环境下可以是靠谱的,这种靠谱体现在具有保证线程安全的能力。如何保证线程安全其实就是JMM的核心。

2.现代计算机架构

要聊线程安全问题,我们首先要聊一下现代计算机的架构,因为要对现代计算机的架构有所认识,才知道为什么会引发线程安全问题。

现代计算机的模型中与JMM相关的部分是CPU、内存。现代计算机的内存架构的目的是在尽可能保证CPU能被最大利用,因此为了匹配内存与CPU间读写速率量级上的差异, 采用了多级缓存架构,层层加速,从而使得数据的读写能匹配 上CPU的速率。以一个4核心的CPU为例:

每一个core都有一套属于自己的多级缓存,L1、L2。

L1、L2是各个内核自己独有一套。 

L3是所有内核共用一个,由L3级缓存来与内存统一进行数据交互。

读数据是从L3到L2最后到L1,写数据是从L1到L2最后到L3

之所以这样设计目的就是通过层层缓存来加速,尽量跟上CPU的读写速度。

好,单核CPU的架构我们已经解开了,总结起来就一句话:

为了跟上CPU的读写效率,疯狂的堆了多级缓存。

3.线程安全理论

3.1.造成线程安全问题的原因

前面我们说了现代计算机的架构种关于内存这一部分总结起来就是疯狂堆了多级缓存。这种架构拉高了CPU的使用率,但是又带来了在并发环境下线程之间的数据一致性是无法保障的线程不安全的问题。

造成线程不安全问题的原因有以下两点:

  • 内存不可见
  • 重排序

3.1.1.内存不可见

内存可见是指在多线程环境中,一个线程对数据进行更新后,后续的其他线程能立即读到这个更新 结果。

但是在现代计算机多级缓存的架构下是会存在“内存不可见”的问题的。前面我们说了CPU处理完数据后数据首先被写入L1缓存,然后可能移动到L2,最终到达L3缓存,但是L3缓存是实时往内存中回写的吗?

L3不是实时向内存中回写的!因为缓存存在的主要目的是给CPU喂数据,所有其大量的时间都是拿来给CPU喂数据了,何时把数据回写回内存?对缓存来说就是——“抽空再说吧。”

当然现代计算机中关于缓存中的数据何时会写到内存,其规则和实现是有多种的,但根本上都没办法实现实时回写到内存中。就是这一小小的回写延迟,就会造成内存不可见,从而出现缓存不一致。

可以说“内存可见”和“CPU高效利用”之间是个互斥的关系,现代计算机架构选择了“CPU高效利用”。 那么解决内存不可见问题以及其附带而来的各个CPU之间缓存不一致的问题,就需要自己想办法解决了。这也正是JAVA的JMM要解决的核心问题之一。

3.1.2.重排序

软件技术和硬件技术的共同目标其实都是在不改变程序运行结果的前提下,尽可能的提高执行时候的性能(也就是并行度)。为了达成这一目的,常常不会按顺序的去执行指令,而是会将指令层层递进的进行重排序找到一个最优的执行顺序然后再执行。

程序在执行时存在着两种重排序现象:

  • 指令重排序
  • 内存重排序

指令重排序:

编译器编译时,基于性能考虑,在不改变结果的前提下,可能不会按照代码层面的顺序来编译出最终指令,编译结果的会按照执行时性能最优的方式进行指令重排序。javac编译器一般不会执行指令重排序,指令重排序一般由JVM中的JIT即时编译器来进行指令重拍。

内存重排序:

处理器执行时,基于性能考虑,可能不按照编译出来的顺序进行执行。比如程序编译出多条指令,CPU为了性能,会首先调度执行已经准备好资源的指令。

重排序机制会提升性能,在单线程环境下不会造成线程安全问题,但是在多线程环境下会对结果的准确性产生影响,因此在多线程环境下需要保证指令执行的有序性。

3.2.解决思路

JMM为了解决内存不可见、指令重排序造成的线程安全问题,遵循了两大原则:

  • as-if-serial
  • happen-before

3.2.1.as-if-serial

as-if-serial规范,用于限制指令重排序,保证单线程中执行结果的正确,即不管基于性能考量如何重排 序,都要保证单线程的执行结果的正确性。as-if-serial规范是一个需要编译器、CPU共同遵守的约定。 遵守此约定,就能保证单线程执行结果的正确。

3.2.2.happen-before

happen-before规范,用于实现内存可见性,保证多线程之间数据的准确性,即A happen-before B, 则A的执行结果必须对B可见。这是JMM自身在语言层面对开发者给出的保证,即在JAVA语言层面一系列 用来保证内存可见性的机制,如:

  • 单线程的每个操作,happen-before该线程后续的任意操作。
  • volatile变量的写入,happen-before后续对该变量的读取操作。
  • synchronized的解锁,happen-before对应后续对这个锁的加锁。

等等......

4.线程安全实现

4.1.Synchronized

Synchronized,同步关键字,使主内存中的共享数据在同一时间只能有一条线程可以持有,用于保 证happen-before,同时也规避了as-if-serial,主存中的数据被串行持有,就能实现线程A happenbefore 线程B。

本质上是利用JAVA对象头中的Mark Word字段来实现锁。Synchronized为了避免CPU态的频繁切 换,设计了锁升级机制,即随着参与争抢锁的竞争者数量的增加,锁的类型和具体实现会变化:

初期锁对象刚创建时,还没有任何线程来竞争,偏向锁标识位是0,锁状态01,说明该对象处于无 锁状态(无线程竞争它)。

当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的 任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word 会记录自己偏爱的线程的ID,把该线程当做自己的熟人。

当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量 级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线 程的栈帧中的锁记录。

如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量 级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监 视器对象用集合的形式,来登记和管理排队的线程。

代码示例:

public class Counter {
    private int count = 0;

    // synchronized 方法,同一时刻只允许一个线程访问
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + counter.getCount()); // 应该输出20000
    }
}

4.2.volatile

volatile,JAVA虚拟机提供的最轻量级的同步机制。其通过实现“缓存一致性协议”和“内存屏障”,保证了happen-before和强制禁止了指令重排序。

  • 缓存一致性协议 保证工作内存(缓存)中的数据和主内存(内存)中的数据的一致性,即一旦工作内存中的数据有变,马上刷新回主内存。 其底层实现是CPU的嗅探机制,所有CPU都盯住总线,监听总线中的数据变化,一旦工作内存中存 在的数据在总线中出现了assign操作,会立即让工作内存中的相应值失效,从而重新从主内存中去读取值。 不同的CPU有不同的缓存一致性协议。
  • 内存屏障用于禁止指令重排序, 具体的实现是在需要禁止重排序的两条代码(指令)之间插入一个标志,标 识标志两边的代码(指令)禁止重排序。这个标志是汇编级别的。

如果对总线相关内容比较模糊的小伙伴可以异步作者另一篇文章:

计算机组成原理(2)总线_单总线和双总线的区别-CSDN博客

代码示例:

public class VolatileExample {
    private volatile boolean keepRunning = true;

    public void runTask() {
        while (keepRunning) {
            // 执行任务...
        }
    }

    public void stopTask() {
        keepRunning = false; // 更改volatile变量,会立即对其他线程可见
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();
        Thread taskThread = new Thread(example::runTask);
        taskThread.start();

        try {
            Thread.sleep(1000); // 模拟一段时间后停止任务
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        example.stopTask(); // 请求停止任务
    }
}

volatile在各类JVM的底层实现细节存在不同,此处以Hotspot为例。

volatile关键字修饰的变量在汇编层面会被汇编语言的lock修饰。

lock指令的作用:

  1. 锁定这块内存区域的缓存
  2. 将这块缓存的数据立即写回系统内存。
  3. 写回内存的操作会引起其他CPU里缓存了该内存地址的数据无效(内存一致性协议)
  4. 提供内存屏障功能,使得lock指令前后不能重排序。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

胶质瘤的发病原因及诊断方式有哪些?

胶质瘤&#xff0c;这个听起来有些陌生的名词&#xff0c;实际上是一种起源于神经胶质细胞的常见脑肿瘤。它的发病原因复杂&#xff0c;涉及遗传、环境、年龄及感染等多种因素。 首先&#xff0c;遗传因素在胶质瘤的发病中占据一席之地。某些遗传性疾病&#xff0c;如结节性硬化…

【代码阅读】SSC:Semantic Scan Context for Large-Scale Place Recognition

一、主函数 官方开源的代码提供了四个主函数&#xff0c;其中eval_pair.cpp和eval_top1.cpp是一组&#xff0c;分别用于计算两帧的相似度分数以及一帧点云在所有的51帧点云中相似度最高的25帧的相似度分数。eval_seq.cpp是在eval_top1.cpp的基础上&#xff0c;给了一堆序列&am…

【Java Web】会话管理

目录 一、为什么需要会话管理&#xff1f; 二、会话管理机制 三、Cookie概述 四、HttpSession概述 4.1 HttpSession时效性 一、为什么需要会话管理&#xff1f; HTTP协议在设计之初就是无状态的&#xff0c;所谓无状态就是在浏览器和服务器之间的通信过程中&#xff0c;服务器并…

简过网:上万元的学费,考公到底要不要报个培训班?

考公报不报班一直是很多朋友比较纠结一件事&#xff0c;报班了学费太贵&#xff0c;不报班又怕考不上&#xff0c;如果你也有这种困扰&#xff0c;那么&#xff0c;不妨看看这篇文章&#xff01; 首先&#xff0c;对于报班VS自学这个问题&#xff0c;小编的建议是&#xff1a;…

LICEcap-开源GIF 屏幕录制工具

LICEcap-开源GIF 屏幕录制工具 开源GIF 屏幕录制工具 下载可以访问&#xff1a;https://www.cockos.com/licecap/ 点击Record&#xff0c;开始录制 点击Stop&#xff0c;停止录制 点击Record&#xff0c;进入该页面 display in animation&#xff08;在动画中显示&#xff09; …

240627_昇思学习打卡-Day9-ResNet50图像分类

240627_昇思学习打卡-Day9-ResNet50图像分类 文章目录 240627_昇思学习打卡-Day9-ResNet50图像分类前言残差网络Residual Block代码实现Bottleneck Block代码实现 BN层&#xff08;Batch Normalization&#xff09;构建ResNet50网络数据集准备与加载模型训练与评估可视化模型预…

高德.js2.0绘制点聚合,并点击点出现自定义样式弹窗

我这里依旧使用AMapLoader插件 代码如下 // 初始化高德地图initMap() {AMapLoader.load({key: "fb35c92d4019cfafeca876fd5514bb47", //key值是key值 和安全密钥不同version: "2.0", // 指定要加载的 JSAPI 的版本&#xff0c;缺省时默认为 1.4.15plugins…

免费恢复微信好友的聊天记录(已删除的好友不能恢复)

非常简单,适用于未删除的微信好友的聊天记录恢复,支持导出 1、下载楼月微信聊天记录导出恢复助手 - 导出手机微信聊天记录 2、官方原文教程链接&#xff1a;官方原文教程链接https://www.louyue.com/weixin.htm

华为升腾显卡选型备忘

目录 1. 开发套件 2. 加速模块 3. 加速卡 4. 训练卡 官方地址&#xff1a;https://www.hiascend.com/ 备注&#xff1a; &#xff08;1&#xff09;V后缀的都是Video视频解析卡&#xff0c;本质是推理卡&#xff1b; &#xff08;2&#xff09;I后缀的都是推理卡&#…

如何通过小猪APP分发轻松实现应用内测分发

搞应用开发的朋友们&#xff0c;都知道内测分发这个环节有多重要。没有内测&#xff0c;一款应用基本上是不可能上线的。毕竟&#xff0c;谁也不想自己的产品在上线的那一刻就被用户吐槽得体无完肤。内测分发的好坏&#xff0c;直接影响到应用的质量和用户的第一印象。如何才能…

postGreSQL关系数据库介绍

什么是postGreSQL关系数据库&#xff1f; PostgreSQL 是一个强大的、开源的对象关系型数据库管理系统&#xff08;ORDBMS&#xff09;。它基于POSTQUEL查询语言的继承&#xff0c;提供了对SQL标准的广泛支持&#xff0c;并扩展了许多高级功能&#xff0c;如事务处理、多版本并…

使用Python实现深度学习模型通常涉及以下几个步骤

学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……&#xff09; 2、学会Oracle数据库入门到入土用法(创作中……&#xff09; 3、手把手教你开发炫酷的vbs脚本制作(完善中……&#xff09; 4、牛逼哄哄的 IDEA编程利器技巧(编写中……&#xff09; 5、面经吐血整理的 面试技…

光电液位传感器工作时容易受哪些因素影响?

光电式水位传感器的检测液位时是必须要接触液体才能进行检测的。当液体覆盖光电式水位传感器的探头时&#xff0c;传感器内的发光二极管发射出去的光线会折射在液体中&#xff0c;而光敏接收器只能接收到少量光电或者接收不到光线。反之正常接收光线则是无水状态。 光电式水位…

想买骨传导耳机怕踩雷?全方位的选购攻略分享给你!

作为一个爱好运动的人来说&#xff0c;现在天气越来越暖和了&#xff0c;很多人选择外出徒步、越野或者骑行。在运动过程中都会佩戴一些入耳式耳机&#xff0c;但是运动一段时间发现入耳式耳机带久了耳朵会很不舒服&#xff0c;而且出汗了的话对于一些不防水的入耳式耳机的话&a…

swiper轮播 loop:true失效解决

数据是写死的时候&#xff0c;能够loop:true是有效的;数据是动态获取的loop:true就会失效。 方法一&#xff1a;在接收到数据后&#xff0c;使用 setTimeout(() > {this.getSwiper(); //生成swiper方法}, 0); 下面是我项目具体使用的参考例子&#xff1a; 方法二&#xff…

详细解释Spring事务的传播机制

详细解释Spring事务的传播机制 Spring框架中&#xff0c;事务传播机制是指在一个事务方法调用另一个事务方法时&#xff0c;Spring如何管理这些方法之间的事务边界。Spring提供了七种事务传播行为&#xff0c;以满足不同的业务需求。下面将详细解释每种传播行为及其适用场景&a…

IDEA 安装与激活详细教程最新(附最新激活码)2099年亲测有效!

我们先从 IDEA 官网下载 IDEA 2024.1 版本的安装包&#xff0c;下载链接如下&#xff1a; https://www.jetbrains.com/idea/download/ 点击下载(下载Ultimate版)&#xff0c;静心等待其下载完毕即可。 激活方式&#xff1a; 正版专属激活码领取

vcruntime140_1.dll是什么东东?vcruntime140_1.dll缺失的8个解决方法

当电脑出现找不到vcruntime140_1.dll,或vcruntime140_1.dll丢失无法打开软件怎么办&#xff1f;小编今天在本文详细为大家介绍解决方法与介绍vcruntime140_1.dll究竟是什么等vcruntime140_1.dll的问题。 一、vcruntime140_1.dll文件是什么 文件概述定义与功能 vcruntime140_…

观测云「可观测性解决方案」荣耀登入华为云官网

继成功上架华为云云商店联营商品后&#xff0c;「观测未来可观测性解决方案」已进一步正式登陆华为云官网&#xff0c;标志着双方合作的深化与拓展。这一全新上架的解决方案是观测云技术实力的集大成之作&#xff0c;为企业提供了一个全面升级的数字化监控观测服务。 观测云&am…

模拟局部下雨的天气思路Mars3d实现参考

目前mars3d里只有下雨滤镜那种&#xff0c;不能表现局部 曲线救国思路参考&#xff1a; 1、根据局部矢量范围求一个外接矩形bbox&#xff0c;根据bbox用turf按照比例尺生成网格&#xff08;比如50x50公里一个网格&#xff09; 2、所有的网格再按照矢量范围裁剪一下&#xff0…