Java 多线程(八)—— 锁策略,synchronized 的优化,JVM 与编译器的锁优化,ReentrantLock,CAS
前言
本文为 Java 面试小八股,一句话,理解性记忆,不能理解就死背吧。
锁策略
悲观锁与乐观锁
悲观锁和乐观锁是锁的特性,并不是特指某个具体的锁。
我们知道在多线程中,锁是会被竞争的,悲观锁就是指锁的竞争程度十分激烈,很多线程都想用这把锁,为了应对这个场景,我们会额外做一些工作。例如:一把锁,此时有几十个线程都想用,并且同一时刻它们都发出申请锁的请求,这时候锁的竞争程度很高,我们可以采取悲观锁的策略,额外做一些工作。
乐观锁则相反,锁的竞争程度很小,就不需要做额外的工作。例如:这一把锁只有两个线程在竞争,并且这两个线程用锁的概率也不是很高,这时候我们可以采用乐观锁策略。
重量级锁与轻量级锁
重量级锁和轻量级锁是遇到特定的场景而出现的解决方案。
上面我们就提到乐观和悲观的场景,重量级锁适用于悲观的场景,相应的也要付出更高的代价,效率相比轻量级锁要低效。
轻量级锁适用于乐观的场景,要付出的代价也要小很多,效率相比重量级锁要高效。
等待挂起锁与自旋锁
挂起等待锁就是指如果一把锁已经被一个线程占用的时候,发现有其他线程还想竞争这把锁,操作系统就会让它们阻塞等待,后续唤醒的时候需要由操作系统的内核来唤醒。
自旋锁就是指如果发现由锁竞争,这时候这些线程不会阻塞等待,而是以忙等的形式进行等待。
看到这里,其实等待挂起锁是适用于悲观的场景下,因为线程竞争激烈,没必要让它们占着 CPU 资源,直接让它们阻塞,释放出CPU 资源,减少资源的消耗。同时由于唤醒的时候是由操作系统的内核实现的,所以操作系统会在内核态和用户态频繁切换,效率也会比较低下。
而自旋锁则适用于乐观的场景,线程以忙等的形式,也就是占用着CPU,但是由于锁竞争不是很激烈,忙等的线程很快就可以获取到锁,所以没必要阻塞等待,因为操作系统的内核唤醒线程的效率要低效一些,所以自旋锁的效率会比等待挂起锁的效率要高。
互斥锁与读写锁
互斥锁就是加锁之后,拥有这把锁的线程才能进行操作,其他线程必须等待拿到锁之后才能进行自己的操作,这就是互斥。
读写锁分为读锁、写锁,因为我们知道线程安全问题是因为写操作而引起的,但是读操作是不会发生线程安全问题的,而读写锁就是针对读操作和写操作进行加锁,读锁和读锁是不会互斥的,写锁与读锁是会互斥的,写锁与写锁是会互斥的(这里可以参考MySQL的幻读、脏读、不可重复读了)
公平锁与非公平锁
公平锁是指锁的分配是按线程的等待时长来分配的,举个例子:假设一把锁已经被一个线程占用,此时有三个线程都想竞争这把锁,那这时候我们会使用额外的数据结构来保存这些线程并且记录每个线程的等待时长,等到锁被释放的时候,操作系统会优先把这把锁分配给等待时长最长的线程,这也避免了线程饥饿。
非公平锁就是随机分配,不按 “先来先得” 的规矩
可重入锁与不可重入锁
可重入锁就是当一个线程拥有这把锁的时候,可以进行重复的加锁。
不可重入锁则相反,即使你这个线程拥有了这把锁,但是还是不能对其进行重复加锁。
synchronized 的优化
根据上面的锁策略,我们来总结一下 synchronized 的特性:
synchronized 具有自适应性
synchronzied 开始时是采取乐观锁策略,如果锁的冲突频繁,则转换为悲观锁
开始时是轻量级锁,如果锁冲突频繁,则转换为重量级锁
synchronized 实现轻量级锁的时候采用自旋锁策略
synchronized 是不公平锁,可重入锁,互斥锁,不是读写锁
锁升级
JVM 会将 synchronized 的锁分为四个状态:无锁、偏向锁、自旋锁、重量级锁。
当还没进入synchronized 的时候,处于无锁状态,一旦进入 synchronized 代码块就会变成偏向锁,偏向锁并非真正加锁,而是通过标记的方式,以此来区分是否真正加锁了。偏向锁本质上相当于 “延迟加锁”,能不加锁就不加锁,避免了不必要的加锁开销,这也是一种懒汉模式的体现。
一旦产生锁竞争,偏向锁就会升级为自旋锁,也就是轻量级锁,如果竞争十分激烈,进一步升级为重量级锁。
synchronized 只能进行锁升级,但是不能进行锁降级!!!
JVM 与编译器的锁优化
锁消除
JVM 会自动检测出一些没有必要加锁的操作,避免这些无意义的加锁操作带来的不必要的开销,JVM 会把这些锁给消除,也就是说你代码加锁了,但是 JVM 给删除了。
大家不用担心这个优化会产生线程安全问题,因为 JVM 的锁消除是在100% 确定这个锁就是一个没必要加的锁,JVM 才会进行锁消除。
锁粗化
首先介绍一个概念,锁的粒度:加锁与解锁之间包含的代码指令越多,锁就越粗;相反,加锁与解锁之间包含的代码指令越少,锁就越细。
public class Test {public static int sum = 0;public static int count = 10000;public static int total = 1000;public static void main(String[] args) {Object locker = new Object();Thread t = new Thread(() -> {for(int i = 0; i < 5000; i++) {synchronized (locker) {sum++;}synchronized (locker) {count--;}synchronized (locker) {total--;}}});}
}
上面的代码就属于锁的粒度太细了,频繁加锁解锁。
Thread t2 = new Thread(() -> {for(int i = 0; i < 5000; i++) {synchronized (locker) {sum++;count--;total--;}}});
这个代码就是锁的粒度粗,加锁和解锁的次数比较少。
⼀段逻辑中如果出现多次加锁解锁,编译器 和 JVM会自动进行锁的粗化。
ReentrantLock
ReentrantLock 和 synchronized 是并列关系,都是用来加锁的,并且都是可重入锁。
简单使用介绍,ReentrantLock 使用 lock() 加锁,unlock() 来解锁,为了避免我们因为加锁和解锁之间有return 或者 抛出异常等等情形没能进入解锁操作,所以这里使用 finally 来包含 unlock() 代码行,避免忘记解锁。
ReentrantLock locker2 = new ReentrantLock();Thread t3 = new Thread(() -> {try {locker2.lock();count++;} finally {locker2.unlock();} });
synchroinzed 和 ReentrantLock 的区别:
synchronized 是 Java提供的关键字,是 JVM 内部通过 C++ 实现的,ReentrantLock 是Java标准库提供的类,由Java代码实现
synchronized 是 通过代码块来实现加锁和解锁的,ReentrantLock 通过 lock() 加锁,unlock() 解锁,一定要注意 unlock() 可能存在未被调用的情况。
ReentrantLock 还有一个 tryLock() 这个方法的调用不会线程产生阻塞,如果加锁成功则返回 true,加锁失败则返回 false,接下来由调用者来根据返回值决定接下来怎么做。可以设置超时时间,当等待时间达到超时时间的时候再返回true / false
ReentrantLock 提供了公平锁的实现,ReentrantLock locker = new ReentrantLock(true);
,默认情况下是非公平锁。
ReentrantLock 搭配的通知等待机制是由Condition 类实现的,相比于 synchronized 的 wait / notify 的功能更强大一些。
synchronized 和 ReentrantLock 都是可重入的互斥锁。
CAS
CAS 全称是 Compare and swap,比较并交换
CAS 在 CPU 里是一条指令,具有原子性。
因此 CAS 操作是线程安全的
举个例子:假设内存原始数据为 V,把这个数据放入寄存器 1 和 寄存器 2 中,数据的加减等操作的结果由寄存器 2 保存。CAS 会先检测原始数据 V 和寄存器 1 的数值是否一致,如果一致的话,可以执行修改也就是把寄存器 2 的结果放入内存中。
下面给出 CAS 的伪代码进行进一步的理解:
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}
第一个参数是内存的数值,第二个参数是寄存器 1 的数值,第三个参数是寄存器 2 的数值。
首先判断内存的数值是否和寄存器的数值一致,如果一致则进行寄存器 2 和内存数值的交换操作,注意 这本质上在 CPU 里是一条指令,具有原子性。
明确的指明:if-else 和 三目运算符在 CPU 里不是一条指令,和 CAS 还是由区别的。
原子类
CPU 有 CAS 指令,并且给操作系统提供了 CAS 的使用接口,操作系统对 CAS 进一步封装,给用户提供相应的接口,C++ 可以直接进行调用,而JVM 是由 C++ 实现的再次对 CAS 进行封装,给Java 程序员提供了 原子类。在这个 java.util.concurrent.atomic
包下就是我们的原子类了。
下面是原子类的伪代码:
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}
}
oldValue 是寄存器,由于Java没有寄存器的使用,所以这里用 int 类型代替。
getAndIncrement() 其实就是 ++ 自增的操作,首先先把内存的数值(value)读到寄存器 1 中,CAS 指令 首先判断 value 是否和寄存器 1 中的数值 oldValue 相等,如果相等就把寄存器 2 的 oldValue + 1 的结果放到内存中,返回 true,否则返回 false 并且进入循环体再次读取内存的数值放入寄存器 1 中。
面对多线程下同时修改一个变量的时候,原子类是最佳的选择。
import java.util.concurrent.atomic.AtomicInteger;public class Demo1 {private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.getAndIncrement();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count.get());}
}
使用 CAS 实现自旋锁
下面是 使用 CAS 实现自旋锁的伪代码:
public class SpinLock {private Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就⾃旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}
核心代码:
while(!CAS(this.owner, null, Thread.currentThread())){}
当 这个锁的拥有者 为 null 的时候,才能由线程Thread.currentThread()
获取这把锁的操作并返回true,否则该线程以忙等的形式等待这把锁。
ABA 问题
ABA 问题是什么?
我们知道 CAS 开始之前会先把内存的数值读到寄存器里,在进行 CAS 的操作之前,可能调度过来别的线程,这个线程对这个内存的数值进行了修改操作,然后又改回来了,看上去这个数值没有任何变化,实际上这个数据已经被动过了,接着把 CAS 调度过来执行,CAS 首先判定内存的数值是否和寄存器的数值一致,如果一致,进行交换操作,这时候数值肯定是一致的,所以交换操作正常被执行了。在进行内存数值和寄存器数值判定是否相等之前内存数值是否被改了又改过,这就是 ABA 问题。
ABA 问题会带来什么BUG?
假设一个人叫做白糖过来取500块钱,假设余额有 4k,这时候 ATM 机有点卡顿,这时候白糖进行了多次按下取款的操作,恰好这时候白糖的好朋友天王星发个信息说之前欠你的500块现在转账还你。
由于多次按下取款操作,就会产生多个取款的线程来执行取款操作,此时中间夹了一个还款操作的线程,大家来看一下下面的流程图:
取款线程 t1 把 account 修改为 3500, 还款线程将 account 修改为 4000, 接着又来了 取款线程 t3 由于内存4000 和寄存器的数值保留的 4000 是一致,所以又将余额修改为了 3500,你会发现白糖小伙就拿出了 500 块,但是余额却多扣了 500, 完了血亏 500,可怜的白糖又要辛苦打工了。
这种事件虽然发生概率极小,但是在庞大的请求数量面前还是不能忽视这个 bug 的。
如何解决 ABA 问题???
因为余额是可以加又可以减的变量,所以会出现上述极端的BUG,但是如果我们换一个指标来作为判断标准的话就可以避免上述的BUG,这里我们可以使用版本号来作为判断的指标,每次修改之后版本号就 + 1,每次进行修改操作的时候判断内存的版本号和寄存器的版本号是否相同
下面给一个伪代码:
int oldVersion = version;if(CAS(version, oldVersion, oldVersion + 1)) {account += 500;}
相关文章:

