【JavaEE】常见的锁策略与CAS的ABA问题

news2024/12/28 8:32:55

文章目录

  • 1 常见的锁策略
    • 1.1 乐观锁与悲观锁
    • 1.2 轻量级锁与重量级锁
    • 1.3 自旋锁与挂起等待锁
    • 1.4 互斥锁与读写锁
    • 1.5 可重入锁与不可重入锁
    • 1.6 公平锁与非公平锁
  • 2 CAS 操作
    • 2.1 CAS 简介
    • 2.2 CAS 的应用
      • 2.2.1 实现原子类
      • 2.2.2 实现自旋锁
  • 3 CAS 的 ABA 问题
  • 写在最后


1 常见的锁策略

1.1 乐观锁与悲观锁

乐观锁
 假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式的对数据是否产生并发冲突进行检测,如果发现了并发冲突,则返回错误信息,让用户来决定如何去做。
在这里插入图片描述

悲观锁
 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

乐观和悲观的区别主要是在于对锁冲突的预估:如果说,预估锁冲突的概率是比较高的,就比较悲观~ 而如果锁冲突的概率是比较低的,就很乐观了~

像 Java 中 synchronizedReentrantLock 等独占锁就是悲观锁思想的实现。

public void performSynchronisedTask() {
    synchronized (this) {
        // 需要同步的操作
    }
}

private Lock lock = new ReentrantLock();
lock.lock();
try {
   // 需要同步的操作
} finally {
    lock.unlock();
}

