【多线程】(5)(常见的锁策略 CAS的应用场景 实现原子类 实现自旋锁 CAS 的 ABA 问题 Synchronized 原理 优化操作 锁升级/锁膨胀 锁消除 锁粗化)


常见的锁策略

乐观锁 vs 悲观锁

这并不是两把具体的锁,应该是"两类锁".这里的乐观锁就是预测锁竞争不是很激烈,悲观锁就是预测锁竞争会很激烈,两类锁出发点不同导致背后所做的工作是截然不同的.
悲观锁:每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

互斥锁 vs 读写锁

互斥锁类似于我们之前用过的像synchronize这样的锁.提供加锁和解锁两个操作,如果一个线程加锁了,另一个线程也尝试加锁,就会阻塞等待.
读写锁提供了三种操作:

  1. 针对读加锁;
  2. 针对写加锁;
  3. 解锁.

区分开可以让我们线程可以更好的去并发执行.

重量级锁 vs 轻量级锁

轻量级锁加锁解锁开销比较小,效率更高.重量级锁加锁解锁开销比较大,效率更低.多数情况下乐观锁也是一个轻量级锁.(不能完全保证).多数情况下,悲观锁也是一个重量级锁.(不能完全保证)

自旋锁(Spin Lock)

线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.但实际上,大部分情况,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.
自旋锁是一种典型的轻量级锁.挂起等待锁是一种典型的重量级锁.

自旋锁: 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.一旦锁被其他线程释放, 就能第一时间获取到锁

  1. 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
  2. 缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是
    不消耗 CPU 的).

公平锁 vs 非公平锁

假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待

公平锁:遵守"先来后到", B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.

非公平锁:不遵守 “先来后到”. B 和 C 都有可能获取到锁.
操作系统和Java synchronize原生都是"非公平锁",操作系统这里的针对加锁的控制本身就依赖于线程调度顺序,这个调度顺序是随机的,不会考虑到这个线程等待锁多久了.要想实现公平锁就得在这个基础上引入一些额外的东西.(引入一个队列,让这些加锁的线程去排队)

总结:上述谈到的 六种 所策略可以视为"锁的形容词"

  1. synchronize既是一个悲观锁,也是一个乐观锁.它默认是乐观锁,但是如果发现当前锁竞争比较激烈就会变成悲观锁 .
  2. synchronize既是一个轻量级锁,也是一个重量级锁.synchronize默认是一个轻量级锁,如果发现当前锁竞争比较激烈,就会转换成重量级锁.
  3. synchronize这里的轻量级锁是基于自旋锁的方式实现的,而重量级锁是基于挂起等待锁的方式实现的.
  4. synchronize不是读写锁.
  5. synchronize是非公平锁.
  6. synchronize是可重入锁.

可重入锁 vs 不可重入锁

不可重入锁:一个线程针对一把锁,连续加锁两次,出现死锁.
可重入锁:一个线程针对一把锁,连续加锁多次,不会出现死锁.

CAS

什么是CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“.

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

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

在这里插入图片描述
上述CAS的过程,并非是通过一段代码实现的,而是通过一条CPU指令完成的.也就是说CAS操作是原子的,就可以在一定程度上解决线程安全问题.

小结:CAS可以理解成CPU给咱提供的一个特殊指令,通过指令就可以一定程度的处理线程安全问题.

伪代码:

boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

CAS的应用场景

实现原子类

Java标准库里提供的类.
AtomicInteger count = new AtomicInteger(0);
在这里插入图片描述

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 < 50000; i++) {
                //因为java不支持运算符重载,所以只能使用普通方法来表示自增自减.
                count.getAndIncrement();//count++
                //count.incrementAndGet();//++count
                //count.getAndDecrement();//count--
                //count.decrementAndGet();//--count
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(count.get());
    }
}

在这里插入图片描述

实现自旋锁

基于 CAS 实现更灵活的锁, 获取到更多的控制权.

在这里插入图片描述

CAS 的 ABA 问题

CAS在运行中的核心,检查value和oldValue是否一致.如果一致,就视为value中途没有被修改过,所以进行下一步交换操作是没问题的.
上述的一致,可能是没改过,也可能是改过,但是还原回来了.这个过程就是ABA问题.ABA这个情况,大部分情况下,其实是不会对代码/逻辑产生太大影响的,但是不排除一些"极端情况",也是会造成影响的.

解决方案

针对上述问题的解决方法就是加入一个版本号.初始版本是1,每次修改,版本号都+1,然后进行CAS的时候,不是以金额为基准,而是以版本号为基准.版本号要是没变,就一定没有发生改变.(版本号是只能增长,不能降低的)

讲解CAS 机制

全称 Compare and swap, 即 “比较并交换”. 相当于通过一个原子的操作, 同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的支撑.

ABA问题怎么解决?

给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败.

Synchronized 原理

两个线程针对同一个对象加锁就会产生阻塞等待.
Synchronized 具有以下特性:

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略.
  4. 是一种不公平锁.
  5. 是一种可重入锁.
  6. 不是读写锁.
    synchronize内部有一些优化机制,可以让锁更高效,更好用.

优化操作

锁升级/锁膨胀

  1. 无锁
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

偏向锁

进行加锁的时候,首先会先进入到偏向锁状态.偏向锁并不是真正的加锁,而只是占个位置.有需要再真加锁.偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.

轻量级锁

当synchronize发生锁竞争的时候就会从偏向锁,升级成轻量级锁.此时,synchronize相当于是通过自旋的方式来进行加锁的.如果很快别人就释放锁了,自旋是划算的,但是如果迟迟拿不到锁,就一直自旋,并不划算.synchronize自旋不是无休止的自旋,自旋到一定程度之后,就会再次升级成重量级锁.(挂起等待锁)

重量级锁

重量级锁则是基于操作系统原生的API来进行加锁了.比如:Linux原生提供了mutex一组API.操作系统内核提供的加锁功能,这个锁会影响线程的调度.如果线程进行重量级锁的加锁,并且发生锁竞争,此时线程就会被放到阻塞队列中,暂时不参与CPU调度了,然后直到锁被释放了,这个线程才有机会被调度到,并且有机会获取到锁.(一旦当前线程被切换出cpu,这就是个比较低效的事情)

锁消除

编译器只能判定,看当前的代码是否真的要加锁,如果这个场景不需要加锁,程序员加了,就把锁给去掉.

比如:StringBuffer的关键方法中都带有synchronize,但是如果在单线程中使用StringBuffer, synchronize加了也白加,此时编译器就会直接把这些加锁操作给去掉.

锁粗化

锁的粒度:synchronize包含的代码越多粒度就越粗,包含的代码越少粒度就越细.

通常情况下,锁的粒度细一点比较好,加锁部分的代码是不能并发执行的,锁的粒度越细,能并发的代码就越多;反之就越少.但有些情况下,锁的粒度粗一些反而更好.

两次解锁之间间隙非常小的时候,倒不如一次大锁搞定就行了-----锁粒度的粗化
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

马尔科686

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

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

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

打赏作者

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

抵扣说明:

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

余额充值