Java 多线程(八)—— 锁策略,synchronized 的优化,JVM 与编译器的锁优化,ReentrantLock,CAS

news2024/11/30 5:40:29

前言

本文为 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;
        }

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

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

相关文章

Uniapp如何处理后端返回图片流验证码

登录验证码请求接口返回内容为乱码 处理代码 uni.request({url: 你请求的地址,method: POST,data:data,header:header,//请求头responseType: arraybuffer,//告诉服务器你希望得到的响应类型为arraybuffer&#xff08;二进制数据&#xff09;类型success: res > {let resul…

QT 多语言转换 ts、qm

QT开发之路 企业级开发系列文章&#xff0c;主要目标快速学习、完善、提升 相关技能 高效完成企业级项目开发 分享在企业中积累的实用技能和经验。 通过具体的编码过程、代码示例、步骤详解、核心内容和展示的方法解决遇到的实际问题。 阅读前声明 本系列文章属于付费内容 禁止…

【论文笔记】Perceiver: General Perception with Iterative Attention

&#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xff0c;为万世开太平。 基本信息 标题: Perceiver: General Perce…

spring-第十一章 注解开发

spring 文章目录 spring前言1.注解回顾1.1原理1.2springIOC注解扫描原理1.2.1解释1.2.2案例 2.声明bean的注解补充&#xff1a;Bean注解&#xff0c;管理三方包对象 3.spring注解的使用3.1加入aop依赖3.2配置文件中添加context命名空间3.3配置文件中指定要扫描的包3.4在Bean上使…

【CSS in Depth 2 精译_055】8.3 伪类 :is() 和 :where() 的正确打开方式

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 【第三部分 现代 CSS 代码组织】 ✔️【第八章 层叠图层及其嵌套】 ✔️ 8.1 用 layer 图层来操控层叠规则&#xff08;上篇&#xff09; 8.1.1 图层的定义&#xff08;上篇&#xff09;8.1.2 图层的…

20241028给荣品RD-RK3588-AHD开发板刷Rockchip原厂的Buildroot之后确认AP6275P的蓝牙BLE功能

20241028给荣品RD-RK3588-AHD开发板刷Rockchip原厂的Buildroot之后确认AP6275P的蓝牙BLE功能 2024/10/28 16:56 手机&#xff1a;realme的GT NEO5【只要手机支持蓝牙BLE即可】 APK&#xff1a;在【你用的手机】应用市场下载 BLE调试助手并安装之后别用。 缘起&#xff1a;为了简…

大模型,多模态大模型面试问题记录【时序,Qformer,卷积,感受野,ControlNet,IP-adapter】

大模型&#xff0c;多模态大模型面试问题记录24/10/27 问题一&#xff1a;视频生成例如Sora或者视频理解internvl2模型怎么提取时序上的特征。问题二&#xff1a;Qformer介绍训练阶段一训练阶段二 问题三&#xff1a;卷积维度计算公式&#xff0c;感受野1. 卷积层输出高度和宽度…

Spring Cloud --- Sentinel 授权规则

授权规则概述 在某些场景下&#xff0c;需要根据调用接口的来源判断是否允许执行本次请求。此时就可以使用 Sentinel 提供的授权规则来实现&#xff0c;Sentinel 的授权规则能够根据请求的来源判断是否允许本次请求通过。 在 Sentinel 的授权规则中&#xff0c;提供了 白名单…

自修室预约系统|基于java和小程序的自修室预约系统设计与实现(源码+数据库+文档)

自修室预约系统 目录 基于java和小程序的自修室预约系统设计与实现 一、前言 二、系统设计 三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农|毕设布道师&#x…

asp.net core 入口 验证token,但有的接口要跳过验证

asp.net core 入口 验证token,但有的接口要跳过验证 在ASP.NET Core中&#xff0c;你可以使用中间件来验证token&#xff0c;并为特定的接口创建一个属性来标记是否跳过验证。以下是一个简化的例子&#xff1a; 创建一个自定义属性来标记是否跳过验证&#xff1a; public clas…

【华为HCIP实战课程二十五】中间到中间系统协议IS-IS配置实战续系统ID区域ID,网络工程师

上章简单讲解了ISIS基本配置,本章继续详细讲解ISIS配置及实施 IS-IS配置拓扑 1、R1进行配置IS-IS [R1]display current-configuration configuration isis isis 1 network-entity 49.0124.1111.1111.1111.00 //配置NET地址,由三部分组成,区域ID、系统ID和固定的SEL 00 i…

Kafka集群数据迁移方案

概述 MirrorMaker2&#xff08;后文简称 MM2&#xff09;在 2019 年 12 月随 Kafka 2.4.0 一起推出。顾名思义&#xff0c;是为了解决 Kafka 集群之间数据复制和数据同步的问题而诞生的 Kafka 官方的数据复制工具。在实际生产中&#xff0c;经常被用来实现 Kafka 数据的备份&a…

鼠标增强工具 MousePlus v5.3.9.0 中文绿色版

MousePlus 是一款功能强大的鼠标增强工具&#xff0c;它可以帮助用户提高鼠标操作效率和精准度。该软件可以自定义鼠标的各种功能和行为&#xff0c;让用户根据自己的习惯和需求来调整鼠标的表现。 详细功能 自定义鼠标按钮功能&#xff1a;可以为鼠标的各个按钮设置不同的功能…

【大模型系列】Mini-InternVL(2024.10)

Paper&#xff1a;https://arxiv.org/pdf/2410.16261Github&#xff1a;https://github.com/OpenGVLab/InternVL/tree/main/internvl_chat/shell/mini_internvlAuthor&#xff1a;Zhangwei Gao et al. 上海人工智能实验室 文章目录 0 总结(省流版)1 模型结构1.1 InternViT-300M…

探讨Facebook的AI研究:未来社交平台的技术前瞻

在数字时代&#xff0c;社交媒体已成为人们日常生活的重要组成部分。作为全球最大的社交网络之一&#xff0c;Facebook不断致力于人工智能&#xff08;AI&#xff09;的研究与应用&#xff0c;以提升用户体验、增强平台功能并推动技术创新。本文将探讨Facebook在AI领域的研究方…

一键导入Excel到阿里云PolarDB-MySQL版

今天&#xff0c;我将分享如何一键导入Excel到阿里云PolarDB-MySQL版数据库。 准备数据 这里&#xff0c;我们准备了一张excel表格如下&#xff1a; 连接到阿里云PolarDB 打开的卢导表&#xff0c;点击新建连接-选择阿里云PolarDB-MySQL版。如果你还没有这个工具&#xff0c;…

[NSSCTF 2nd]php签到 详细题解

知识点: linux文件后缀名绕过 表单文件上传 pathinfo 函数 file_put_contents()函数 命令执行 代码审计: <?phpfunction waf($filename){$black_list array("ph", "htaccess", "ini");$ext pathinfo($filename, PATHINFO_EXTENSION…

[0260].第25节:锁的不同角度分类

MySQL学习大纲 我的数据库学习大纲 从不同维度对锁的分类&#xff1a; 1.对数据操作的类型划分:读锁和写锁 1.1.读锁 与 写锁概述&#xff1a; 1.对于数据库中并发事务的读-读情况并不会引起什么问题。对于写-写、读-写或写-读这些情况可能会引起一些问题&#xff0c;需要使用…

云原生后端开发教程

云原生后端开发教程 引言 随着云计算的普及&#xff0c;云原生架构逐渐成为现代软件开发的主流。云原生不仅仅是将应用部署到云上&#xff0c;而是一种构建和运行应用的方式&#xff0c;充分利用云计算的弹性和灵活性。本文将深入探讨云原生后端开发的核心概念、工具和实践&a…

Docker 常用命令全解析:提升对雷池社区版的使用经验

Docker 常用命令解析 Docker 是一个开源的容器化平台&#xff0c;允许开发者将应用及其依赖打包到一个可移植的容器中。以下是一些常用的 Docker 命令及其解析&#xff0c;帮助您更好地使用 Docker。 1. Docker 基础命令 查看 Docker 版本 docker --version查看 Docker 运行…