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

相关文章

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java中如何正确的停掉线程

《Java中如何正确的停掉线程》Java通过interrupt()通知线程停止而非强制,确保线程自主处理中断,避免数据损坏,线程池的shutdown()等待任务完成,shutdownNow()强制中断... 目录为什么不强制停止为什么 Java 不提供强制停止线程的能力呢?如何用interrupt停止线程s

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Python实战之SEO优化自动化工具开发指南

《Python实战之SEO优化自动化工具开发指南》在数字化营销时代,搜索引擎优化(SEO)已成为网站获取流量的重要手段,本文将带您使用Python开发一套完整的SEO自动化工具,需要的可以了解下... 目录前言项目概述技术栈选择核心模块实现1. 关键词研究模块2. 网站技术seo检测模块3. 内容优化分析模

Java实现复杂查询优化的7个技巧小结

《Java实现复杂查询优化的7个技巧小结》在Java项目中,复杂查询是开发者面临的“硬骨头”,本文将通过7个实战技巧,结合代码示例和性能对比,手把手教你如何让复杂查询变得优雅,大家可以根据需求进行选择... 目录一、复杂查询的痛点:为何你的代码“又臭又长”1.1冗余变量与中间状态1.2重复查询与性能陷阱1.

Python内存优化的实战技巧分享

《Python内存优化的实战技巧分享》Python作为一门解释型语言,虽然在开发效率上有着显著优势,但在执行效率方面往往被诟病,然而,通过合理的内存优化策略,我们可以让Python程序的运行速度提升3... 目录前言python内存管理机制引用计数机制垃圾回收机制内存泄漏的常见原因1. 循环引用2. 全局变

python 线程池顺序执行的方法实现

《python线程池顺序执行的方法实现》在Python中,线程池默认是并发执行任务的,但若需要实现任务的顺序执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录方案一:强制单线程(伪顺序执行)方案二:按提交顺序获取结果方案三:任务间依赖控制方案四:队列顺序消

Python多线程应用中的卡死问题优化方案指南

《Python多线程应用中的卡死问题优化方案指南》在利用Python语言开发某查询软件时,遇到了点击搜索按钮后软件卡死的问题,本文将简单分析一下出现的原因以及对应的优化方案,希望对大家有所帮助... 目录问题描述优化方案1. 网络请求优化2. 多线程架构优化3. 全局异常处理4. 配置管理优化优化效果1.

MySQL设置密码复杂度策略的完整步骤(附代码示例)

《MySQL设置密码复杂度策略的完整步骤(附代码示例)》MySQL密码策略还可能包括密码复杂度的检查,如是否要求密码包含大写字母、小写字母、数字和特殊字符等,:本文主要介绍MySQL设置密码复杂度... 目录前言1. 使用 validate_password 插件1.1 启用 validate_passwo

MySQL中优化CPU使用的详细指南

《MySQL中优化CPU使用的详细指南》优化MySQL的CPU使用可以显著提高数据库的性能和响应时间,本文为大家整理了一些优化CPU使用的方法,大家可以根据需要进行选择... 目录一、优化查询和索引1.1 优化查询语句1.2 创建和优化索引1.3 避免全表扫描二、调整mysql配置参数2.1 调整线程数2.