而像 Java 中 java.util.concurrent.atomic 包下面的原子变量类(比如AtomicInteger、LongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。关于 CAS 我们往后再谈~

那么悲观锁和乐观锁分别适用于什么样的场景呢?

在这里插入图片描述
想必大家一定有和辅导员请假的经历,我们可以把乐观锁和悲观锁看成两类人:A 和 B。假设 A 同学 和 B 同学都想要给辅导员请假。

乐观 A 同学:

乐观 A 同学认为,辅导员是比较 空闲 的。于是,想要请假的时候就直接去办公室找辅导员。如果辅导员比较忙,则请假失败,等辅导员空闲的时候再次尝试请假;而如果辅导员真的比较闲,就能直接请假了。(在本次流程中,没有进行加锁操作,而是直接访问资源。就算没有请假成功,也进行了数据冲突的识别~)

悲观 B 同学:

悲观 B 同学认为,辅导员是比较 忙碌 的。因此,B 同学会先给辅导员打电话,询问其是否有时间(相当于加锁操作)。得到辅导员的肯定答复后,才会去辅导员办公室进行请假流程。如果得到了否定回答,则会等待一定时间,下次再和辅导员确定。

 两种方式乍一眼看区别不太大,但是实际上,适合的场景是不同的:如果辅导员是真的忙,那么使用悲观锁比较合适~ 如果使用乐观锁,就会像 A 同学那样,每次都跑去办公室确认是否空闲,无形中耗费了很多资源;如果辅导员是比较空闲的,那么使用乐观锁比较合适,如果使用悲观锁,则会让效率更低~
在这里插入图片描述

1.2 轻量级锁与重量级锁

轻量和重量则单纯是 从时间消耗 来看的,对于轻量级锁来说,其获取锁的速度会更快;对于重量级锁来说,获取锁的速度会更慢~

对于锁的原子性,追根溯源是 CPU 提供的:

  • CPU 提供了原子操作指令;
  • 操作系统基于原子操作指令,实现了 mutex 互斥锁;
  • JVM 又基于操作系统提供的互斥锁实现了 synchronizedReentrantLock 等关键字和类。

重量级锁 : 加锁机制重度依赖了操作系统提供的 mutex

  • 大量的内核态和用户态的切换
  • 容易引发线程的调度

轻量级锁 :与重量级锁不同,其加锁机制尽可能不使用操作系统提供的互斥锁,而是尽量在 用户态 完成,操作系统提供的 mutex 是下下策~

  • 少量的内核态和用户态的切换
  • 不太容易引发线程的调度

1.3 自旋锁与挂起等待锁

自旋锁是轻量级锁的典型实现,挂起等待锁是重量级锁的典型实现。

自旋锁
 一般情况下,线程在尝试获取锁失败后就会进入阻塞状态,而放弃CPU,等待被调度。而自旋锁则不同,其策略是:如果获取锁失败,则立即尝试获取锁,直到获取锁为止!一旦锁被其他线程释放,就能在第一时间获得锁。

伪代码如下:

while(抢锁(lock) == 失败) {
}

那么该如何区别理解自旋锁和挂起等待锁呢?

这就不得不谈一谈“舔狗”的心路历程了!

在这里插入图片描述
我们可以把自旋锁看作一个标准的舔狗!怎么舔呢?死皮赖脸!死缠烂打!坚持不懈的追求女神~ 当女神和前任分手后就能抓住一切机会上位!

而挂起等待锁就比较摆烂了,它追求女神的方式比较特别,仅仅是通知妹子: “那啥,我喜欢你嗷~” 于是就摆烂了。一直等到女神回过神儿来,看也没什么意思,要不就和你试试吧:“单着没?要不试试?” 可是,在你“上岸”前,女神到底中途又谈了多少任你是不清楚的。

自旋锁相较挂起等待锁来说,由于没有放弃CPU,不涉及线程的调度与阻塞,一旦锁被释放就能第一时间获取~ 但是,如此一来,也有很大的弊端!舔狗虽好,但是累啊!假设,舔狗在追求妹子的时候,妹子和现任谈的天长地久,你就 需要长时间的自旋,而这会消耗 CPU资源,是需要付出巨大成本的!而挂起等待锁,在挂起等待的时候是不需要消耗 CPU 的~

1.4 互斥锁与读写锁

互斥锁是一种很形象的说法:就像一个房间只能住一个人,任何人进去之后就把门锁上了,其他人都不可以进去,直到进去的人重新解锁,既是释放了这个锁资源为止。

而在多线程中,数据的读取方之间不会产生线程安全问题,但是数据的写入方之间以及和读取方之间都需要进行互斥。 而如果在此两种情景下都使用互斥锁,则会产生很大的性能损耗,而读写锁就是为了解决这一问题~
在这里插入图片描述

读写锁就是把读操作和写操作区分对待

  • 读加锁和读加锁之间, 不互斥.
  • 写加锁和写加锁之间, 互斥.
  • 读加锁和写加锁之间, 互斥.

在 Java 标准库,实现了 ReentrantReadWriteLock 类, 实现了读写
锁:

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.

注意,上述内容涉及到互斥,而互斥就涉及到了挂起等待了。而线程一旦被挂起,再次被唤醒就是未知的了。所以,尽可能减少互斥是提高效率的有效途径~

1.5 可重入锁与不可重入锁

可重入锁顾名思义就是可以重新进入的锁,允许同一个线程多次获取同一把锁。 连续的两次加锁并不会导致死锁。

Java 中的 Reentrant 开头命名的锁都是可重入锁,synchronized关键字锁也是可重入的。

不可重入锁该如何理解呢? - > 将自己锁死
在这里插入图片描述
伪代码如下(synchronized是可重入锁,这里只是用伪代码举例,并不会真的阻塞):
即一个线程没有释放锁,又尝试再次加锁

// 加锁!
synchronized(locker) {
    // 第二次加锁,锁已占用,阻塞等待
	synchronized(locker) {
	}
}

可重入锁的实现方式
实现的方式是在锁中记录该锁持有的线程身份,以及一个计数器(记录加锁次数)。如果发现当前加锁的线程就是持有锁的线程,则直接计数自增。

1.6 公平锁与非公平锁

假设有三个线程 A B C,A 获取锁成功, B 获取锁失败,阻塞等待, C 获取锁失败,同样阻塞等待,此时 A 释放锁,B C 会发生什么?

公平锁: 遵循 “先来后到”。当 A 释放锁后,B 能先于 C 获取锁。

非公平锁: 不遵循 “先来后到”,当 A 释放锁后,B 和 C 都有可能获取到锁。


2 CAS 操作

2.1 CAS 简介

Compare and swap,字面意思:”比较并交换“,相当于通过一个原子的操作, 同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤。本质上需要 CPU 指令的支撑。一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V。(交换)
  3. 返回操作是否成功。

一句话概括,就是将寄存器A的值和内存V的值进行比对,如果值相同,就把寄存器B的值和V的值进行交换。

伪代码如下:

boolean CAS(address, expectValue, swqpValue) {
	if (&address == expectValue) {
		&address = swqpValue;
		return true;
	}
	return false;
}

address -> 内存地址, expectValue -> 寄存器A,swapValue -> 寄存器B
CAS操作是一条 CPU 指令,具有原子性,伪代码只是用于方便理解。

CAS 可以视为一个乐观锁,当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

2.2 CAS 的应用

2.2.1 实现原子类

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的。
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作。

操作示例代码

AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

原子类通过CAS操作是如何实现的呢?

伪代码如下:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

由于 Java 没法表示在寄存器中的值,oldValue 可以视为一个寄存器。
如果发现 value 和 oldValue 值相同,就把 oldValue + 1 设置到 value 中,相当于进行了 ++ 操作。

2.2.2 实现自旋锁

即通过 CAS 查看当前锁是否被某个线程所持有,如果已经被持有了则进行自旋等待;如果没有被持有,就把 owner 设置为当前加锁的线程~

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;
   }
}

