多线程(进阶)——常见锁策略+CAS+Synchronized优化过程

🥝常见锁策略

锁策略和我们这样普通的程序猿基本无缘,和实现锁的人才才有关系.
这里提到的锁策略,和我们学的Java本身没什么关系,适用于所有和锁相关的情况

🥝悲观锁和乐观锁

悲观锁:预期锁冲突的概率很高;
乐观锁:预期锁冲突的概率很低;
锁冲突即是锁竞争,多个线程对一把锁的竞争

我们举个🌰 :现在疫情很严重,高风险地区的人们面临着买菜难的问题,终于在一段时间的努力下,我们战胜了疫情,但是为了防止下一波疫情的到来,有的人吸取了上次疫情的教训,
开始大量的囤货,他需要买一个很大的冰箱来存储食物->去超市排队买菜->买米面油->买生活必需用品…这就是悲观锁的表现
但是有的人认为 大家有了上次疫情的经验,就算下一波疫情到来,大家也能买到菜,便不做过多的准备,也不再多过多的工作,这就是乐观锁的表现
我们可以看出——悲观锁做的多,付出的成本多,但是比较低效.乐观锁做的少,付出的成本少,更高效
悲观锁和乐观锁的概念是我们在处理锁冲突时的态度,下面我们介绍一下处理锁冲突产生的结果的锁策略

🥝 重量级锁和轻量级锁

重量级锁:表示做了很多的事,开销比较大;
轻量级锁:表示做了很少的事,开销比较小;

我们会发现重量级锁和轻量级锁与悲观锁乐观锁似乎有着某种联系,没错
通常我们认为悲观锁是重量级锁,乐观锁是轻量级锁,但这不是绝对的

在使用的锁中,如果锁是基于内核的功能来实现的(比如调用了操作系统提供的mutex接口),此时一般认为这是重量级锁(操作系统中的锁会在内核中做很多的事情,比如让线程堵塞等待)
如果锁是纯用户态实现的,此时一般认为这是轻量级锁(用户态的代码更可控也更高效)

🥝挂起等待锁和自旋锁

挂起等待锁:往往是通过内核的一些机制来实现的,往往较重[重量级锁就是典型实现案列]
自旋锁:往往是通过用户态代码来实现的,往往较轻[轻量级锁就是典型实现案例]

介绍完上面三种对立锁,我们发现,似乎说的是一种东西啊?
确实如此!!!只不过,是逐步进行深入了解 把抽象的概念变得越来越具体,但是这三种对立锁并不是同一种概念,这不是绝对的,只能说他们之间有着千丝万缕的关系.

🥝读写锁和普通的互斥锁

互斥锁:普通的互斥锁只有两个操作,加锁,解锁,只要两个线程对同一个对象加锁就会产生互斥
读写锁:加读锁(如果代码只是进行读操作,就加读锁),加写锁(如果代码只是进行了修改操作,就加写锁),解锁
两个读操作不会产生互斥,只有写操作才会产生互斥,多个线程同时读一个变量,不会产生线程安全问题

所有的加锁操作只发生在多线程中,没有多线程的地方谈不上加锁,比如,我们会发现JDK的源码无法进行修改,可能有的同学就会认为这是写加锁的操作,这是不正确的,这是权限的问题,我们需要注意一下

🥝公平锁和非公平锁

所谓公平,就是遵守规定,不是绝对的,此处我们遵守的是先来后到的规则,重要遵守了这样的规则,我们就认为这是公平锁(先来的线程优先获得锁)
但是如果我们不遵守先来后到的规则,认为线程获取锁的概率是相等的,我们就认为这是非公平锁(每个线程获取锁的概率是相同的)

公平的实现是很困难的,需要付出很大的代价,需要把这些线程重排序

🥝可重入锁和不可重入锁

一个线程针对一个对象加了多个锁产生了死锁,这叫做不可重入锁,如果不产生死锁就叫做可重入锁(Synchronized就是典型案列)

上述这些锁策略就是我们在开发中常见的锁策略,我们需要了解,避免面试官问这些的时候太懵逼

🥝 Synchronized与锁策略之间的联系

