锁策略 和 CAS 和 synchronized 的自适应过程 学习总结


这几天学习了关于锁策略和CAS的知识, 于是乎有了此文作笔记

锁策略

对于多线程中的锁有很多有意思的思想, 这些思想应用在不同的地方可以减少锁竞争, 提高程序运行效率

乐观锁和悲观锁

  1. 乐观锁就是预测接下来发生锁冲突的概率不大, 觉得每一次操作数据的时候都觉得不会发生锁冲突, 就不会加锁。只是当更新数据的时候会检查一下数据有没有更新过, 如果更新过, 就会重新读取一下数据, 再检查是否更改, 并尝试更新。乐观锁很适合应用在读取次数多, 修改次数少的场景中
  2. 悲观锁就是预测接下来发生锁冲突的概率很大, 认为每一次操作数据都会发生锁冲突, 就会一直加锁。如果当前线程加上了锁, 其他线程就会一直阻塞。
  3. synchronized 是一种自适应锁, 在锁冲突概率小的时候, 就会是乐观锁, 具体表现为偏向锁(下面会说), 当锁冲突大的时候, 就会是悲观锁

读写锁

加锁的操作更加细致化, 可以加的锁有两种 : 读锁 和 写锁

  1. 如果多个线程都只进行读取操作,就可以共享读取这一操作, 多个线程一起共同读取数据, 相当于没有锁
  2. 如果线程要进行写入数据操作, 就会进行加锁, 别的线程无法进入, 也就无法进行读取操作和写操作
  3. 如果频繁读取数据, 该锁就可以避免很多不必要的开销, 提升效率

重量级锁和轻量级锁

重量级锁

  1. 重量级锁开销更大, 那么重量级锁开销大体现在哪里嘞??

  2. 我们先简单说一下内核态和用户态是什么:
    操作系统启动时会对内存进行分区,操作系统的数据都是存放于内核空间,用户进程的数据是存放于用户空间的。处于用户态级别的程序只能访问用户空间,而处于内核态级别的程序可以访问用户空间和内核空间。
    当一个进程执行系统调用而进入内核代码中执行时,我们就称进程处于内核态

  3. 然而, 重量级锁在底层是在操作系统中需要依靠其中的锁实现的, 所以在重量级锁的状态下, 线程反复阻塞或者被唤醒, 就会导致操作系统在内核态和用户态的频繁切换, 获取锁and释放锁, 造成很大的资源开销

  4. 重量级锁如果没有得到锁, 就会进入阻塞状态;锁被释放后, 也无法马上获取锁挂起等待锁是重量级锁的一个具体实现, 但是好处被阻塞的线程不会消耗CPU

轻量级锁

  1. 相反于重量级锁, 它锁开销更小, 会避免使用操作系统内的锁, 尽量在用户态就能正常工作
  2. 自旋锁是轻量级锁的具体实现方式, 发生锁冲突的时候, 会马上再试一下能不能获得锁, 努力能在其他线程释放锁后能马上得到锁, 当然, 这样的弊端就是会导致 CPU 循环判断锁状态, 持续消耗 CPU 资源

公平锁和非公平锁

  1. 公平锁
    各个线程会按照申请锁的顺序进入队列, 按照"先到先得"的原则, 最先申请的会更快获取到锁。这么做的好处是所有线程都有机会得到锁
  2. 非公平锁
    线程会先尝试去获取锁, 如果能获取到, 那就获取到了锁;如果获取不到, 就会进入等待的队列, 相反, 这样的弊端是部分线程会长时间拿不到锁

可重入锁和不可重入锁

  1. 可重入锁
    即某个线程已经获得了锁, 可以再次获取本对象的锁而不出现死锁, 即可重入锁, 比如这种情况(有点像双层锁)
public static Object lock = new Object();
    
public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("第一层锁");
                synchronized (lock) {
                    System.out.println("第二层锁");
                }
            };
        }).start();
    }
在第一层 lock 锁获取而未释放的情况下, 再次获取该锁

在这里插入图片描述

  1. 不可重入锁
    这个就和上面相反, 当线程获取锁后, 必须要等这个线程的锁释放后, 才能获取其他锁

CAS

正文

CAS 即 compare and swap, 是操作系统提供的一种原子操作的机制

CAS 涉及到三个变量 : A. 内存地址, B.寄存器中用来比较值, C.用来交换的值

如果 内存中的值用于比较的值 相等, 那就将 交换的值 赋给 内存地址对应的值