Java 多线程(八)—— 锁策略,synchronized 的优化,JVM 与编译器的锁优化,ReentrantLock,CAS
前言 本文为 Java 面试小八股,一句话,理解性记忆,不能理解就死背吧。 锁策略 悲观锁与乐观锁 悲观锁和乐观锁是锁的特性,并不是特指某个具体的锁。 我们知道在多线程中,锁是会被竞争的,悲观锁就是指锁…...

【项目分享】法拉利中控台模拟 html+css+js
引入: 制作一个模拟法拉利中控台的网页是一个有趣且富有挑战性的项目。为了简化这个任务,我们可以使用一些HTML、CSS和JavaScript来实现一个基本的界面。以下是一个简单的示例,展示了如何创建一个基本的法拉利中控台模拟网页。 效果展示&…...

Rust 力扣 - 2461. 长度为 K 子数组中的最大和
文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们遍历长度为k的窗口,用一个哈希表记录窗口内的所有元素(用来对窗口内元素去重),我们取哈希表中元素数量等于k的窗口总和的最大值 题解代码 use std::collecti…...
stm32103c8t6 pwm驱动舵机(SG90)
本方法采用通用定时器(TIM2、TIM3、TIM4、TIM5)实现 代码: PWM.h #ifndef __PWM_H // 防止头文件重复包含 #define __PWM_H#include "stm32f10x.h" // 包含STM32F10x系列的设备头文件// 函数声明 void TIM2_PWM_In…...
Python For循环
Python 的 for 循环是自动化重复任务的强大工具,可以使代码更高效、更易于管理。本教程将解释 for 循环的工作原理,探讨不同的应用场景,并提供大量实用示例。无论你是初学者还是希望提升技能的开发者,这些示例都将帮助你更好地在 …...

