JVM(六)——内存模型与高效并发

内存模型与高效并发

一、java 内存模型

【java 内存模型】是 Java Memory Model(JMM)
简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序
性、和原子性的规则和保障

1)原子性

原子性在学习线程时讲过,下面来个例子简单回顾一下:
问题提出,两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

2)问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。
在这里插入图片描述

3)解决方法

使用 synchronized(同步关键字)

synchronized( 对象 ) {要作为原子操作代码
}

注意:上例中 t1 和 t2 线程必须用 synchronized 锁住同一个 obj 对象,如果 t1 锁住的是 m1 对
象,t2 锁住的是 m2 对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果。

二、可见性

1)退不出的循环

一种现象,main 线程对另一个线程 t 中的变量的修改不可见。
在这里插入图片描述

在这里插入图片描述
由于t 线程需要反复调用 run 变量,JIT 会把 run 变量放在 工作内存中的高速缓存中,不需要从总内存中读取。
在这里插入图片描述
所以即使更改主内存中的 run 变量,也无法改变高速缓存中的 run 变量, t 线程会一直运行。

2)解决方法

volatile(易变关键字)。一般避免使用 volatile,因为不能保证线程的安全。而使用 synchronized 关键字既可以保证线程的可见性又可以保证线程的原子性。

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到
主存中获取它的值,线程操作 volatile 变量都是直接操作主存

3)可见性

volatile 只是保证多线程之间的可见性,不保证原子性,即不保证多线程安全性。仅用在一个写线程,多个读线程的情况

synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。

如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,因为 println() 方法底层 使用了 synchronized 关键字,使用 synchronized 关键字会清理缓存。

三、有序性

1)诡异的结果

int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}
}
// 线程2 执行此方法
public void actor2(I_Result r) {num = 2;ready = true;
}

情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1

情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1

情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

结果还有可能是 0。

这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行num = 2。

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现

2)解决方法

volatile 修饰的变量,可以禁用指令重排

3)有序性理解

JVM 在不影响正确性的前提下,可以调整语句的执行顺序。这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性,例如著名的 double-checked locking 模式实现单例。

public final class Singleton {private Singleton() { }private static Singleton INSTANCE = null;public static Singleton getInstance() {// 实例没创建,才会进入内部的 synchronized代码块if (INSTANCE == null) {synchronized (Singleton.class) {// 也许有其它线程已经创建实例,所以再判断一次if (INSTANCE == null) {INSTANCE = new Singleton();}}}return INSTANCE;}
}

以上的实现特点是:
懒惰实例化
首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

但在多线程环境下,上面的代码是有问题的, 如果有两个线程,一个线程为 INSTANCE 分配了空间,INSTANCE != null,但还未进行初始化操作,另一个线程在 INSTANCE != null时,直接返回了未初始化完成的单例。

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才
会真正有效

4)happens-before

happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,
抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变
量的读可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z

四、CAS 与 原子类

1)CAS

CAS 即 Compare and Swap, 它体现的是一种乐观锁的思想,比如多个线程要对一个共享的整型变量执行 +1 操作:

// 需要不断尝试
while(true) {int 旧值 = 共享变量 ; // 比如拿到了当前值 0int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1/*这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候compareAndSwap 返回 false,重新尝试,直到:compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰*/if( compareAndSwap ( 旧值, 结果 )) {// 成功,退出循环}
}

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无
锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令。

2)乐观锁与悲观锁

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁
    你们都别想改,我改完了解开锁,你们才有机会。

3)原子操作类

juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、
AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的。

五、synchronized 优化

synchronized 是一个重量级锁,如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免的陷入用户态到内核态的转换中,进行这种转换需要耗费很多的处理器时间,状态转换消耗的时间甚至会比用户代码本身执行的时间还要长。

但 synchronized有非常大的优化余地,JDK 6 之后synchronized synchronized与ReentrantLock的性能基本上能够持平。

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存
储这个对象的 哈希码 、 分代年龄 ,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指
针 、 重量级锁指针 、 线程ID 等内容

1)轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

在这里插入图片描述
轻量级锁的解锁过程是通过CAS操作来进行的。
如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志
的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。

2)锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻
量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

3)重量级锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退
出了同步块,释放了锁),这时当前线程就可以避免阻塞。

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能
性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等
    待时间长了划算)
  • Java 7 之后不能控制是否开启自旋功能

4)偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID是自己的就表示没有竞争,不用重新 CAS。

偏向锁中的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线
程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需
要再进行同步。

偏向锁可以提高带有同步但无竞争的程序性能,但它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数-XX:-UseBiasedLocking来禁止偏向锁优化反而可以提升性能。

5)其他优化

1.减少上锁时间

同步代码块中尽量短

2.减少锁的粒度

将一个锁拆分为多个锁提高并发度,例如:

  • ConcurrentHashMap
  • LongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时候,会使用 CAS 来累加值到 base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值
  • LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
3. 锁粗化