这里用代码语言翻译一下应该比较清晰, 但是实际上并非如此实现! 仅供理解

            内存地址      用来比较的值      用来交换的值
boolean CAS(address,     compareValue,    swapValue) {
    if (&address == compareValue) {
    	&address = swapValue;
    	return true;
    }
    return false;
}

如果成功修改, 返回 true, 否则返回 false

AtomicInteger 类就应用了 CAS 机制, 其中的 getAndIncrement() 就是原子性的自增操作, 应用 CAS 后, 我们不加锁, 也可以实现两个线程各自对一个变量自增1000次 ↓

public static void main(String[] args) throws InterruptedException {
		// 创建 AtomicInteger 对象, 并赋值为 0
        AtomicInteger cas = new AtomicInteger(0);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i ++) {
                cas.getAndIncrement();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                cas.getAndIncrement();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        // 打印 cas 值
        System.out.println(cas.get());
    }

运行结果如下
在这里插入图片描述

总结

  1. 合理使用 CAS 能够减少上锁的次数, 也就减少了线程唤醒和线程阻塞之间的转换, 能够提高效率
  2. CAS 同时还有一个弊端, ABA问题

ABA

我们举个例子, 假设目前有三个线程 ABC, 对同一内存地址进行访问并更改

突然有两个线程AB进行一样的操作, 都读取内存值100, 想更新为 50, 但是线程 B 突然阻塞了, 而 A 顺利进行, 将内存中的100, 更新为了 50
在这里插入图片描述

线程A更改后,B还在阻塞, 线程C这时候进来读取内存值为50 , 线程C又是顺利改为100

在这里插入图片描述

这时候线程B回来了, 原来想要读取内存值为100, 如果是100, 那就更新。但是现在的内存值仍是100, 而线程 B 并不知道这个100不是原来那个100了, 已经是经过一两轮变化的了! 就继续更改为50
在这里插入图片描述

这种情况看起来可能暂时没什么问题, 但是如果这种场景发生在银行, 那就不是个小问题了, 例如

还是上面的情形,
由于某种原因, 飞飞到银行里取钱, 余额 100, 想拿 50, 但是飞飞不小心多按了一次, 导致同时有两个线程同时执行这个操作。
还是一样, 线程 B 突然给系统整阻塞了, 线程 A 顺利执行, 飞飞拿出了50块钱, 余额剩50
在这里插入图片描述

B还在阻塞的时候, 这时候你的好兄弟突然就给你转了 50, 即线程 C 读取到余额50, 并要更新到 100

在这里插入图片描述

然后线程B回来了, 线程B一开始的操作就是→读取余额100, 更改为50。所以现在还是没变化, 继续读取余额100, 然后更改为50
在这里插入图片描述

诶, 问题就来了, 原来有100, 飞飞拿了50, 好兄弟转了50, 余额应该是 100 才对, 现在多扣了50, 这就很尴尬了

为了解决这种问题, CAS 引入了版本号, 并规定版本号只增不减, 并且每次在比较内存值的时候, 都需要比较版本号是否一致, 这个版本号也就一定程度反映了数据的变化过程

就像上面那个问题, 当线程B阻塞的时候, 其他线程CAS完后, 版本号都会自增, 当线程B回来更新数据时候, 就会发现版本号不一样而无法更新。

synchronized 自适应过程

然后我们再来谈谈 synchronized 加锁的时候经历的几个阶段

  1. 无锁 : 当线程创建后, 还没有线程获取锁, 就会处于无锁状态
  2. 偏向锁 : 当只有一个线程参与锁的获取与释放的时候, 就处于偏向锁状态。获取偏向锁后, 执行完同步代码块的时候也不会主动释放锁, 而是等到下一次进入同步代码块的时候判断一下自己有没有偏向锁, 如果还是同一个线程, 那由于之前没有释放锁, 现在也不用再获取锁, 只有当出现锁竞争的时候, 就会撤销偏向锁, 膨胀为轻量级锁, 这样就能大量减少获取锁的消耗。
  3. 轻量级锁 : 开始发生锁竞争的时候, 开始转向轻量级锁, 会自旋尝试获取锁
  4. 重量级锁: 当够多的线程竞争锁, 或者自旋次数过多又没获取到锁的时候, 进入重量级锁, 没获取到锁的线程阻塞, 防止 CPU 空转
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

答辣喇叭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值