C++入门——“C++11-右值引用和移动语义”
C11相比于C98增加以许多新特性,让C语言更加灵活好用,但是貌似也增加了许多学习的难度,现在先看第一部分。 一、右值引用和移动语义 1.右值引用和左值引用 在C中,值可以大致分为右值和左值,左值大概是哪些已经被定义的变…...
timm使用笔记
timm(Timm is a model repository for PyTorch)是一个 PyTorch 原生实现的计算机视觉模型库。它提供了预训练模型和各种网络组件,可以用于各种计算机视觉任务,例如图像分类、物体检测、语义分割等等。timm(库提供了预训…...

android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
Android 浏览器是一种运行在Android操作系统上的应用程序,主要用于访问和查看互联网内容。以下是关于Android浏览器的详细介绍: 1. 基本功能 Android浏览器提供了用户浏览网页的基本功能,如: 网页加载:支持加载静态…...

贪心算法入门(一)
1.什么是贪心算法? 贪心算法是一种解决问题的策略,它将复杂的问题分解为若干个步骤,并在每一步都选择当前最优的解决方案,最终希望能得到全局最优解。这种策略的核心在于“最优”二字,意味着我们追求的是以最少的时间和…...
C# ref和out 有什么区别,分别用在那种场景
在C#中,ref和out都是用于按引用传递参数的关键字,但它们有一些细微的差别和使用场景。 ref 关键字 ref 关键字用于按引用传递参数。这意味着当你将一个变量作为参数传递给一个方法时,你不是传递变量的值,而是传递变量的引用。因…...

TikTok直播专线:提升直播效果和体验
作为当今全球最受欢迎的社交媒体平台之一,TikTok为商家提供了无限的商机和市场。然而,商家在使用TikTok时也面临着许多挑战,如网络延迟、直播中断以及账号被封等问题。TikTok直播专线旨在为商家提供高速稳定的网络连接,助力他们在…...

由浅入深逐步理解spring boot中如何实现websocket
实现websocket的方式 1.springboot中有两种方式实现websocket,一种是基于原生的基于注解的websocket,另一种是基于spring封装后的WebSocketHandler 基于原生注解实现websocket 1)先引入websocket的starter坐标 <dependency><grou…...

