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 旧值 = 共享变量 ; // 比如拿到了当前值 0
	int 结果 = 旧值 + 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

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

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

相关文章

MySQL修改root用户的密码

在Windows上重置MySQL的root用户密码可以通过以下步骤进行: 停止MySQL服务:使用快捷键WINR打开运行窗口,输入cmd进入命令行。在命令行中输入net stop mysql来关闭MySQL服务。启动MySQL跳过授权表:将目录切换到MySQL安装目录下的b…

CSMM软件过程能力成熟度模型

软件过程能力成熟度模型旨在通过提升组织的软件开发能力帮助顾客提升软件的业务价值。 本模型借鉴吸收了软件工程、项目管理、产品管理、组织治理、质量管理、卓越绩效管理、精益软件开发等领域的优秀实践,为组织提供改进和评估软件过程能力的一个成熟度模型。 总体…

腾讯云4核8G12M云服务器一年646元,送3个月时长

2024年腾讯云4核8G服务器租用优惠价格:轻量应用服务器4核8G12M带宽646元15个月,CVM云服务器S5实例优惠价格1437.24元买一年送3个月,腾讯云4核8G服务器活动页面 txybk.com/go/txy 活动链接打开如下图: 腾讯云4核8G服务器优惠价格 轻…

C语言程序编译与链接(拓宽视野的不二之选)

文章目录 翻译环境和运行环境翻译环境预处理编译汇编链接 运行环境 翻译环境和运行环境 1,在ANSI C的任何⼀种实现中,存在两个不同的环境。 第1种是翻译环境,在这个环境中源代码被转换为可执⾏的机器指 令(⼆进制指令&#…

文生图大模型Stable Diffusion的前世今生!

1、引言 跨模态大模型是指能够在不同感官模态(如视觉、语言、音频等)之间进行信息转换的大规模语言模型。当前图文跨模态大模型主要有: 文生图大模型:如 Stable Diffusion系列、DALL-E系列、Imagen等 图文匹配大模型:如CLIP、Chinese CLIP、…

由浅到深认识Java语言(29):集合

该文章Github地址:https://github.com/AntonyCheng/java-notes 在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.c…

【深度学习基础(4)】pytorch 里的log_softmax, nll_loss, cross_entropy的关系

一、常用的函数有: log_softmax,nll_loss, cross_entropy 1.log_softmax log_softmax就是log和softmax合并在一起执行,log_softmaxlogsoftmax 2. nll_loss nll_loss函数全称是negative log likelihood loss, 函数表达式为:f(x,class)−x[…

Django开发复盘

一、URL 对于一个不会写正则表达式的蒟蒻来说,在urls.py中就只能傻傻的写死名字,但是即便这样,还会有很多相对路径和绝对路径的问题(相对ip端口的路径),因为我们网页中涉及到页面跳转,涉及到发送…

pytorch常用的模块函数汇总(1)

目录 torch:核心库,包含张量操作、数学函数等基本功能 torch.nn:神经网络模块,包括各种层、损失函数和优化器等 torch.optim:优化算法模块,提供了各种优化器,如随机梯度下降 (SGD)、Adam、RMS…

Maven的pom.xml中resources标签的用法

spring-boot-starter-parent-2.4.1.pom文件中resources标签内容如下&#xff1a; <build><resources><resource><directory>${basedir}/src/main/resources</directory><filtering>true</filtering><includes><include>…

考研数学|张宇《1000题》太难了,根本刷不动?怎么破!

即使一直在看张宇的课程&#xff0c;但在做1000题时仍然感到困难。这其实是许多考生会出现的问题&#xff0c;所以不用担心&#xff0c;希望看完这篇文章能对你有帮助。 首先是理论与实践的差距。听课时&#xff0c;你可能是在接受知识&#xff0c;而做题则需要将这些知识应用…

鸿蒙开发之ArkUI组件常用组件文本输入

TextInput、TextArea是输入框组件&#xff0c;通常用于响应用户的输入操作&#xff0c;比如评论区的输入、聊天框的输入、表格的输入等&#xff0c;也可以结合其它组件构建功能页面&#xff0c;例如登录注册页面。 TextInput为单行输入框、TextArea为多行输入框 TextArea 多行…

阿里云服务器租用价格表(最新CPU/内存/带宽/磁盘收费标准)

阿里云服务器一个月多少钱&#xff1f;最便宜5元1个月。阿里云轻量应用服务器2核2G3M配置61元一年&#xff0c;折合5元一个月&#xff0c;2核4G服务器30元3个月&#xff0c;2核2G3M带宽服务器99元12个月&#xff0c;轻量应用服务器2核4G4M带宽165元12个月&#xff0c;4核16G服务…

如何用Python操作xlsx文件并绘制折线图!

​大家好&#xff0c;数据分析在现代社会越来越重要&#xff0c;而Excel作为数据分析的利器&#xff0c;几乎人手一份。但是&#xff0c;Excel的操作有时候略显繁琐&#xff0c;更是感觉无从下手。 你知道吗&#xff1f;Python这个神奇的工具不仅能帮你处理海量的数据&#xf…

【GPU系列】选择最适合的 CUDA 版本以提高系统性能

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

c语言中动态内存管理

说到内存&#xff0c;大家一定都知道。但是有一种函数可以实现动态内存管理&#xff0c;下面大家一起学习。 文章目录 一、为什么要有动态内存管理&#xff1f;二、malloc 和 free1.malloc2.free 三、calloc 和 realloc1.calloc2.realloc3.常见的动态内存的错误3.1对NULL指针的…

【SpringBoot框架篇】37.使用gRPC实现远程服务调用

文章目录 RPC简介gPRC简介protobuf1.文件编写规范2.字段类型3.定义服务(Services) 在Spring Boot中使用grpc1.父工程pom配置2.grpc-api模块2.1.pom配置2.2.proto文件编写2.3.把proto文件编译成class文件 3.grpc-server模块3.1.pom文件和application.yaml3.2.实现grpc-api模块的…

Linux——信号概念与信号产生方式

目录 一、概念 二、前台进程与后台进程 1.ctrlc 2.ctrlz 三、信号的产生方式 1.键盘输入产生信号 2.系统调用发送信号 2.1 kill()函数 2.2 raise()函数 2.3 abort()函数 3.异常导致信号产生 3.1 除0异常 3.2 段错误异常 4.软件条件产生信号 4.1 管道 4.2 闹钟…

最新可用免费VPS云服务器整理汇总

随着云计算技术的不断发展&#xff0c;越来越多的个人和企业开始关注和使用VPS云服务器。VPS云服务器以其高度的灵活性、可定制性和安全性&#xff0c;成为了一种受欢迎的服务器解决方案。然而&#xff0c;对于初学者或者预算有限的用户来说&#xff0c;如何选择合适的免费VPS云…

ZYNQ学习之Ubuntu系统的简单设置与文本编辑

基本都是摘抄正点原子的文章&#xff1a;<领航者 ZYNQ 之嵌入式Linux 开发指南 V3.2.pdf&#xff0c;因初次学习&#xff0c;仅作学习摘录之用&#xff0c;有不懂之处后续会继续更新~ 一、Ubuntu的简单操作 1.1 切换拼音输入法 Ubuntu 自带的拼音输入法&#xff0c;有两种…