3 CAS 的 ABA 问题

什么是ABA问题呢?

假设有两个线程 t1 和 t2,有一个共享变量 num,其初始值为 A。接下来,线程 t1 想要使用 CAS 把值改成 B 就需要进行下面的步骤:

  • 读取 num 的值,记录到 oldNum 变量中
  • 使用 CAS 判定当前 num 是否为 A,如果为 A 则修改为 B

但是,在 t1 执行这两个操作之间,线程 t2 可能进行了某种骚操作!t2 可能将 num 的值修改成了 B 又修改成了 A。
在这里插入图片描述
需要明确的是,t1 线程使用 CAS 的初衷是期望 num 不变则进行修改,然并卵,t1 线程并不知道 num 是否被 t2 线程更改过并进行了复原~ 虽然,单单对修改 num 值来说,并没什么大问题。

ABA 问题可能引发的 BUG

在大部分情况下,t2 这样反复横跳的骚操作并不会引发什么问题,但是总有些特殊情况~

举个例子~

假设小黄有100块钱,想从 ATM 取款机上取 50 块钱。假设 ATM 创建了两个线程 t1 和 t2 并发执行 -50 的扣款过程~ 我们期望 t1 线程扣款成功,t2 线程扣款失败,使用 CAS 处理扣款过程。

一般情况下:

  1. t1 和 t2 线程都获取当前存款为 100 块,期望更新为 50,假设 t1 先执行,t2 阻塞等待
  2. t1 线程执行,扣款成功,当前存款剩余 50 块
  3. t2 线程执行,发现当前的存款是 50 块,与一开始的 100 块不同,所以扣款失败~

在这里插入图片描述

异常情况:

  1. t1 和 t2 线程都获取当前存款为 100 块,期望更新为 50,假设 t1 先执行,t2 阻塞等待
  2. t1 线程执行,扣款成功,当前存款剩余 50 块
  3. 在 t2 线程执行前,七七给小黄进行了转账 50 块的操作
  4. t2 线程执行,发现当前的存款是 100 块,与一开始的 100 块相同,所以扣款成功,当前存款剩余 50 块~

天呐~ 发现没有!因为 ABA 问题,导致了扣款两次!!!
在这里插入图片描述

如果解决 ABA 问题引发的 BUG?

想要解决上述所述问题,其实也很简单,只需要在原来的基础上 引入一个版本号~

具体操作如下:

  • CAS 在读取旧值的时候,也需要读取一个版本号。
  • 如果需要进行修改数据,则进行版本号的判断。如果当前版本号与之前读到的版本号旧值相同,则进行修改,并且版本号 + 1;如果当前版本号比之前的版本号旧值还要高,则认为已经修改过,不作处理。

