JVM多线程读写和锁

文章目录

  • 1 原子性
  • 2 可见性
  • 3 有序性
  • 4 CAS
  • 5 synchronized 优化
    • 5.1 轻量级锁
    • 5.2 锁膨胀
    • 5.3 自旋
    • 5.4 偏向锁
    • 5.5 其他优化

1 原子性

问题:两个线程对初始值为 0 的静态变量 i 一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

i++产生JVM字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i

i++产生JVM字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i

交错执行的可能导致结果可能为正,也可能为负,也可能为0,为正的情况如下:

// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1

显而易见,两个线程谁后对静态变量做赋值,另一方的赋值就被覆盖了

解决办法: 想要保证 i++ 和 i-- 代码的原子性,需使用 synchronized 对象锁

2 可见性

问题:main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止?

static boolean run = true;
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....}});t.start();Thread.sleep(1000);run = false; // 线程t不会如预想的停下来
}

之前说过,JIT应用场景之一就有字段优化,可见于 回顾:JVM类加载

①初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存

②热点字段run渐被缓存至t线程自己的工作内存,以减少对主内存的访问

③main线程对run的更新虽然同步至主内存,但t线程的run永远都是旧值

解决办法: volatile(易变关键字),强制 使用到该变量的线程 到主存中获取它的值

关键字使用场景作用/特点
synchronized多个写线程既可以保证代码块的原子性,也同时保证代码块内变量的可见性,
属于重量级操作,性能相对更低
volatile多读一写可见性

3 有序性

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;
}

两个线程对r对象的r1属性值做修改,问能得到哪几种结果?

情况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 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现

解决办法: volatile 修饰的变量,可以禁用指令重排,用其修饰 num变量 或者 ready变量即可

有序性理解:

①指令重排的出发点是在不影响正确性的前提下,可以调整语句的执行顺序

⑤多线程下『指令重排』会影响正确性,例如著名的 double-checked locking 模式实现单例

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;}
}	

虽然方法能懒惰实例化并加锁,但是多线程下还是有问题的, INSTANCE = new Singleton() 对应的字节码为:

0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field
INSTANCE:Lcn/itcast/jvm/t4/Singleton;

问题就在 4 和 7 两步,正常是对象初始化在将其地址赋值给静态变量,但是可能指令重排,先7后4, 导致的结果就是对象还未来得及执行初始化方法,其地址就先赋给了静态变量,此时另一个线程调用该方法,未初始化的对象直接通过静态变量return出去了,这有问题

时间1 t1 线程执行到 INSTANCE = new Singleton();
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0 处)
时间3 t1 线程将引用地址赋值给 INSTANCE,这时 INSTANCE != null7 处)
时间4 t2 线程进入getInstance() 方法,发现 INSTANCE != nullsynchronized块外),直接
返回 INSTANCE
时间5 t1 线程执行Singleton的构造方法(4 处)

解决办法:对 INSTANCE 使用 volatile 修饰

4 CAS

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

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

简单说: 就是CAS用于检验共享变量的结果达到预期要求, 因此它配上 volatile 修饰变量保证该变量的可见性,可以实现无锁并发,效率提升,缺点就是不达要求不断重试,会争抢资源,效率反而下降,因此它适用于竞争不激烈、CPU多核的场景,不然还是稳妥起见选用synchronized悲观锁

原子操作类, 底层就是采用 CAS 技术 + volatile 来实现的。以 AtomicInteger 为例:

// 创建原子整数对象
private static AtomicInteger i = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int j = 0; j < 5000; j++) {i.getAndIncrement(); // 获取并且自增 i++// i.incrementAndGet(); // 自增并且获取 ++i}});Thread t2 = new Thread(() -> {for (int j = 0; j < 5000; j++) {i.getAndDecrement(); // 获取并且自减 i--}});t1.start();t2.start();t1.join();t2.join();System.out.println(i);
}

5 synchronized 优化

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

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

5.1 轻量级锁

假设有两个方法同步块,利用同一个对象加锁:

