• 多线程---锁策略与CAS


    常见的锁策略

    乐观锁 VS 悲观锁

    乐观锁和悲观锁描述的是两种不同的加锁态度。

    乐观锁:预测锁冲突的概率不高,因此做的工作就可以简单一点。
    悲观锁:预测锁冲突的概率较高,因此做的工作就要复杂一点。

    比如:你明天就要去面试:
    乐观的做法:早睡早起,按时正常去参加面试流程。
    悲观的做法:晚上熬夜复习、订好回家的机票、第二天早上早早起来准备、提前到公司一段时间再参加面试。

    读写锁 VS 普通互斥锁

    普通互斥锁:就像synchronized,当两个线程竞争同一把锁,就会有人得阻塞等待。

    读写锁:分为加读锁和加写锁
    读锁和读锁之间,不会产生竞争。 多个线程同时读一个变量没事儿。
    写锁和写锁之间,会产生竞争。 就像普通互斥锁,有“锁竞争”,发生阻塞等待。
    读锁和写锁之间,也会产生竞争。 就像普通互斥锁,有“锁竞争”,发生阻塞等待。

    注:在实际环境中,读的频率要远远大于写的频率。加读写锁就会少很多的“锁竞争”,优化了执行效率

    重量级锁 VS 轻量级锁

    重量级锁:加锁和解锁开销比较大。 是典型的从用户态进入内核态的逻辑,开销较大。
    轻量级锁:加锁和解锁开销比较小。 是典型的纯用户态的逻辑,开销较小。

    注:

    1. 乐观锁和悲观锁是站在过程的角度考虑:看重加锁解锁过程中工作做的多少。
    2. 重量级锁和轻量级锁是站在结果的角度考虑:看重加锁解锁过程中消耗时间的多少。
    3. 通常情况下,乐观锁是轻量级锁,悲观锁是重量级锁。

    自旋锁 VS 挂起等待锁

    自旋锁:如果获取锁失败,立即再次尝试重新获取锁,无限循环,直到获取到了锁。

    优点:第一:不释放CPU资源;第二:如果其他线程释放了锁,这个线程就能马上获取到锁。
    缺点:如果其他线程持有锁的时间较长时,会造成CPU资源的浪费。

    挂起等待锁:如果获取锁失败,则进入阻塞等待状态,一段时间后再次尝试重新获取锁。

    优点:在阻塞等待阶段会释放CPU资源。
    缺点:不能及时获取到锁。

    公平锁 VS 非公平锁

    前提:三个线程请求获取锁的先后顺序:t1、t2、t3

    公平锁:遵守先来后到。t1先获取到锁、然后t2获取到锁、最后t3获取到锁
    非公平锁:随机调度。t1、t2、t3谁能先获取到锁是随机的

    可重入锁 VS 不可重入锁

    可重入锁:同一个线程针对同一把锁,连续加锁两次,不会死锁。
    不可重入锁:同一个线程针对同一把锁,连续加锁两次,会死锁。

    总结:
    对于synchronized

    1. 既是乐观锁也是悲观锁
    2. 既是轻量级锁也是重量级锁
    3. 乐观锁的部分是基于自旋锁实现的;悲观锁的部分是基于挂起等待锁实现的
    4. 是普通互斥锁不是读写锁
    5. 是非公平锁
    6. 是可重入锁

    注:
    synchronized是自适应的。初始使用的时候是乐观锁、轻量级锁、自旋锁;如果当前“锁竞争”不激烈,就保持最开始的状态不变。如果“锁竞争”激烈,就会自动升级为悲观锁、重量级锁、挂起等待锁。

    CAS

    什么是CAS?

    CAS(Compare And Swap):比较和交换。

    即:把内存中的某个值和CPU寄存器A中的值进行比较。如果两个值相同,就把寄存器B中的值和内存中的值进行交换。如果不同,就不做操作。

    优势:这个操作是通过一条指令来完成的。所以是线程安全的,也是高效率的。

    CAS的使用

    • 实现原子类
        //原子类  多用于计数
        //count.getAndIncrement   =   count++
        //count.incrementAndGer   =   ++count
        //count.getAndDecrement   =   count--
        //count.decrementAndGer   =   --count
        public static void main(String[] args) {
            AtomicInteger count = new AtomicInteger();
    
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    //相当与count++;
                    count.getAndIncrement();
                }
            });
    
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++){
                    //相当于count++;
                    count.getAndIncrement();
                }
            });
    
            thread.start();
            thread1.start();
    
            try {
                thread.join();
                thread1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            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

    在这里插入图片描述

    • 实现自旋锁

    CAS的ABA问题

    在CAS中进行比较的时候,当主内存和寄存器A中的值相同时,我们无法判断主内存中的值是一直没变,还是已经变了又变回来了。

    前提:用户的存款有1000元
    在这里插入图片描述

    解决办法:
    另外搞一个内存(寄存器C),记录内存中数据的变化。比如:

    1. 保存主内存的修改次数,只增不减
    2. 保存主内存的版本号, 只增不减
    3. 保存上次修改时间 只增不减

    在每次比较的时候,同时比较寄存器A和寄存器C中读到的数据和主内存中的数据是否一致。

    死锁

    死锁:一个线程在加上锁之后,就无法释放锁了。

    场景一:一个线程,一把锁,该线程连续加锁两次。
    解决:使用可重入锁,比如:synchronized

    场景二:两个线程,两把锁。
    解决:设计时考虑周到

        //死锁的案例
        public static void main(String[] args) {
            Object locker1 = new Object();
            Object locker2 = new Object();
    
            Thread thread1 = new Thread(() -> {
                synchronized (locker1){
                    System.out.println("在locker1里,获取对locker1的锁");
    
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    synchronized (locker2){
                        System.out.println("在locker1里,获取对locker2的锁");
                    }
                }
            });
    
            Thread thread2 = new Thread(() -> {
                synchronized (locker2){
                    System.out.println("在locker2里,获取对locker2的锁");
    
                   try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                   
                    synchronized (locker1){
                        System.out.println("在locker2里,获取对locker1的锁");
                    }
                }
            });
    
            thread1.start();
            thread2.start();
    
    
        }
    }
    
    
    • 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
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    场景三:多个线程,多把锁。“哲学家就餐问题”
    解决:1. 约定必须先拿哪一把锁再拿哪一把锁 2. “银行家算法”

    总结:

    死锁的必要条件:

    1. 互斥使用。锁A被线程1占用,线程2就用不了
    2. 不可抢占。锁A被线程1占用,线程2就不能把锁A抢过来,除非线程1主动释放
    3. 请求和保持。有多把锁,线程1拿到锁A后,不想释放锁A,还想再拿一个锁B
    4. 循环等待。线程1等待线程2释放锁,线程2等待线程1释放锁。

    对应的解决:

    1. 锁的基本特性,解决不了。
    2. 锁的基本特性,解决不了。
    3. 在写代码时自己注意,不普适。
    4. 约定好加锁顺序,就可以避免循环等待。比如:给锁编号。
  • 相关阅读:
    【漏洞复现】Apache_HTTP_2.4.50_路径穿越漏洞(CVE-2021-42013)
    进阶面试皆宜!阿里强推Java程序员进阶笔记,差距不止一点点
    Docker挂载镜像到本地(日常记录)
    5分钟制作可直接导入GPTs知识库中的自动爬虫
    JUC笔记(四) --- 内存共享模型
    stable diffusion , parameters in Outpainting
    Java_代码块/单例设计模式(饿汉式 / 懒汉式)
    常见的linux命令
    C#中数组、ArrayList与List对象的区别及使用场景
    计算机毕业设计Python+Django的银行取号排队系统(源码+系统+mysql数据库+Lw文档)
  • 原文地址:https://blog.csdn.net/weixin_62976968/article/details/134088963