多次循环进入同步代码块不如同步块内多次循环
另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次,(因为都是对同一个对象加锁,
没必要重入多次)

new StringBuffer().append("a").append("b").append("c");
4. 锁消除

JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候
就会被即时编译器忽略掉所有同步操作。

5. 读写分离

CopyOnWriteArrayList
ConyOnWriteSet

参考:
https://wiki.openjdk.java.net/display/HotSpot/Synchronization

http://luojinping.com/2015/07/09/java锁优化/

https://www.infoq.cn/article/java-se-16-synchronized

https://www.jianshu.com/p/9932047a89be

https://www.cnblogs.com/sheeva/p/6366782.html

https://stackoverflow.com/questions/46312817/does-java-ever-rebias-an-individual-lock

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

stable diffusion 的 GPU 不足怎么解决

稳定扩散(stable diffusion)是一种用于图像处理和计算机视觉任务的图像滤波算法。 当使用Stable Diffusion过程中遇到GPU显示内存不足的问题时。解决这个问题的方法有以下几种: 目前,对我来说,就最后一点能够暂时解决当…

dy消息监控,python协议

2024年3月27日 dy消息监控,协议版本,有wss和轮训2种方式,支持监听好友和群消息,支持消息回复,消息类型包括如下: 表情,文本,作品,语音,图片,视频…

ehters.js:provider

ethers.jsV5.4文档 安装ethers npm install ethers5.4.0// 引入 import { ethers } from ethersProviders /** Provider类* Provider类是对以太坊网络连接的抽象,为标准以太坊节点功能提供简洁、一致的接口。 */ const provider new ethers.providers.Web3Provider…

rabbitmq集群问题排查

blowcode-test-redis04、blowcode-test-redis05、blowcode-test-redis06 这3个节点搭建的rabbitmq集群,04是主节点。 某次分别观察3个节点的管理页面,先都只能看到自己的节点是正常的绿色状态,猜测节点都各自为政了。 下图是05节点成功加入0…

【书生·浦语大模型实战营第二期】学习笔记1

1. Introduction 开源llm举例:LLaMA 、Qwen 、Mistral 和Deepseek 大型语言模型的发展包括预训练、监督微调(SFT)和基于人类反馈的强化学习(RLHF)等主要阶段 InternLM2的显著特点 采用分组查询注意力(GQA…

【3D目标检测】Det3d—SE-SSD模型训练(前篇):KITTI数据集训练

SE-SSD模型训练 1 基于Det3d搭建SE-SSD环境2 自定义数据准备2.1 自定义数据集标注2.2 训练数据生成2.3 数据集分割 3 训练KITTI数据集3.1 数据准备3.2 配置修改3.3 模型训练 1 基于Det3d搭建SE-SSD环境 Det3D环境搭建参考:【3D目标检测】环境搭建(OpenP…

任何程序员错过这些宝藏接单平台,都会难过好吗?

财富自由何时才能实现?首先,可以很负责地告诉你:安安心心地当一头牛马肯定是不能!兢兢业业十几年,归来仍是马喽。 说实在的,这两年经济下行,互联网红利又日渐褪去。前者既争又抢,后…

低功耗、低成本 NAS 的可能性

使用现状:多台工作电脑,家里人手一台,还在两个住处 有好几台工作电脑,不同电脑有不同的用途,最大的问题就是各个电脑上文件的同步问题,这里当然就需要局域网里的公共文件夹,在NAS的问题上查了网…

SQLServer CONCAT 函数的用法

CONCAT函数用于将多个字符串值连接在一起。以下是一个简单的示例,演示了如何使用CONCAT函数: -- 创建一个示例表 CREATE TABLE ExampleTable (FirstName NVARCHAR(50),LastName NVARCHAR(50) );-- 插入一些示例数据 INSERT INTO ExampleTable (FirstNam…

搜维尔科技【应急推演】虚拟仿真技术的发展为煤炭矿井的安全生产找到新的出口

煤炭矿井的安全生产一直是我国关注的重大事项,保证煤炭矿井的安全生产,减少人员伤亡等不可逆的损失成为重中之重。虚拟仿真技术的发展为煤炭矿井的安全生产找到了新的出口。依托虚拟仿真技术,对煤炭矿井进行实时的生产监测,对矿井…

c# RichTextbox添加行号

使用另一个RichTextBox放在要添加行号的左边 使用以下代码 //uiRichTextBox1为右侧文本框,uiRichTextBox2为左侧文本框int lineIndex 0;private void uiRichTextBox1_TextChanged(object sender, EventArgs e){if (lineIndex > uiRichTextBox1.Lines.Length){L…

如何清理释放群晖客户端缓存?

任正菲说:企业最大的浪费,是经验的浪费! 而一个一个的经验,又都来自企业的每一个工作者。 因此当我们在工作过程中遇到一些问题时,我们就应该下意识的把解决问题的经验沉淀下来,从而可以与大家进行分享。…