[线程]常见锁策略, 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

相关文章

C#使用HttpClient进行Post请求出现超时问题的解决及优化

《C#使用HttpClient进行Post请求出现超时问题的解决及优化》最近我的控制台程序发现有时候总是出现请求超时等问题,通常好几分钟最多只有3-4个请求,在使用apipost发现并发10个5分钟也... 目录优化结论单例HttpClient连接池耗尽和并发并发异步最终优化后优化结论我直接上优化结论吧,

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

Python 中 requests 与 aiohttp 在实际项目中的选择策略详解

《Python中requests与aiohttp在实际项目中的选择策略详解》本文主要介绍了Python爬虫开发中常用的两个库requests和aiohttp的使用方法及其区别,通过实际项目案... 目录一、requests 库二、aiohttp 库三、requests 和 aiohttp 的比较四、requ

Redis过期键删除策略解读

《Redis过期键删除策略解读》Redis通过惰性删除策略和定期删除策略来管理过期键,惰性删除策略在键被访问时检查是否过期并删除,节省CPU开销但可能导致过期键滞留,定期删除策略定期扫描并删除过期键,... 目录1.Redis使用两种不同的策略来删除过期键,分别是惰性删除策略和定期删除策略1.1惰性删除策略

MySQL不使用子查询的原因及优化案例

《MySQL不使用子查询的原因及优化案例》对于mysql,不推荐使用子查询,效率太差,执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会受到一定的影响,本文给大家... 目录不推荐使用子查询和JOIN的原因解决方案优化案例案例1:查询所有有库存的商品信息案例2:使用EX

MySQL中my.ini文件的基础配置和优化配置方式

《MySQL中my.ini文件的基础配置和优化配置方式》文章讨论了数据库异步同步的优化思路,包括三个主要方面:幂等性、时序和延迟,作者还分享了MySQL配置文件的优化经验,并鼓励读者提供支持... 目录mysql my.ini文件的配置和优化配置优化思路MySQL配置文件优化总结MySQL my.ini文件

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

C语言线程池的常见实现方式详解

《C语言线程池的常见实现方式详解》本文介绍了如何使用C语言实现一个基本的线程池,线程池的实现包括工作线程、任务队列、任务调度、线程池的初始化、任务添加、销毁等步骤,感兴趣的朋友跟随小编一起看看吧... 目录1. 线程池的基本结构2. 线程池的实现步骤3. 线程池的核心数据结构4. 线程池的详细实现4.1 初

Java子线程无法获取Attributes的解决方法(最新推荐)

《Java子线程无法获取Attributes的解决方法(最新推荐)》在Java多线程编程中,子线程无法直接获取主线程设置的Attributes是一个常见问题,本文探讨了这一问题的原因,并提供了两种解决... 目录一、问题原因二、解决方案1. 直接传递数据2. 使用ThreadLocal(适用于线程独立数据)