对于上述的转账过程中,由于 ABA 问题引发的异常情况,引入版本号后也可以得到解决,具体如下:

  1. t1 和 t2 线程都获取当前存款为 100 块,期望更新为 50,假设 t1 先执行,t2 阻塞等待。默认读取的 版本号初始为 1。
  2. t1 线程执行,扣款成功,当前存款剩余 50 块,并进行版本号 + 1 的操作,此时 版本号为 2。
  3. 在 t2 线程执行前,七七给小黄进行了转账 50 块的操作, 版本号 + 1,此时 版本号为 3。
  4. t2 线程执行,发现当前的存款是 100 块,与一开始的 100 块相同,但是由于当前版本号为 3,而之前读取的版本号为 1,高于旧值,所以不进行修改操作,余额依然为 100。

在这里插入图片描述


写在最后

 以上便是本文的全部内容啦!创作不易,如果你有任何问题,欢迎私信,感谢您的支持!
本文被 JavaEE编程之路 收录点击订阅专栏 , 持续更新中。
 创作不易,如果你有任何问题,欢迎私信,感谢您的支持!

在这里插入图片描述

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

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

相关文章

Nacos 客户端的服务发现与服务订阅机制的纠缠 - 篇七

Nacos 客户端的服务发现与服务订阅机制的纠缠 - 篇七 历史篇章 🕐Nacos 客户端服务注册源码分析-篇一 🕑Nacos 客户端服务注册源码分析-篇二 🕒Nacos 客户端服务注册源码分析-篇三 🕓Nacos 服务端服务注册源码分析-篇四 &am…

最新入河排污口设置论证、水质影响预测与模拟、污水处理工艺分析及建设项目入河排污口方案报告书

随着水资源开发利用量不断增大,全国废污水排放量与日俱增,部分河段已远远超出水域纳污能力。近年来,部分沿岸入河排污口设置不合理,超标排污、未经同意私设排污口等问题逐步显现,已威胁到供水安全、水环境安全和水生态安全&#x…

ChatGPT探索系列之五:讨论人工智能伦理问题及ChatGPT的责任

文章目录 前言一、安全二、隐私和道德三、我们应该做什么总结 前言 ChatGPT发展到目前,其实网上已经有大量资料了,博主做个收口,会出一个ChatGPT探索系列的文章,帮助大家深入了解ChatGPT的。整个系列文章会按照一下目标来完成&am…

STM32(十六)正交编码器

一、简介 增量式编码器 增量式编码器也称为正交编码器,是通过两个信号线的脉冲输出来进行数据处理,一个输出脉冲信号就对应于一个增量位移,编码器每转动固定的位移,就会产生一个脉冲信号 通过读取单位时间脉冲信号的数量&#xff…

自动修改文章的软件-自动修改文案原创软件

有没有自动修改文章的软件 修改文章可能是很多人日常工作中必须完成的任务,但一般情况下,这需要人工完成。幸运的是,现在有很多文章修改软件可以帮助我们节省时间和精力。本文将向您介绍一款优秀的修改文章软件,名为147SEO&#…

SLAM论文速递【SLAM—— TwistSLAM:动态环境下的约束SLAM】—4.17(2)

论文信息 题目: Optimization RGB-D 3-D Reconstruction Algorithm Based on Dynamic SLAM 基于动态SLAM的RGB-D三维重建算法优化论文地址: https://ieeexplore.ieee.org/abstract/document/10050782发表期刊: IEEE Transactions on Instru…

Apache POI 实现用Java操作Excel完成读写操作

简介 Apache POI是一个用于操作Microsoft Office格式文件(包括xls、docx、xlsx、pptx等)的Java API库。POI全称为Poor Obfuscation Implementation,是Apache Software Foundation的一个开源项目。它提供了一组Java API,使得Java程…

LLM_StableDiffusion_studio发布

背景: 从chatgpt发布到现在已经超过半年时间,AGI的势头越来越猛。大家都在做各种的尝试和组合,把chatgpt通用的强大的知识表达和理解能力尝试应用在自己的业务场景。前期也是出现非常多的业务应用,但是主要还是围绕chatgpt本身已…

