JavaEE 第12节 常见锁策略

前言:

本节内容不局限于JavaEE,主要是介绍多线程中锁相关的一些专业术语以及概念。

1、乐观锁vs悲观锁

概念

这里所讲的乐观\悲观指的是线程去获取锁时, 出现锁竞争的概率小还是大

  • 乐观锁: 这种锁比较“乐观”,它默认锁竞争的概率比较小,所以在读取数据时不加锁,在更新变量的时候才上锁,并没有长期持有锁,提高了并发性,进而这种锁是资源开销相对小。
  • 悲观锁 这种锁“悲观的”认为锁竞争是比较激烈的,所以安全起见,它决定在去取数据的时候就上锁,因此锁的占用时间比较大,其他线程出现阻塞的概率就变大了,并发性就会降低,进而这种所资源开销比较大。

注意:  乐观锁和悲观锁各有优缺点,谁有谁劣不能一概而论,要根据场景分析。

应用场景:

  1. 乐观锁:适合在低冲突高并发的场景下使用。
  2. 悲观锁:适合在高冲突、数据严格要求一致的场景下使用。

2、轻量级锁vs重量级锁

这一组概念与上面的悲观锁&乐观锁是比较重合的。你可以近似的认为:轻量级锁就是乐观锁,重量级锁就是悲观锁,只是这两组概念描述锁性质的角度不同而已。

锁是“原子性”的原因:
锁的核心特质是“原子性”,不同的编程语言之所以都能通过锁,实现原子性的原因归根结底来自于CPU这种硬件设备的支持:

  1. CPU提供的“原子性”指令。
  2. 操作系统基于这些指令,实现了mutex1互斥锁。
  3. JVM基于操作系统提供的互斥锁,实现了synchronized和ReentrantLock等关键字和类2

在这里插入图片描述

重量级锁: 较为悲观,所以未雨绸缪,做的工作多。

  • 大量的内核态用户态切换,重度依赖刚才所讲的mutex
  • 它容易引发线程调度

以上两种操作,成本是比较高的,它让线程更安全,但效率也降低了。

轻量级锁: 较为乐观,所以活在当下,做的工作比较少。

  • 少量的用户态内核态切换,能不用mutex,就不用。
  • 不太容易引发线程调度。

如果不理解这两个操作也没关系,直到重量级做的工作多,开销大,轻量级做的工作少,开销小即可。

3、自旋锁vs挂起等待锁

自旋锁&挂起等待锁,实际上就是乐观锁(轻量级锁)&悲观锁(重量级锁)的典型实现。

自旋锁:

  • 定义: 一个线程尝试获取自旋锁,如果锁被持有,那么这个线程不会进入睡眠状态,而是一直循环检查所是否被释放,直到锁被释放,这种“循环检查”的锁,称之为自旋锁。
  • 特点: 忙等 ,一个线程在循环等待锁释放的过程中,此线程不会释放CPU资源,即使锁被持有的时间会很长,这种一致检查锁是否被释放,不计资源开销的行为就叫忙等。
  • 优点:
    • 低延时: 在每个线程持有锁的时间都很短的情况下,线程的响应速度是非常快的。
    • 实现简单: 自旋锁实现通常比较简单,比较轻量,只需要进行原子操作。
  • 缺点: 在锁竞争比较激烈的情况下或者锁持有时间比较长的情况下,自旋锁会本身会占用大量的CPU资源,降低运行效率。

挂起等待锁:

  • 定义: 一个线程在尝试获取一个被持有的锁时,不会忙等,而是被挂起然后进入睡眠状态,直到锁被释放,这种机制通常依赖操作系统的支持。
  • 优点: 当无法获取锁时,线程会被挂起等待,不在消耗CPU资源,把CPU资源腾给别的线程用,高效利用了CPU资源
  • 缺点: 实现锁本身就比较复杂,并且涉及上下文切换3,需要内核态支持,较为重量,运行效率不高。

总结:

自旋锁和挂起等待锁各有专长,根据实际情况使用。如果锁持有时间短,低锁竞争,采用自旋锁设计。如果持有锁时间长,高锁竞争,同步性要求高,采用挂起等待锁的设计。

4、公平锁

公平锁是一种锁机制,这里讲的公平是先到先得
主要特点:

  • 先来先服务: 锁按照请求的先后顺序分配;
  • 防止饥饿: 每个线程都有机会拿到锁,一般不会等待太长时间;
  • 实现复杂: 需要引入额外的数据结构(如队列)来确保公平性;

应用场景:
需要严格要求顺序访问资源的多线程环境,避免某些线程没有机会或者长时间拿不到锁。

5、读写锁

概念:

  • 给读加锁(共享锁): 共享锁是“没有互斥性”的,这意味着多个线程之间可以共同读取共享的数据,而不会相互阻塞。
  • 给写加锁(独占锁): 这和一般的锁性质一样具有互斥性,多线程中只要独占锁参与,那么就会涉及锁竞争。

注意事项:

  • 读加锁和读加锁之间,不互斥。
  • 读加锁和写加锁之间,互斥。
  • 写加锁和写加锁之间,互斥。

应用场景:
读锁的开销比写锁的开销小很多,非常适用于读操作频率远大于写操作的频率的情景。这样可以支持高并发(读是共享的),提升程序效率。

6、锁粗化

锁粗化是编译器的一种优化手段。粗细的评判标准在于锁的粒度,粒度粗就是粗化,粒度细,就是细化。
粒度粗细的评判标准:在这里插入图片描述
锁粗化:粗化的概念
锁粗化的优点:

  • 减少锁开销: 每次加锁解锁有一点过开销,锁粗化后,加锁次数减少;
  • 减少锁竞争: 使用锁的频率降低,所冲突减少;
  • 提高代码可读性: 在书写代码的时候,尽量增加锁粒度,减少加锁次数,可以降低代码复杂度,提升可读性,利于代码维护;

7、可重入

在逻辑上讲,锁是不可重入的,不然会造成死锁4

不过在有些编程语言中,会对加锁操作进行优化,可以避免重复锁造成的死锁的情况发生,程序员不用关心是否有重复上锁造成死锁的威胁。
比如说Java中的synchronized关键字。但是想C++/Python这写编程语言就不支持可重入。

8、锁消除

与锁粗化一样,锁消除也是编译器的一种优化手段
如下代码,在单线程环境下,执行这个含有synchronized关键字的代码,如果没有编译器优化,执行效率会很低:

public class Test1 {
    public static void main(String[] args) {
        StringBuffer sb=new StringBuffer();
        sb.append('a');
        sb.append('b');
        sb.append('c');
        sb.append('d');
    }
}

像这种没有必要加锁的地方,编译器会消除这个锁,避免不必要的资源开销。


  1. mutex可以理解为操作系统为其他程序提供锁机制的一组API。 ↩︎

  2. synchronized的实现不止是依靠互斥锁实现的,它实际上还做了很多工作,关于synchronized的一些性质特点请参阅JavaEE 第13节。 ↩︎

  3. 简要的理解就是线程调度的过程。这里的上下文指的是某一个线程/进程在某一个时间点的状态,以及相关信息。要切换线程/进程调度,首先需要保存当前上下文,然后加载下一个上下文,最后才切换到另一个线程/进程。上下文切换的开销是比较大的。 ↩︎

  4. 重复对一个地方加锁,产生死锁的原因,具体请看JavaEE 第4节中,“synchronized的可重入(Reentrant)”,里面有详细的图文解释。 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值