多线程 - 锁策略 & CAS

常见的锁策略

此处谈到的锁策略,不局限于 Java,C++,Python,数据库,操作系统......但凡是涉及到锁,都是可以应用到下列的锁策略的

乐观锁 vs 悲观锁

锁的实现者,预测接下来锁冲突(锁竞争,两个线程针对一个对象加锁,产生阻塞等待了)的概率是大,还是不大,根据这个冲突的概率,来接下来做什么

~~ 这不是两把具体的锁,而是"两类锁".

悲观锁:预测锁竞争不是很激烈.

乐观锁:预测锁竞争会很激烈.

通常来说,悲观锁一般做的工作更多一些,效率更低一些,乐观锁做的工作会更少一些,效率更高一点.但是并不绝对.

悲观锁和乐观锁,唯一的区别,主要就是看预测锁竞争激烈程度的结论.

轻量级锁 vs 重量级锁

轻量级锁: 加锁解锁,过程更快更高效.

重量级锁: 加锁解锁,过程更慢,更低效.

一个乐观锁很可能也是一个轻量级锁,一个悲观锁很可能也是一个重量级锁.

多数情况下,乐观锁,也是一个轻量级锁.

多数情况下,悲观锁也是一个重量级锁.

自旋锁 vs 挂起等待锁

自旋锁是轻量级锁的一种典型实现.

挂起等待锁是重量级锁的一种典型实现.

互斥锁 vs 读写锁

互斥锁

synchronized,是互斥锁

synchronized 只有两个操作:

1.进入代码块,加锁

2.出了代码块,解锁

加锁,就只是单纯的加锁,没有更细化的区分了

读写锁

~~ 读写锁,能够把读和写两种加锁区分开

读写锁:

1.给读加锁

2.给写加锁

3.解锁

注: 如果多个线程读同一个变量,不会涉及到线程安全问题!!!

读写锁中,约定:

1.读锁和读锁之间,不会锁竞争.不会产生阻塞等待,不会影响程序的速度,代码执行很快.

2.写锁和写锁之间,有锁竞争,减慢速度,但是保证准确性.

3.读锁和写锁之间,也有锁竞争,减慢速度,但是保证准确性.

注:

1.非必要不加锁.

2.读写锁更适合于,一写多读的情况.

3.多线程针对同一个变量并发读,这时是没有线程安全问题的,也就不需要加锁控制.

4.很多开发场景中,读操作非常高频,比写操作的频率高很多.

5.在Java标准库里面也提供了读写锁的具体实现(两个类,读锁类,写锁类).

公平锁 vs 非公平锁

此处把公平定义成"先来后到"

公平锁: 当女神分手之后,就由等待队列中,最早来的舔狗上位.

非公平锁: 雨露均沾了.

注: 操作系统和 Java synchronized 原生都是"非公平锁",操作系统这里的针对加锁的控制,本身就依赖于线程调度顺序.这个调度顺序是随机的,不会考虑到这个线程等待锁多久了.

要想实现公平锁,就得在这个基础上,能够引入额外的东西(引入一个队列,让这些加锁的线程去排队).

可重入锁 vs 不可重入锁

不可重入锁: 一个线程针对一把锁,连续加锁两次,出现死锁.

可重入锁: 一个线程针对一把锁,连续加锁多次都不会死锁.

注: 系统原生的锁,C++标准库的锁,Python标准库的锁...都不是可重入的锁!

synchronized是个"可重入锁",(加锁的时候会判定一下,看当前尝试申请锁的线程是不是已经就是锁的拥有者了,如果是,直接放行)

synchronized锁

针对上述六组锁策略, synchronized这把锁属于哪种呢??

synchronized 既是悲观锁,也是乐观锁 ~~ synchronized会根据当前锁竞争的激烈程度,自适应.

既是轻量级锁,也是重量级锁 ~~ synchronized默认是轻量级锁,如果发现当前锁竞争比较激烈,就会转换成重量级锁.

synchronized这里的轻量级锁部分基于自旋锁的方式实现,synchronized这里的重量级锁部分基于挂起等待锁的方式实现.

synchronized不是读写锁.

synchronized是非公平锁.

synchronized是可重入锁.

总结: 上述谈到的六种锁策略,可以视为是"锁的形容词".

CAS

CAS ~~ 全称Compare and swap, 字面意思:"比较并交换"