1-petalinux 问题记录-根文件系统分区问题
在MPSOC上使用SD第二分区配置根文件系统的时候,需要选择对应的bootargs,但是板子上有emmc和sd两个区域,至于配置哪一种mmcblk0就出现了问题,从vivado中的BlockDesign和MLK XCZU2CG原理图来看的话,我使用的SD卡应该属于…...

微信小程序的上拉刷新与下拉刷新
效果图如下: 上拉刷新 与 下拉刷新 代码如下: joked.wxml <scroll-view class"scroll" scroll-y refresher-enabled refresher-default-style"white" bindrefresherrefresh"onRefresh" refresher-triggered&qu…...

【大语言模型】ACL2024论文-05 GenTranslate: 大型语言模型是生成性多语种语音和机器翻译器
【大语言模型】ACL2024论文-05 GenTranslate: 大型语言模型是生成性多语种语音和机器翻译器 GenTranslate: 大型语言模型是生成性多语种语音和机器翻译器 目录 文章目录 【大语言模型】ACL2024论文-05 GenTranslate: 大型语言模型是生成性多语种语音和机器翻译器目录摘要研究背…...

KPRCB结构之ReadySummary和DispatcherReadyListHead
ReadySummary: Uint4B DispatcherReadyListHead : [32] _LIST_ENTRY 请参考 _KTHREAD *__fastcall KiSelectReadyThread(ULONG LowPriority, _KPRCB *Prcb)...

