多线程 - 锁策略 & 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 的时候不是以金额为基准了,而是以版本号为基准.此时,版本号要是没变,就是一定没有发生改变(版本号是只能增长,不能降低的).

相关推荐
栗子不爱栗子1 小时前
从理解AI到驾驭文字:一位技术爱好者的写作工具探索手记
python·学习·ai
sjg200104143 小时前
golang学习随便记x[2,3]-字符串处理与正则表达式
开发语言·学习·golang
fictionist3 小时前
动态 Web 开发技术入门篇
java·服务器·开发语言·笔记·学习·mysql·spring
恰薯条的屑海鸥4 小时前
零基础学前端-传统前端开发(第三期-CSS介绍与应用)
前端·css·学习·css3·前端开发·前端入门·前端教程
阑梦清川5 小时前
国防科技大学计算机基础慕课课堂学习笔记
笔记·学习·数学建模
水水沝淼㵘6 小时前
嵌入式开发学习日志(数据库II && 网页制作)Day38
服务器·c语言·网络·数据结构·数据库·学习
守护者1706 小时前
JAVA学习-练习试用Java实现“一个词频统计工具 :读取文本文件,统计并输出每个单词的频率”
java·学习
不太可爱的叶某人7 小时前
【学习笔记】深入理解Java虚拟机学习笔记——第3章 垃圾收集器与内存分配策略
java·笔记·学习
Chef_Chen7 小时前
从0开始学习R语言--Day21--Kruskal-Wallis检验与Friedman检验
学习
新中地GIS开发老师8 小时前
2025武汉考研形势分析,趋势、挑战与应对策略
学习·考研·arcgis·大学生·gis开发·webgis·地理信息科学