synchronized是我们之前介绍的重点知识,他也可以对应我们所介绍的这些锁策略

  • 即使一个悲观锁也是一个乐观锁(根据锁的竞争程度,来适应)
  • 即是一个轻量级锁也是一个重量级锁(根据锁的竞争程度,来适应)
  • 轻量级锁的部分基于自旋锁来实现,重量级锁的部分基于挂起等待锁来实现
  • 不是读写锁,只是一个普通的互斥锁
  • 非公平锁
  • 可重入锁

🍉CAS

CAS:Compare.And.Swap

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

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V。(交换)
  3. 返回操作是否成功。

这是CAS的具体描述,我知道,你现在肯定没理解,因为我也看不懂
我们结合伪代码来分析

请添加图片描述

adress:待比较的内存地址
expectValue:预期内存中的值
swapValue:希望内存交换之后的值
如果待比较的值和我们预期内存的值相等,我们就将内存中的值与swapValue交换,并返回true,失败就返回false

上面的伪代码很显然是线程不安全的,因为里面又有读操作又有写操作,而且读与写不是原子性的,但是CAS的设定能帮我们解决这个问题

CAS是CPU提供的一个单独的指令,通过这一条指令就完成了这些操作,这样就保证了上述伪代码的功能,在此之前入过我们想要保证原子性就需要使用synchronized关键字,但是在这,我们就不需要这个操作了,CAS就帮我们解决了这个棘手的问题

这也是CAS最大的意义,就是让我们线程安全的代码时,能有一个新思路和方向(就不需要锁的使用了)
这样,我们不难看出,其实系统的很多功能,软件能实现,硬件也能实现,就例如我们的CAS,通过CPU上的一个CAS指令,就能解决代码的原子性问题

🍉CAS的作用

  1. 基于CAS能够实现原子类
    Java标准库中提供了一组原子类,针对一些常用的int,long,int array进行了一些封装,根据CAS的方法进行修改,这样线程就变的安全了,代码如下
public class CAS {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger num = new AtomicInteger(0);
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                num.getAndIncrement();
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                num.getAndIncrement();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(num);
    }
}

这样代码就不存在线程不安全的操作了基于CAS实现的++操作既能保证线程更安全,也能保证比synchronzide更高效,CAS不会产生锁的竞争

下面我们来看一下AtomicInteger类伪代码的实现请添加图片描述
下面我们来对这个伪代码进行解析

value:代表的是存储的原始数据
oldValue:此处并不是代表变量的意思,他代表的是寄存器中的值,就是把数据读到寄存器中,伪代码不好表示寄存器
然后进行CAS操作判断value和oldValue的值是否相等,在做相应操作,写到这里可能有小伙伴会问难道value和oldValue的值会不相等吗?此时我们需要知道如果代码是单线程的他俩的值始终会是相等的,但是如果是多线程的,可就不一定了,可能第一个线程正在运行,如果线程因为某种原因休息了一下,此时第二个线程对变量的值进行了修改,这岂不是很糟糕,变量的值就会发生改变.一般情况来说,这就会产生线程不安全,但是如果在CAS上,就会避免这种情况发生.我们作图来解析

请添加图片描述

请添加图片描述
请添加图片描述
请添加图片描述

通过上面的操作,就可以不使用重量级锁,就能达到多线程安全的效果

  1. 实现自旋锁

自旋锁上面我么提到过,是我们用户态用代码实现的锁,我们基于CAS也能够实现自旋锁

伪代码如下

请添加图片描述

我们在对这个伪代码进行分析

Thread owner 表示当前的锁被哪个线程拥有了,初始值为null,代表当前未加锁.
此处和原子类差不多,也是用一个while循环来进行CAS操作,如果当前锁没被线程占有,就把null改成当前的线程,此时这个锁就被这个线程占有了,如果不是null就代表此时的锁已经被其他的线程锁拥有,则返回false,就会进入循环,产生自旋(忙等),直到拿到线程

前面,我们在定时器的章节中接到了忙等的概念,当时我们说,这种忙等是很蛋疼的,毫无意义,所以我们进行了wait方法来解决忙等的问题,但是在CAS里,此时的忙等是很有必要的,正因为有这种忙等我们才有了这个自旋锁的概念
自旋锁我们认为它是一种轻量级锁,是乐观锁,当前这把锁虽然没能立即拿到,但是预期很快就能拿到,短暂的自旋几次,浪费点CPU问题并不是很大,好处就是这边一释放锁就能够立即拿到这个锁