批处理之for语句从入门到精通--呕血整理
文章目录 一、前言二、for语句的基本用法三、文本解析显神威:for /f 用法详解四、翻箱倒柜遍历文件夹:for /r五、仅仅为了匹配第一层目录而存在:for /d六、计数循环:for /l后记 for语句从入门到精通 一、前言 在批处理中&#…...

pycharm小游戏贪吃蛇及pygame模块学习()
由于代码量大,会逐渐发布 一.pycharm学习 在PyCharm中使用Pygame插入音乐和图片时,有以下这些注意事项: 插入音乐: - 文件格式支持:Pygame常用的音乐格式如MP3、OGG等,但MP3可能需额外安装库…...
redis实战--黑马商城 记录
一、视频地址 黑马程序员Redis入门到实战教程,深度透析redis底层原理redis分布式锁企业解决方案黑马点评实战项目 二、笔记地址 Redis基础篇Redis实战篇...

机器人技术革新:人工智能的强力驱动
内容概要 在当今世界,机器人技术与人工智能的结合正如星星与大海,彼此辉映。随着科技的不断进步,人工智能不仅仅是为机器人赋予了“聪明的大脑”,更是推动了整个行业的快速发展。回顾机器人技术的发展历程,我们会发现…...

循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...
xmind转换为markdown
文章目录 解锁思维导图新姿势:将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件(ZIP处理)2.解析JSON数据结构3:递归转换树形结构4:Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...

【大模型】RankRAG:基于大模型的上下文排序与检索增强生成的统一框架
文章目录 A 论文出处B 背景B.1 背景介绍B.2 问题提出B.3 创新点 C 模型结构C.1 指令微调阶段C.2 排名与生成的总和指令微调阶段C.3 RankRAG推理:检索-重排-生成 D 实验设计E 个人总结 A 论文出处 论文题目:RankRAG:Unifying Context Ranking…...

网页端 js 读取发票里的二维码信息(图片和PDF格式)
起因 为了实现在报销流程中,发票不能重用的限制,发票上传后,希望能读出发票号,并记录发票号已用,下次不再可用于报销。 基于上面的需求,研究了OCR 的方式和读PDF的方式,实际是可行的ÿ…...
Android屏幕刷新率与FPS(Frames Per Second) 120hz
Android屏幕刷新率与FPS(Frames Per Second) 120hz 屏幕刷新率是屏幕每秒钟刷新显示内容的次数,单位是赫兹(Hz)。 60Hz 屏幕:每秒刷新 60 次,每次刷新间隔约 16.67ms 90Hz 屏幕:每秒刷新 90 次,…...

C++11 constexpr和字面类型:从入门到精通
文章目录 引言一、constexpr的基本概念与使用1.1 constexpr的定义与作用1.2 constexpr变量1.3 constexpr函数1.4 constexpr在类构造函数中的应用1.5 constexpr的优势 二、字面类型的基本概念与使用2.1 字面类型的定义与作用2.2 字面类型的应用场景2.2.1 常量定义2.2.2 模板参数…...