⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/qq_43575801/article/details/127760429 「Killing Vibe」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

所谓锁的策略就是指如何实现锁。Java、MySQL、Go、C++等等都有类似的锁策略。

一、乐观锁和悲观锁

这两种锁都有相应的应用场景。

1.1 定义

乐观锁:

每次读写数据都认为不会发生冲突,线程不会阻塞,一般来说,只有在进行数据更新时才会检查是否发生冲突,若没有冲突,直接更新,只有冲突(多个线程都在更新数据)了才解决冲突问题。

当线程冲突不严重的时候,可以采用乐观锁策略来避免多次的加锁解锁操作。

悲观锁:

每次去读写数据都会冲突,每次在进行数据读写时都会上锁(互斥),保证同一时间段只有一个线程在读写数据。

当线程冲突严重时,就需要加锁,来避免线程频繁访问共享数据失效带来的CPU空转问题。

1.2 生动有趣滴例子

举个栗子:

悲观锁策略:

每次你(线程)跑来找我(线程或者资源)都认为我忙着呢,先给我发个消息 “嗨嗨嗨,VIBE在吗?”(尝试加锁),我没回或者回了个”忙着呢“,你就得等待(线程阻塞),一直等到我回复你”我好了“ (CPU唤醒了等待线程,尝试重新加锁),此时你被唤醒,对我加锁,我们就可以愉快聊天了~

乐观锁策略:

你认为每次找我的时候,我都闲着呢,直接就找我发消息要请我吃火锅(不上锁,直接访问数据),若我确实闲着呢(直接响应,避免了加锁和解锁的操作),如果我此时忙着呢,我就一直不回复,你一看我没及时回复你,你就跑去干别的事情了(线程不会阻塞,去干别的事情),过段时间再来。

乐观锁不是真的把线程阻塞了。乐观锁的实现一般都会采用版本号机制来实现~

1.3 版本号机制

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

  • 一般锁的实现都是乐观锁和悲观锁并用的策略。
  • synchronized最开始就是乐观锁,当竞争激烈再升级为悲观锁。

下面博主将画图详细讲解版本号机制:

(1) 线程1和2从主内存读取到数据到自己的工作内存中,此时版本号都是 ”1“。

(2)线程1把自己的V值改成30,线程2把自己的V值改成70

(3)假如线程1先完成修改,将数据版本号+1(version = 2),然后一起写回主内存

(4)此时线程2想更新自己的工作内存值到主内存,发现不满足”提交版本必须大于记录当前版本才能执行更新“的乐观锁策略,就认为这次写回失败。

(5) 线程2写入失败,就从主存中读取最新的值和版本号到自己工作内存中,然后尝试在最新的数据上进行操作,若最后写回成功,主存和工作内存的值+1,否则执行CAS策略,不断重试写回,直到成功为止。

二、读写锁

2.1 读写锁的由来

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

读写锁特别适用于线程基本都在读数据,很少有写数据的情况。

多线程访问数据时,并发读取数据不会有线程安全问题,只有在更新数据(增删改)时会有线程安全问题,将锁分为读锁和写锁。

  • 多个线程并发访问读锁(读数据),则多个线程都能访问到数据,读锁和读锁是并发的,不互斥
  • 两个线程都需要访问写锁(写数据),则这两个线程互斥,只有一个线程能成功获取到写锁,其他线程阻塞
  • 当一个线程读,另一个线程写(也互斥,只有当写线程结束时,读线程才能继续执行)

注意, 只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了.

因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径

2.2 生动有趣de例子

举个栗子:

比如大家都看过的网文,作者在码字的时候,所有读者都得等作者写完,才能读。

2.3 ReentrantReadWriteLock 类

synchronized不是读写锁,JDK内置了另一个ReentrantReadWriteLock实现读写锁

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

三、重量级锁与轻量级锁

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

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

3.1 定义

重量级锁:

需要操作系统和硬件支持,线程获取重量级锁失败进入阻塞状态(os,用户态切换到内核态,开销非常大)

轻量级锁:

尽量在用户态执行操作,线程不阻塞,不会进行状态切换。

3.2 生动活泼の例子

举个栗子:

假如此时要去银行办理业务,窗口外部自己处理的业务就属于用户态,窗口内部需要工作人员协助的就处于内核态

重量级锁: 若某个业务涉及到赚钱打款,就要频繁切换用户态和内核态,非常耗时。

轻量级锁: 此时可以把这些操作业务都放在用户态解决

3.3 自旋锁(Spin Lock)

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

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

轻量级锁的常用实现就是采用自旋锁

自旋锁就是循环,以下是伪代码:

while (获取(lock) == false) {//循环}

线程获取锁失败并不会让出CPU,线程也不阻塞,不会从用户态切换到内核态,线程在CPU上空跑,当锁被释放,此时这个线程很快就会获取到锁。

举个栗子:

比如等红绿灯:

  • 如果每次等都熄火,当绿灯再打火启动,这就是挂起等待锁
  • 如果每次发动机不熄火,踩着刹车,等绿灯亮了可以直接走,这就是自旋锁

四、公平锁与非公平锁

公平锁:

获取锁失败的线程进入阻塞队列,当锁被释放,第一个进入队列的线程首先获取到锁(等待时间最长的线程获取到锁)

非公平锁:

获取锁失败的线程进入阻塞队列,当锁被释放,所有在队列中的线程都有机会获取到锁,获取到锁的线程不一定就是等待时间最长的线程

  • synchronized锁就是非公平锁
  • ReentrantLock默认是非公平锁,可以在构造方法中传入true开启公平锁

五、可重入锁和不可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。

举个栗子:

比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

而 Linux 系统提供的 mutex 是不可重入锁.

文章目录
  1. 1. 一、乐观锁和悲观锁
    1. 1.1. 1.1 定义
    2. 1.2. 1.2 生动有趣滴例子
    3. 1.3. 1.3 版本号机制
  2. 2. 二、读写锁
    1. 2.1. 2.1 读写锁的由来
    2. 2.2. 2.2 生动有趣de例子
    3. 2.3. 2.3 ReentrantReadWriteLock 类
  3. 3. 三、重量级锁与轻量级锁
    1. 3.1. 3.1 定义
    2. 3.2. 3.2 生动活泼の例子
    3. 3.3. 3.3 自旋锁(Spin Lock)
  4. 4. 四、公平锁与非公平锁
  5. 5. 五、可重入锁和不可重入锁