一个 CAS 涉及到以下操作 :

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

1.比较 A 与 V 是否相等。(比较)

2.如果比较相等,将 B 写入 V。(交换)

3.返回操作是否成功

此处最特别的地方,上述这个 CAS 的过程,并非是通过一段代码实现的,而是通过一条 CPU 指令 完成的 => CAS 操作是原子的 ~~ 就可以在一定程度上回避线程安全问题

因此解决线程安全问题除了加锁之外,又有了一个新的方向了.

小结: CAS 可以理解成是 CPU 给我们提供的一个特殊指令,通过这个指令,就可以一定程度的处理线程安全问题.

CAS 伪代码

注: 下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解 CAS 的工作流程.

CAS 的应用场景

1.实现原子类(Java 标准库里提供的类)

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

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: fly(逐梦者)
 * Date: 2023-10-09
 * Time: 10:49
 */
public class ThreadDemo28 {
    public static void main(String[] args) throws InterruptedException {
        // 这些原子类,就是基于 CAS 实现了 自增,自减等操作.此时进行这类操作不需要加锁,也是线程安全的.
        AtomicInteger count = new AtomicInteger(0);

        // 使用原子类, 来解决线程安全问题
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                // 因为 java 不支持运算符重载,所以只能使用普通方法来表示自增自减
                count.getAndIncrement();// count++
                //count.incrementAndGet(); => ++ count
                //count.getAndDecrement(); => count--
                //count.decrementAndGet(); => -- count
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                count.getAndIncrement();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count.get());
    }
}

伪代码实现AtomicInteger

2.实现自旋锁

自旋锁伪代码

注: CAS 属于"特殊方法",synchronized 属于"通用方法" --> 各种场景,都能使用(打击面广)

CAS 的典型问题: ABA 问题

CAS 在运行中的核心,检查 value 和 oldValue 是否一致.++如果一致,就视为value 中途没有被修改过++,所以进行下一步交换操作是没问题的.

这里一致,可能是没改过.也可能是,改过,但是还原回来了?!

把 value 的值设为A的话, CAS 判定 value 为 A,此时可能确实value始终是A,也可能是value本来是A,被改成了B,又还原成了A ...

ABA 问题就相当于,买个手机,买到的这个手机,可能是新机,也可能是翻新机.

翻新机: 二手的,被销售商回收了,经过一些翻新操作(把外壳换了,重新包装).

ABA 这个情况,大部分情况下,其实是不会对代码/逻辑产生太大影响的,但是不排除一些"极端情况",也是可能造成影响的.

例子:

上述场景,概率非常低!!!一方面,恰好,滑稽这边多按了几次,产生多个扣款动作了,另一方面,恰好在这个非常极限的时间内,有人转账了一样的金额.

解决方案

针对当前问题,采取的方案,就是加入一个版本号.想象成,初始版本号是1,每次修改版本号都+1,然后进行 CAS 的时候不是以金额为基准了,而是以版本号为基准.此时,版本号要是没变,就是一定没有发生改变(版本号是只能增长,不能降低的).

相关推荐
limengshi1383923 小时前
通信工程学习:什么是IP网际协议
网络·网络协议·学习·tcp/ip·信息与通信
IM_DALLA5 小时前
【Verilog学习日常】—牛客网刷题—Verilog快速入门—VL70
学习·fpga开发·verilog学习
布丁不叮早起枣祈5 小时前
10.6学习
学习
望森FPGA5 小时前
HDLBits中文版,标准参考答案 | 3.1.2 Multiplexers | 多路复用器
学习·fpga开发
985小水博一枚呀5 小时前
【深度学习基础模型】稀疏自编码器 (Sparse Autoencoders, SAE)详细理解并附实现代码。
人工智能·python·深度学习·学习·sae·autoencoder
小魏冬琅5 小时前
命题逻辑与谓词逻辑 - 离散数学系列(二)
学习
9毫米的幻想6 小时前
【C++】—— 继承(上)
c语言·开发语言·jvm·c++·学习
mljy.6 小时前
C++《string》
c++·学习
xs_20127 小时前
Python selenium库学习使用实操二
python·学习·selenium
xiandong208 小时前
240930_CycleGAN循环生成对抗网络
图像处理·人工智能·深度学习·神经网络·学习·生成对抗网络