🍉CAS的ABA问题

CAS虽然能帮我们解决变量自增保持代码的原子性的问题,但是他也有缺陷,毕竞世上没有十全十美的存在,这个缺陷就是CAS的ABA问题

ABA:CAS的关键就是先比较,在交换,比较的就是当前的值和旧值是否相等,正常情况下相等就是相等,返回true,不相等就返回false,但是在极端情况下,我们无法判断当前的值是否发生了变化,也就是说当前的值在一段时间内变为了另外一个值,但是过了一段时间后又变回来了,这就是ABA问题的引出
我们用🌰 来具体的分析一下

ABA 的问题: 假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A. 接下来, 线程 t1 想使用 CAS 把num 值改成 Z, 那么就需要 先读取 num 的值, 记录到 oldNum 变量中. 使用 CAS 判定当前 num 的值是否为 A请添加图片描述
如果为 A, 就修改成 Z. 但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了A 线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过又改成 A 了. 这 个时候 t1究竟是否要更新num的值为Z呢?

🍉ABA产生的BUG

我们也举一个具体的 🌰 来分析:
1.
请添加图片描述

2请添加图片描述
3. 请添加图片描述
根据例子我们发现,🤪滑稽老哥损失了五十块钱,这会让他认为他的朋友没有给他转账,但是他的朋友的的确确的给他转了五十,俩人就会因此产生没必要的误会,但事实上俩人都没有错,这就产生了BUG.

🍉解决ABA问题

CAS的主要过程就是比较和交换,我们可以在引进一个自增的变量,或者指向一个方向变化的变量,例如版本号或者时间戳,来参与CAS的比较,这样,我们比较的不是变量本身了,而是版本号了

🌰

请添加图片描述
请添加图片描述

只要我们我们原始数据进行了修改操作,那么版本号就会改变,这就避免了ABA问题的产生

🍓Synchronized优化过程

synchronized锁是经过优化的,下面我们介绍几个典型的优化手段

🍓锁膨胀/锁升级

JVM会把synchronized锁分为无锁,偏向锁,轻量级锁,重量级锁

请添加图片描述

接下来我们对这几个状态进行分析

  1. 在使用synchronized之前对象是没有锁的
  2. 加上synchronized之后对象是一种偏向锁,我们对这个偏向锁进行深度分析.偏向锁是我们经典的面试题目,他并不是一种锁,而是对锁对象进行了标记,我们举个🌰 假设有一个美女,他找到了一个高富帅,但是没有立即确定男女关系,只是做了一些男女朋友才会做的事,如看电影,煲电话粥.如果不久后,这个美女看上了另一个高富帅,就可以和当前的男朋友分道扬镳,不至于有太多的麻烦,这个时候是没有人和她竞争的,就避免了不必要的开销,但是如果当前的这个高富帅有其他的女生追了,并且可能会威胁到这个美女的地位,那么此时这个美女就会立即和男生确立关系,并宣誓主权,此时这个偏向锁就升级为乐轻量级锁
    总的来说偏向锁如果没有其他的锁和他竞争那么此时只是在这个锁对象做了个标记,一但有其他的锁和他竞争,就会升级为轻量级锁,这样做会避免不必要的开销
  3. 自旋锁:.这就是偏向锁升级成的轻量级锁
  4. 重量级锁,如果锁的竞争激烈,就会演变成重量级锁、

🍓 锁粗化

锁可以分为粗化和细化,这里的粗细指的是锁的粒度,代码涉及到的锁的范围范围大,锁的粒度大,锁就越粗,锁的范围小,锁就越细,锁的范围小

我们无法判断锁是粗好,还是细好,他们带来的效果也不一样

锁细,并发行高,但是开销大
锁粗,开销小,并发行低

请添加图片描述

在这里,编译器会进行优化,他会判断没如果锁太细,编译器就会进行粗化
但是代码锁的范围太大,也就是锁粗编译器不会进行优化,只有锁太细了,编译器才会进行优化

请添加图片描述

🍓锁消除

这个比较好理解,就是代码是单线程的,明明不用加锁,程序猿却加上了锁,这是没有必要的,那么编译器就会把这个锁消除

🌰 :
请添加图片描述

以上就是多线程进阶的部分知识

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值