多线程---锁策略与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());
    }

在这里插入图片描述

  • 实现自旋锁

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. “银行家算法”

总结:

死锁的必要条件:

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

对应的解决:

  1. 锁的基本特性,解决不了。
  2. 锁的基本特性,解决不了。
  3. 在写代码时自己注意,不普适。
  4. 约定好加锁顺序,就可以避免循环等待。比如:给锁编号。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值