[线程]常见锁策略, synchronized的优化策略, CAS

2024-09-04 06:12

本文主要是介绍[线程]常见锁策略, synchronized的优化策略, CAS,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 一. 常见的锁策略
    • 1. 悲观锁 乐观锁
    • 2. 重量级锁 轻量级锁
    • 3. 自旋锁 挂起等待锁
    • 4. 可重入锁 不可重入锁
    • 5. 公平锁 非公平锁
    • 6. 互斥锁 读写锁
  • 二. 编译器对synchronized锁的优化策略
    • 1. synchronized锁的"自适应"
      • 偏向锁
    • 2. 锁消除
    • 3. 锁粗化
  • 三. CAS
    • CAS的介绍
    • 标准库中的CAS
    • CAS实现自旋锁Spin Lock
    • CAS的ABA问题
    • 解决ABA问题

注意: 接下来介绍的内容, 秋招面试中会考, 但是实际工作中不会用到!!!

一. 常见的锁策略

锁策略, 其实就是在 加锁 / 解锁 / 遇到锁冲突的时候, 都会怎么做

先介绍几个锁的类型:

1. 悲观锁 乐观锁

根据加锁的时候, 预测当前锁冲突的概率大还是小, 还区分悲观锁和乐观锁
如果预测当前锁冲突概率大, 后续要做的工作往往会更多, 加锁的开销就更大, 就叫悲伤锁
如果预测当前锁冲突概率小, 后续要做的工作往往会更少, 加锁的开销就更小, 就叫乐观锁

那么java中使用的synchronized属于哪种锁?
答案: 即使乐观锁, 也是悲观锁
synchronized支持自实行, 能够自动统计出当前的锁冲突的次数, 进行判定当前是锁冲突概率高还是概率低

c++中的std::mutex, 就属于悲伤锁

2. 重量级锁 轻量级锁

一般来说,
悲伤锁后续做的工作往往会很多, 所以是重量级锁
乐观锁后续做的工作往往会很少, 所以是轻量级锁

这两组概念, 可能会混着用

那么java中的synchronized就是既属于轻量级锁, 也属于重量级锁

3. 自旋锁 挂起等待锁

这两个概念可以理解为是获取锁的方式

如果是轻量级锁, 他获取锁的方式就是自旋锁
自旋锁伪代码的实现大概是这样
在这里插入图片描述
此时, cpu在空转, 忙等的状态, 消耗了更多的cpu资源,
但是一旦锁被释放, 就能第一时间拿到锁, 拿到所得速度快

如果是重量级锁, 他获取锁的方式就是挂起等待锁
借助系统中的线程调度机制, 当尝试加锁, 并且这个锁被占用了, 出现锁冲突, 就会让当前这个尝试加锁的线程被挂起(阻塞状态), 此时线程就不参与调度了, 直到这个锁被释放, 然后系统才能唤醒这个线程, 去尝试重新获取锁
此时, 节省了cpu
但是拿到锁的速度就慢了

那么, java中的synchronized
轻量级的部分, 基于自旋锁实现
重量级的部分, 基于挂起等待锁实现

4. 可重入锁 不可重入锁

针对一把锁, 可以连续加锁两次, 就是可重入锁
针对一把锁, 不可以连续加锁两次, 就是不可重入锁

那么, java中的synchronized属于可重入锁

5. 公平锁 非公平锁

这组概念, 可以理解为是获取锁的顺序

公平锁: 严格按照先来后到的顺序来获取锁, 哪个线程等待的时间长, 哪个线程就拿到锁
非公平锁: 若干个线程, 各凭本事, 随机获取到锁, 和线程的等待顺序无关

那么, java中的synchronized属于非公平锁

系统本身的线程调度就是随机的
如果需要引入公平锁, 就需要引入额外的队列, 按照加锁顺序, 把这些获取锁的线程入队列, 再按顺序取

6. 互斥锁 读写锁

这组概念, 可以理解为是锁的种类

互斥锁, 只有两种操作: 加锁和解锁
读写锁, 有三种操作: 加读锁, 加写锁, 解锁

java的读写锁是这样设定的:

  1. 读锁和读锁之间, 不会产生互斥
  2. 写锁和写锁之间, 会产生互斥
  3. 读锁和写锁之间, 会产生互斥

因为多个线程之间读同一个变量, 是不会有安全问题的
在日常开发中, 很多场景, 属于du多写少, 大部分操作都是读
如果使用普通的互斥锁, 此时每次读操作之间, 都会互斥, 就比较影响效率
如果使用读写锁, 就能够有效的降低锁冲突的概率, 提高效率

注意, 这里的读写锁和对mysql中的事务操作不同:
mysql中,
给读操作加锁: 读的时候不能写
给写操作加锁: 写的时候不能读