循环神经网络(RNN)简单介绍—包括TF和PyTorch源码,并给出详细注释

文章目录 循环神经网络(RNN)入门教程1. 循环神经网络的原理2. 循环神经网络的应用3. 使用keras框架实现循环神经网络3.1导入对应的库及加载数据集3.2.数据预处理3.3定义RNN模型3.4训练模型3.5测试模型 4.使用PyTorch框架实现上述功能—注释详细5.结论 循…

动静态库的制作和使用

动静态库 一,什么是库二,静态库的制作静态库原理 三,动态库的制作四,动态库的配置五,动态库的加载 一,什么是库 🚀库这个东西我们一直在使用,举个简单了例子,无论你是用…

netplan, NetworkManager, systemd-networkd简介

1、systemd-networkd简介 systemd-networkd是systemd 的一部分 ,负责 systemd 生态中的网络配置部分(systemd-networkd.service, systemd-resolved.service)。使用 systemd-networkd,你可以为网络设备配置基础的 DHCP/静态IP网络等,还可以配…

U8W/U8W-Mini使用与常见问题解决

U8W/U8W-Mini使用与常见问题解决 U8WU8W/U8W-mini简介准备工作U8W/U8W-mini在线联机下载U8W/U8W-mini脱机下载第一步,把程序下载到U8W/U8W-mini烧录器中:第二步,用U8W/U8W-mini烧录器给目标单片机脱机下载 U8W/U8W-mini烧录器使用中常见的问题…

初识Linux运维

一.初识Linux 1.Linux系统内核 内核提供了Linux系统的主要功能,如硬件调度管理的能力。 Linux内核是免费开源的,任何人都可以查看内核的源代码,甚至是贡献源代码。 2.Linux系统发行版 内核无法被用户直接使用,需要配合应用程…

淘宝iOS拍立淘微距能力探索与实现

画面模糊问题的源头也是来自用户的微距体验不佳,我们对问题深入分析,适当拆解。通过 Apple Development Doc 的查阅及实践,一步步抽丝剥茧,最终完美解决用户的体验痛点,也为我们自身沉淀了展示微距的能力。 前言 在最近…

Unix和Linux

UNIX 诞生于 20 世纪 60 年代末 Windows 诞生于 20 世纪 80 年代中期 Linux 诞生于 20 世纪 90 年代初 1965 年,贝尔实验室、美国麻省理工学院和通用电气公司联合发起了Multics 工程计划,目标是开发一种交互式的、具有多道程序处理能力的分时操作系统&a…

NTP服务与SSH服务

NTP:时间同步服务,采用UDP协议,端口号为123。 配置NTP时间服务器,确保客户端主机能和服务主机同步时间 首先,我们必须确保服务端与客户端在同一时区。 更改时区:timedatectl set-timezone asia/shanghai …

隋唐洛阳“西宫”:上阳宫的GIS视角

隋唐洛阳城简介 营建 隋大业元年(605年),在隋炀帝的授意下,隋代著名城市设计师宇文恺,在汉魏故城以西重新选址,历时8个月,日役劳工200万,兴建新都洛阳城。 城和苑 隋唐洛阳城采用…

页面注册案例

效果图: 分析业务模块: 发送验证码模块各个表单验证模块勾选已经阅读同意模块下一步验证全部模块:只要上面有一个input验证不通过就不同意提交 业务 1 :发送验证码 用户点击之后,显示05秒后重新获取时间到了&…

大国护眼学习笔记01

第一天(23.4.17) 2—11节什么是近视? 1、“近视离焦”是指成像点落在视网膜的哪里? 前面 2、“远视离焦”是指成像点落在视网膜的哪里? 后面 3、眼轴变长时,成像点会往前移还是往后移? 前移 4、…

毛灵栋 : 以兴趣为壤,育能力之实 | 提升之路系列(一)

导读 为了发挥清华大学多学科优势,搭建跨学科交叉融合平台,创新跨学科交叉培养模式,培养具有大数据思维和应用创新的“π”型人才,由清华大学研究生院、清华大学大数据研究中心及相关院系共同设计组织的“清华大学大数据能力提升项…