• 多线程 - 锁策略 & CAS


    v2-4b5a5ad9fde3b92509faea52047f65bc_b

    常见的锁策略

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

    乐观锁 vs 悲观锁

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

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

    通常来说,悲观锁一般做的工作更多一些,效率更低一些,乐观锁做的工作会更少一些,效率更高一点.但是并不绝对.
    悲观锁和乐观锁,唯一的区别,主要就是看预测锁竞争激烈程度的结论.

    轻量级锁 vs 重量级锁

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

    一个乐观锁很可能也是一个轻量级锁,一个悲观锁很可能也是一个重量级锁.
    多数情况下,乐观锁,也是一个轻量级锁.
    多数情况下,悲观锁也是一个重量级锁.

    自旋锁 vs 挂起等待锁

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

    image-20231010185737617

    互斥锁 vs 读写锁

    互斥锁

    synchronized,是互斥锁
    synchronized 只有两个操作:
    1.进入代码块,加锁
    2.出了代码块,解锁
    加锁,就只是单纯的加锁,没有更细化的区分了

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

    读写锁:
    1.给读加锁
    2.给写加锁
    3.解锁
    注: 如果多个线程读同一个变量,不会涉及到线程安全问题!!!

    读写锁中,约定:
    1.读锁和读锁之间,不会锁竞争.不会产生阻塞等待,不会影响程序的速度,代码执行很快.
    2.写锁和写锁之间,有锁竞争,减慢速度,但是保证准确性.
    3.读锁和写锁之间,也有锁竞争,减慢速度,但是保证准确性.
    注:
    1.非必要不加锁.
    2.读写锁更适合于,一写多读的情况.
    3.多线程针对同一个变量并发读,这时是没有线程安全问题的,也就不需要加锁控制.
    4.很多开发场景中,读操作非常高频,比写操作的频率高很多.
    5.在Java标准库里面也提供了读写锁的具体实现(两个类,读锁类,写锁类).

    公平锁 vs 非公平锁

    此处把公平定义成“先来后到”
    image-20231010214216578

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

    image-20231010220437659

    非公平锁: 雨露均沾了.
    image-20231011011841261

    注: 操作系统和 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.返回操作是否成功

    image-20231011162715937

    此处最特别的地方,上述这个 CAS 的过程,并非是通过一段代码实现的,而是通过一条 CPU 指令完成的 => CAS 操作是原子的 ~~ 就可以在一定程度上回避线程安全问题
    因此解决线程安全问题除了加锁之外,又有了一个新的方向了.
    小结: CAS 可以理解成是 CPU 给我们提供的一个特殊指令,通过这个指令,就可以一定程度的处理线程安全问题.

    CAS 伪代码

    image-20231011164911903

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

    CAS 的应用场景

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

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

    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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    伪代码实现AtomicInteger

    image-20231011235536304

    2.实现自旋锁

    自旋锁伪代码

    image-20231012094749343

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

    CAS 的典型问题: ABA 问题

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

    这里一致,可能是没改过.也可能是,改过,但是还原回来了?!
    把 value 的值设为A的话, CAS 判定 value 为 A,此时可能确实value始终是A,也可能是value本来是A,被改成了B,又还原成了A …

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

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

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

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

    解决方案

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

  • 相关阅读:
    Rust中的 into和from如何使用?
    简单三招,就能将ppt翻译成英文,快来学习
    33.Python从入门到精通—Python3 正则表达式 re.match函数 re.search方法 re.match与re.search的区别
    图扑数字孪生空冷机组,助推智慧电厂拥抱“双碳”
    Kafka详解
    InnoDB引擎架构
    XPS表面及表面分析技术-科学指南针
    37-Spring
    【智能优化算法】基于改进生物地理学优化算法求解单目标优化问题附matlab代码
    objection 基础案例 一
  • 原文地址:https://blog.csdn.net/m0_73740682/article/details/133796997