总结一下上面:
synchronized
即使乐观锁, 也是悲伤锁
即使轻量级锁, 也是重量级锁
即使自旋锁, 也是挂机等待锁
是可重入锁
是非公平锁
是互斥锁

二. 编译器对synchronized锁的优化策略

关于synchronized的锁优化策略, 主要分为以下三块

1. synchronized锁的"自适应"

synchronized是有一个锁升级的过程的:
偏向锁 -> 轻量级锁 -> 重量级锁
未加锁的状态(无锁) -----代码中开始调用synchronized-----> 偏向锁
偏向锁 -----遇到锁冲突-----> 轻量级锁
轻量级锁 -----冲突进一步提升-----> 重量级锁

注意:上述升级过程是不可逆的, 只能升级, 不能降级

偏向锁

synchronized首次对对象进行加锁时, 不是真的加锁, 而只是做了一个"标记", 这个操作非常轻量级, 几乎没有开销
后续如果没有别的线程尝试对这个对象加锁, 就可以保持这个关系, 一直到解锁(修改上述标记), 也几乎没有开销
但是, 如果在偏向锁的状态下, 有某个线程也尝试对这个对象加锁, 就立马把偏向锁升级成轻量级锁, 此时就是真正的加锁了, 真的会发生互斥了

偏向锁本质上就是"懒"字的体现

2. 锁消除

代码中写了加锁操作, 编译器和JVM会对当前的代码做出判定, 看这个地方是否真的需要加锁
如果不需要加锁, 就会自动把加锁操作给优化掉
这样做的目的, 是为了提高效率, 因为加锁是个效率很低的操作

最典型的, 就是在单线程中, 使用synchronized, 就会被优化掉

3. 锁粗化

先介绍一个概念: 锁的粒度
锁的粒度: 表示加锁的范围内, 包含了多少代码,
包含的代码越多, 就认为锁的粒度就越粗
包含的代码越少, 就认为锁的粒度就越细

锁粗化, 就是在有些逻辑中, 需要频繁地对同一对象加锁解锁, 那么编译器就会自动的把多次细粒度的锁, 合并成一次粗粒度的锁, 本质上也是在提高效率

锁粗化的伪代码如下:
在这里插入图片描述

三. CAS

CAS的介绍

CAS是compare and swap , 比较和交换
这时一条cpu指令(是原子的), 可以完成 比较和交换 这样的一套操作下来

为了理解CAS, 可以把CAS想象成一个方法:
在这里插入图片描述
*address: 表示获取内存地址中的值
reg1: 表示寄存器1中的值
reg2: 表示寄存器2中的值

那么此时, CAS做的工作, 其实就是
先比较address内存地址中的值和reg1中的值是否相同
如果相同, 则交换address地址中的值和reg2中的值
其实, 此时的交换操作, 更多理解成是赋值, 把reg2中的值赋值给了内存(因为我们并不关心reg2中的值)

其实, CAS就相当于, 对比较和交换(赋值)操作, 进行了加锁, 但是CAS比加锁高效很多!!

标准库中的CAS

由于CPU提供了上述指令, 因此操作系统内核, 能够完成CAS, 提供了CAS的api
JVM又对系统的CASapi进行进一步封装, 那么我们在java代码中也就可以使用CAS操作了(但是CAS被封装到了一个"unsafe"包中, 不建议使用, 容易出错)
在java中, 也有一些类, 对CAS进行了进一步的封装, 典型的就是"原子类"
在这里插入图片描述
原子类都存放在这个包中
在这里插入图片描述
包中包含了这么多方法, 我们就简单了解一下AtomicInteger类
这个类就相当于对int进行了封装, 可以保证此处的+±-操作, 是原子的
在这里插入图片描述
下面我们写一个多线程代码, 如果我们直接用count++, 可能会出现bug, 原因是count++不是一个原子操作, 我们的解决办法就是加锁, 现在我们就可以使用AtomicInteger来解决

public class Demo31 {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();//count++;}});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);}
}

此时运行的结果:
在这里插入图片描述

像这样, 基于CAS, 不加锁来实现线程安全代码的方式, 也称为==“无锁编程”==

CAS实现自旋锁Spin Lock

自旋锁是基于CAS实现的
自旋锁的伪代码:

public class SpinLock {
//owner表示持有锁的线程是谁, 未加锁的状态, 此时owner就是nullprivate Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就⾃旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}

CAS的ABA问题

ABA 的问题:
假设存在两个线程 t1 和 t2. 有⼀个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使⽤ CAS 把 num 值改成 Z, 那么就需要
• 先读取 num 的值, 记录到 oldNum 变量中.
• 使⽤ CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
但是, 在 t1 执⾏这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, ⼜从 B 改成了 A
线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过⼜改成 A 了. 这个时
候 t1 究竟是否要更新 num 的值为 Z 呢?
到这⼀步, t1 线程⽆法区分当前这个变量始终是 A, 还是经历了⼀个变化过程.

但是, 大多是情况下, 区分不区分不太影响, 也不会有啥问题
但是在一些极端情况, 就可能会产生bug
举例:
假设 滑稽⽼哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. (假设取款操作是按照CAS的方式执行的)
假设下面是取款的伪代码:
在这里插入图片描述
balance为当前用户余额

在取款的过程中, 发生了bug, 按了一下取款, 卡住了, 他又按了一下
取款机创建了两个线程, 并发的来执⾏ -50 操作.
正常的情况:
在这里插入图片描述

如果在t2取款的同时, 有另一个人给滑稽老铁转了500, 引入了t3线程:
在这里插入图片描述

此时就导致, 取款500, 但是余额少了1000
这就是CAS问题的典型bug场景

解决ABA问题

引入版本号, 约定版本号, 只能加, 不能减, 每次操作一次余额, 版本号都要+1
在这里插入图片描述
此时, t1就不会再取款一次了

这篇关于[线程]常见锁策略, synchronized的优化策略, CAS的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

MySQL查看表的最后一个ID的常见方法

《MySQL查看表的最后一个ID的常见方法》在使用MySQL数据库时,我们经常会遇到需要查看表中最后一个id值的场景,无论是为了调试、数据分析还是其他用途,了解如何快速获取最后一个id都是非常实用的技... 目录背景介绍方法一:使用MAX()函数示例代码解释适用场景方法二:按id降序排序并取第一条示例代码解

利用Python实现时间序列动量策略

《利用Python实现时间序列动量策略》时间序列动量策略作为量化交易领域中最为持久且被深入研究的策略类型之一,其核心理念相对简明:对于显示上升趋势的资产建立多头头寸,对于呈现下降趋势的资产建立空头头寸... 目录引言传统策略面临的风险管理挑战波动率调整机制:实现风险标准化策略实施的技术细节波动率调整的战略价

IDEA实现回退提交的git代码(四种常见场景)

《IDEA实现回退提交的git代码(四种常见场景)》:本文主要介绍IDEA实现回退提交的git代码(四种常见场景),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1.已提交commit,还未push到远端(Undo Commit)2.已提交commit并push到

python进行while遍历的常见错误解析

《python进行while遍历的常见错误解析》在Python中选择合适的遍历方式需要综合考虑可读性、性能和具体需求,本文就来和大家讲解一下python中while遍历常见错误以及所有遍历方法的优缺点... 目录一、超出数组范围问题分析错误复现解决方法关键区别二、continue使用问题分析正确写法关键点三

JAVA数组中五种常见排序方法整理汇总

《JAVA数组中五种常见排序方法整理汇总》本文给大家分享五种常用的Java数组排序方法整理,每种方法结合示例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录前言:法一:Arrays.sort()法二:冒泡排序法三:选择排序法四:反转排序法五:直接插入排序前言:几种常用的Java数组排序

正则表达式r前缀使用指南及如何避免常见错误

《正则表达式r前缀使用指南及如何避免常见错误》正则表达式是处理字符串的强大工具,但它常常伴随着转义字符的复杂性,本文将简洁地讲解r的作用、基本原理,以及如何在实际代码中避免常见错误,感兴趣的朋友一... 目录1. 字符串的双重翻译困境2. 为什么需要 r?3. 常见错误和正确用法4. Unicode 转换的

SpringBoot中HTTP连接池的配置与优化

《SpringBoot中HTTP连接池的配置与优化》这篇文章主要为大家详细介绍了SpringBoot中HTTP连接池的配置与优化的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、HTTP连接池的核心价值二、Spring Boot集成方案方案1:Apache HttpCl

PyTorch高级特性与性能优化方式

《PyTorch高级特性与性能优化方式》:本文主要介绍PyTorch高级特性与性能优化方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、自动化机制1.自动微分机制2.动态计算图二、性能优化1.内存管理2.GPU加速3.多GPU训练三、分布式训练1.分布式数据

C语言中的常见进制转换详解(从二进制到十六进制)

《C语言中的常见进制转换详解(从二进制到十六进制)》进制转换是计算机编程中的一个常见任务,特别是在处理低级别的数据操作时,C语言作为一门底层编程语言,在进制转换方面提供了灵活的操作方式,今天,我们将深... 目录1、进制基础2、C语言中的进制转换2.1 从十进制转换为其他进制十进制转二进制十进制转八进制十进

在 PyQt 加载 UI 三种常见方法

《在PyQt加载UI三种常见方法》在PyQt中,加载UI文件通常指的是使用QtDesigner设计的.ui文件,并将其转换为Python代码,以便在PyQt应用程序中使用,这篇文章给大家介绍在... 目录方法一:使用 uic 模块动态加载 (不推荐用于大型项目)方法二:将 UI 文件编译为 python 模