常见的锁策略

目录

[一.乐观锁 vs 悲观锁](#一.乐观锁 vs 悲观锁)

[二.轻量级锁 vs 重量级锁](#二.轻量级锁 vs 重量级锁)

[三.自旋锁 vs 挂起等待锁](#三.自旋锁 vs 挂起等待锁)

[四.互斥锁 vs 读写锁](#四.互斥锁 vs 读写锁)

[五.可重入锁 vs不可重入锁](#五.可重入锁 vs不可重入锁)


一.乐观锁vs悲观锁

悲观锁 :
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:

假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

简单来说,就是锁的实现者,预测接下来锁冲突的概率是大还是不大.根据这个冲突的概率,决定接下来该怎么做.

  • 乐观锁:预测接下来冲突概率不大
  • 悲观锁:预测接下来冲突概率比较大

这样就导致最终要做的事是不一样的.

通常来说,悲观锁一般做的工作会更多一点,效率会更低一点,而乐观锁做的工作会更少一点,效率更高.但是这也并不绝对,要根据实际情况来判断使用.

Synchronized 初始使用乐观锁策略 . 当发现锁竞争比较频繁的时候 , 就会自动切换成悲观锁策略 .

乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 "版本号" 来解决.

我们来看下面这个例子:

二.轻量级锁 vs 重量级锁

  • 轻量级锁,加锁解锁过程更快更高效
  • 重量级锁,加锁解锁过程更慢更低效

这两种锁虽然和乐观悲观锁不是一回事,但是确实有一定的重合

  • 一个乐观锁很可能也是一个轻量级锁
  • 一个悲观锁也很可能是一个重量级锁

接下来我们从更深层来介绍轻量级锁和重量级锁

锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.

  1. CPU 提供了 "原子操作指令".
  2. 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
  3. JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.

即如下图所示:

注意, synchronized 并不仅仅是对 mutex 进行封装, 在 synchronized 内部还做了很多其他的
工作

重量级锁 : 加锁机制重度依赖了 OS 提供的 mutex

  • 大量的内核态用户态切换
  • 很容易引发线程的调度

这两个操作 , 成本比较高 . 一旦涉及到用户态和内核态的切换 , 就意味着 " 沧海桑田 ".
轻量级锁 : 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成 . 实在搞不定了 , 再使用 mutex.

  • 少量的内核态用户态切换.
  • 不太容易引发线程调度

**synchronized开始是一个轻量级锁.如果锁冲突比较严重,**就会变成重量级锁

三.自旋锁 vs 挂起等待锁

  • 自旋锁是轻量级锁的一种典型体现
  • 挂起等待锁是重量级锁的一种典型体现

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.

但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.

一旦锁被其他线程释放, 就能第一时间获取到锁

  • 自旋锁通常是用户态的,不需要经过内核态(时间相对更短)
  • 挂起等待锁通过内核的机制来实现挂起等待(时间相对更长)

那么针对上述三组策略,synchronized 这把锁属于哪种呢?

synchronized 既是悲观锁也是乐观锁,既是轻量级锁,也是重量级锁.

轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现

synchronized 会根据当前锁竞争的激烈程度,自适应

  • 如果锁冲突不激烈,以轻量级锁/乐观锁的状态运行
  • 如果锁冲突激烈,以重量级锁/悲观锁的状态运行

四.互斥锁 vs 读写锁

synchronized就是互斥锁,但是它的加锁就只是简单的加锁,就没有更细化的区分了

想synchronized就只有两个操作

  1. 进入代码块,加锁
  2. 出代码块,解锁

然而,除了这个之外,还有一种读写锁,能够把读和写两种加锁区分开.

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。

一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.

  • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
  • 两个线程都要写一个数据, 有线程安全问题.
  • 一个线程读另外一个线程写, 也有线程安全问题.

读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行 加锁解锁.
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进 行加锁解锁.

其中,

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

只要是涉及到 " 互斥 ", 就会产生线程的挂起等待 . 一旦线程挂起 , 再次被唤醒就不知道隔了多
久了 .
因此尽可能减少 " 互斥 " 的机会 , 就是提高效率的重要途径

读写锁特别适合于 "频繁读, 不频繁写" 的场景中.

Synchronized****不是读写锁

五.可重入锁 vs不可重入锁

如果一个锁,在一个线程中,连续对该锁加锁两次,不死锁的就叫做可重入锁,如果死锁了,就叫做不可重入锁
可重入锁的字面意思是 " 可以重新进入的锁 " ,即 允许同一个线程多次获取同一把锁

比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是 可重入 (因为这个原因可重入锁也叫做 递归锁
Java 里只要以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。
而 Linux 系统提供的 mutex 是不可重入锁 .

一个线程没有释放锁, 然后又尝试再次加锁.

java 复制代码
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待. 
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第

二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无

法进行解锁操作. 这时候就会 死锁, 这样的锁称为 不可重入锁.

相关推荐
小O_好好学6 分钟前
vi | vim基本使用
linux·编辑器·vim
农民小飞侠6 分钟前
python AutoGen接入开源模型xLAM-7b-fc-r,测试function calling的功能
开发语言·python
-SGlow-7 分钟前
Linux相关概念和重要知识点(4)(自举、vim)
linux·运维·vim
指尖流烟8 分钟前
C#调用图表的使用方法
开发语言·c#
敲代码不忘补水11 分钟前
Python 项目实践:简单的计算器
开发语言·python·json·项目实践
蒟蒻的贤20 分钟前
Web APIs 第二天
开发语言·前端·javascript
ljp_nan27 分钟前
QT --- 初识QT
开发语言·qt
多多*31 分钟前
OJ在线评测系统 登录页面开发 前端后端联调实现全栈开发
linux·服务器·前端·ubuntu·docker·前端框架
ᅠᅠᅠ@32 分钟前
异常枚举;
开发语言·javascript·ecmascript
hai4058733 分钟前
Spring Boot中的响应与分层解耦架构
spring boot·后端·架构