static Object obj = new Object();
public static void method1() {synchronized( obj ) {// 同步块 method2();}
}public static void method2() {synchronized( obj ) {// 同步块 B}
}
线程 1对象 Mark Word线程 2
访问同步块 A,把 Mark 复制到
线程 1 的锁记录
01(无锁)
CAS 修改 Mark 为线程 1 锁记录
地址
01(无锁)
成功(加锁)00(轻量锁)线程 1
锁记录地址
执行同步块 A00(轻量锁)线程 1
锁记录地址
访问同步块 B,把 Mark 复制到
线程 1 的锁记录
00(轻量锁)线程 1
锁记录地址
CAS 修改 Mark 为线程 1 锁记录
地址
00(轻量锁)线程 1
锁记录地址
失败(发现是自己的锁)00(轻量锁)线程 1
锁记录地址
锁重入00(轻量锁)线程 1
锁记录地址
执行同步块 B00(轻量锁)线程 1
锁记录地址
同步块 B 执行完毕00(轻量锁)线程 1
锁记录地址
同步块 A 执行完毕00(轻量锁)线程 1
锁记录地址
成功(解锁)01(无锁)
01(无锁)访问同步块 A,把 Mark 复制到
线程 2 的锁记录
01(无锁)CAS 修改 Mark 为线程 2 锁记录
地址
00(轻量锁)线程 2
锁记录地址
成功(加锁)

5.2 锁膨胀

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

static Object obj = new Object();
public static void method1() {synchronized( obj ) {// 同步块}
}
线程 1对象 Mark Word线程 2
访问同步块,把 Mark 复制到 线程 1 的锁记录01(无锁)
CAS 修改 Mark 为线程 1 锁记录 地址01(无锁)
成功(加锁)00(轻量锁)线程 1 锁记录地址
执行同步块00(轻量锁)线程 1 锁记录地址
执行同步块00(轻量锁)线程 1 锁记录地址访问同步块,把 Mark 复制
到线程 2
执行同步块00(轻量锁)线程 1 锁记录地址CAS 修改 Mark 为线程 2 锁
记录地址
执行同步块00(轻量锁)线程 1 锁记录地址失败(发现别人已经占了
锁)
执行同步块00(轻量锁)线程 1 锁记录地址CAS 修改 Mark 为重量锁
执行同步块10(重量锁)重量锁指
阻塞中
执行完毕10(重量锁)重量锁指
阻塞中
失败(解锁)10(重量锁)重量锁指
阻塞中
释放重量锁,唤起阻塞线程竞争01(无锁)阻塞中
10(重量锁)竞争重量锁
10(重量锁)成功(加锁)

5.3 自旋

重量级锁竞争的时候,还可以使用自旋来进行优化

如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

如果自旋重试失败则线程阻塞

自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

5.4 偏向锁

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

5.5 其他优化

优化操作
减少上锁时间同步代码块中尽量短
减少锁的粒度将一个锁拆分为多个锁提高并发度
锁粗化多次循环进入同步块不如同步块内多次循环
锁消除JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候
就会被即时编译器忽略掉所有同步操作。
读写分离CopyOnWriteArrayList
ConyOnWriteSet

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

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

相关文章

.NET集成IdGenerator生成分布式全局唯一ID

前言 生成分布式唯一ID的方式有很多种如常见的有UUID、Snowflake&#xff08;雪花算法&#xff09;、数据库自增ID、Redis等等&#xff0c;今天我们来讲讲.NET集成IdGenerator生成分布式全局唯一ID。 分布式ID是什么&#xff1f; 分布式ID是一种在分布式系统中生成唯一标识符…

Unity - transform使用

Test_03 TransformTest 控制物体位置和物体间的父子关系 public class TransformTest : MonoBehaviour {void Start(){// 【获取位置】// 世界坐标中物体的位置Debug.Log(transform.position);// 物体相对于父物体的位置Debug.Log(transform.localPosition);// 【…

实用的SQLite数据库可视化管理工具推荐

前言 俗话说得好“工欲善其事&#xff0c;必先利其器”&#xff0c;合理的选择和使用可视化的管理工具可以降低技术入门和使用门槛。今天推荐7款实用的SQLite数据库可视化管理工具(GUI)&#xff0c;帮助大家更好的管理SQLite数据库。 什么是SQLite&#xff1f; SQLite是一个…

【每日一题】2.LeetCode——删除有序数组中的重复项

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有限&#xff0c;欢迎各位大佬指点&…

JavaScript中的事件

&#xff11; 事件&#xff08;Event&#xff09; 事件也就是用户或者浏览器执行的某种动作&#xff0c;而JS与Html之间的交互是通过事件而来的。使用仅在事件发生时执行的**监听器&#xff08;事件处理程序&#xff09;**来订阅事件。web浏览器可以发生多种事件&#xff0c;在…

【Linux】yum本地配置

配置将来是在干什么&#xff1f; yum会根据/etc/yum.repos.d/该路径下面的配置文件&#xff0c;来构成自己的下载路径&#xff0c;&#xff08;根据OS版本&#xff0c;根据你要下载的软件&#xff09;&#xff0c;yum帮助我们下载&#xff0c;安装 一般的机器&#xff0c;内置…

10.常用统计分析方法——主成分分析和因子分析

基础知识&#xff1a; 主成分分析概念 主成分分析PCA&#xff1a;是一种数据降维的技巧&#xff0c;将大量相关变量转化为一组很少的不相关变量&#xff0c;这些无关变量称为主成分。 在特征选择方法中有一种方法是方差过滤&#xff0c;即如果一个特征的方差很小&#xff0c…

yarn的安装及使用教程

Yarn 是一个快速、可靠、安全的包管理工具&#xff0c;用于管理 JavaScript 项目的依赖项。下面是关于 Yarn 的安装和基本使用的详细教程&#xff1a; 安装 Yarn 访问 Yarn 官网 并按照指示下载适合你操作系统的安装程序。安装程序会自动安装 Yarn&#xff0c;并将其添加到系…

强化学习:MuJoCo机器人强化学习仿真入门(1)

声明&#xff1a;我们跳过mujoco环境的搭建&#xff0c;搭建环境不难&#xff0c;可自行百度 下面开始进入正题&#xff08;需要有一定的python基础与xml基础&#xff09;&#xff1a; 下面进入到建立机器人模型的部分&#xff1a; 需要先介绍URDF模型文件和导出MJCF格式 介绍完…

CNAS中兴新支点——软件测试中的非功能测试包含哪些测试内容

软件测试中的非功能测试是指对软件产品在功能之外的其他特性进行测试&#xff0c;包括性能测试、可靠性测试、安全性测试、易用性测试、可维护性测试等。 1、性能测试&#xff1a;性能测试是对软件产品在特定条件下的性能进行测试和评估。包括负载测试、强度测试、数据库容量测…

Flink中的时间和窗口(时间语义,水位线,窗口,迟到数据的处理)

目录 Flink中的时间和窗口 1时间语义 1.1Flink中的时间语义 1.1.1处理时间 1.1.2事件时间 1.2那种时间语义更重要 2 水位线 2.1 事件时间和窗口 2.2 什么是水位线 2.3 如何生成水位线 2.3.1使用WatermarkGenerator 2.3.2使用SourceFunction 2.4 水位线的传递 2.5 水位…

Adobe XD 55.2.12.2软件安装教程(附软件下载地址)

软件简介&#xff1a; 软件【下载地址】获取方式见文末。注&#xff1a;推荐使用&#xff0c;更贴合此安装方法&#xff01; Adobe XD 55.2.12.2是一款专业级的图形界面UI/UX设计工具&#xff0c;拥有丰富的功能和强大的性能。它集成了原型设计、界面设计和交互设计等多种功能…

从0开始学习C++ 第十一课:常量

第十一课&#xff1a;常量 学习目标&#xff1a; 了解什么是常量以及为什么要使用常量。学习如何在C中定义常量。理解字面量常量、const关键字、宏定义 学习内容&#xff1a; 常量的概念 概念&#xff1a; 常量是在程序执行过程中其值不会改变的量。使用常量可以提高程序的可…

系统移植及相关介绍

一.系统移植 系统移植&#xff08;System Porting&#xff09;是将一个操作系统从一种硬件平台迁移到另一种硬件平台的过程。这个过程涉及将操作系统的内核和相关的软件组件调整和适配到新的硬件环境中。 1.嵌入式系统的特点 三低&#xff1a;功耗低 成本低 体积小 两高&am…

【蓝桥备赛】质因数个数——数论

题目链接 质因数个数 个人思路 根据算术基本定理&#xff0c;每个大于1的数都可以写成若干质数相乘的形式。因此&#xff0c;我们从小到大依次求出这个数可能存在的质因数。 参考代码 Java import java.util.Scanner;public class Main {public static void main(String[…

android 扫描某个包下的所有类

注意事项 如果在用Android Studio开发过程中&#xff0c;如果新增了类&#xff0c;扫描不到。只能把APP卸载了&#xff0c;才能扫描到。 可能是Instance Run 的影响。 后面研究一下这篇文章&#xff0c;看看能不能解决 Android 遍历Apk下的所有类文件 package com.trs.nmip.…

Goroutines 和 Channels

为了提高效率&#xff0c;有时需要两个函数一起运行。 f() // 程序会等到 f() 执行完才会执行下一步 go f() // 创建一个 goroutine 来调用 f() ,程序不等待&#xff0c;直接执行下一步1. 下述代码中主函数中的 goroutine 计算了第45个斐波那契数 package mainimport ("…

面试篇-大厂的面试流程和面试注意事项

以前找工作的时候&#xff0c;对于流程中的面试总是好奇流程走到哪一步了&#xff0c;这一轮面试有没有通过&#xff0c;后面不通过还有没有消息通知等问题。今天作为一个求职者和面试官的身份来主要讲一下大厂招聘&#xff0c;内部的面试过程以及流转的流程是什么样的以及该注…

高性能跨平台网络通信框架 HP-Socket v5.9.5

项目主页 : http://www.oschina.net/p/hp-socket开发文档 : https://www.docin.com/p-4478351216.html下载地址 : https://github.com/ldcsaa/HP-SocketQQ Group: 44636872, 663903943 v5.9.5 更新 一、主要更新 问题修复&#xff1a;由于 v5.9.4 版本升级了 KCP 导致 UDP AR…

arcgis 线要素shp数据处理

回顾 上篇写了arcgis 点shp数据处理之少数点和批量点坐标如何生成点要素&#xff0c;这个可能在做一些设计及查询中需要做的第一步。那么今天将对如何点集转线、线要素编辑数据处理做一记录。 一、工具 arcToolbox工具箱、编辑器 二、操作方法 1.点集转线 还是用上篇处理成…