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

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

HDFS—存储优化(纠删码)

纠删码原理 HDFS 默认情况下,一个文件有3个副本,这样提高了数据的可靠性,但也带来了2倍的冗余开销。 Hadoop3.x 引入了纠删码,采用计算的方式,可以节省约50%左右的存储空间。 此种方式节约了空间,但是会增加 cpu 的计算。 纠删码策略是给具体一个路径设置。所有往此路径下存储的文件,都会执行此策略。 默认只开启对 RS-6-3-1024k

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

MySQL高性能优化规范

前言:      笔者最近上班途中突然想丰富下自己的数据库优化技能。于是在查阅了多篇文章后,总结出了这篇! 数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符 临时库表必须以tmp_为前缀并以日期为后缀,备份

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api

JVM 常见异常及内存诊断

栈内存溢出 栈内存大小设置:-Xss size 默认除了window以外的所有操作系统默认情况大小为 1MB,window 的默认大小依赖于虚拟机内存。 栈帧过多导致栈内存溢出 下述示例代码,由于递归深度没有限制且没有设置出口,每次方法的调用都会产生一个栈帧导致了创建的栈帧过多,而导致内存溢出(StackOverflowError)。 示例代码: 运行结果: 栈帧过大导致栈内存

关键字synchronized、volatile的比较

关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字的执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。多线程访问volatile不会发生阻塞,而synchronize