【并发编程】 --- Reentrantlock源码解析1:同步方法交替执行的处理逻辑

源码地址:https://github.com/nieandsun/concurrent-study.git


上篇文章《【并发编程】 — 从JVM源码的角度进一步去理解synchronized关键字的原理》讲到,在JDK1.6之前使用synchronized关键字会有这么一个问题:

无论同步方法是否正在面临并发,都会调用内核函数,也就是说都会让CPU进行用户态和内核态之间的切换 — 这种切换会带来大量的系统资源消耗。

  • 在确实存在并发的情况下,这种做法一点问题都没有;
  • 但有研究表明程序在大多数情况下都是交替执行,不会出现并发的
    — 举个极端的栗子,假如一块同步方法,在99%的时间内都不会并发执行(当然也就不会产生并发安全问题)
    — 那为什么非要把那1%的处理逻辑强加到这99%上呢,而且这1%的处理逻辑又确实是比较耗性能的
    — 自然而然地就会引出这样的思考:能不能在程序交替执行但并未发生并发的情况下,不调用内核函数,而是到真正发生了并发时再调用???
    — 其实Reentrantlock和JDK1.6及之后对synchronized关键字的优化就着重考虑了这一点。

本篇文章先来看一下Reentrantlock是如何做的。


1 Reentrantlock前置知识


1.1 理清代码同步的本质

Reentrantlock实现代码同步的本质可以用下图进行表示:
在这里插入图片描述
其实大家可以想想,synchronized关键字实现同步方法的本质不也正是如此嘛!!!

理清了其本质之后,相信你自己都可以实现一把锁 —> 当然要想实现一个像Doug Lea搞得Reentrantlock这么牛X的锁,你还需要必备一些前置知识。


1.2 Reentrantlock的核心三板斧


1.2.1 Compare And Swap(CAS) — 保证同一时刻只有一个线程可以抢到锁

了解了synchronized关键字的底层原理之后,应该知道可以通过_recursions变量是否为0判断monitor对象是否已经被抢占了,也就是锁对象是否被抢占了。 与此类似,Reentrantlock判断当前锁是否被抢占也使用了一个类似的变量 — state,在Reentrantlock中:

  • 所谓的加锁就是通过CAS操作将state的值由0变为1,重入+1(当然还涉及到将当前线程设为锁拥有者等其他操作。。。)
  • 释放锁就是通过CAS操作将state的值由1变为0

CAS的原理这里不再叙述,但必须清楚

  • CAS的整个操作是原子操作 —》 也就是说 CAS保证了同一时刻只可能有一个线程抢到锁。
  • CAS操作不需要调用内核函数 —》 也就是说 CAS操作是轻量级的 — 这一点一定要非常明确!!!

有兴趣的可以参考我的文章《【并发编程】 — Compare And Swap(CAS)原理分析》对CAS操作进一步地了解。


1.2.2 park & unpark — 让竞争不到锁的线程立刻挂起和从挂起中唤醒

当理清了让代码同步的本质之后,假如让你自己来实现一个锁的话,我猜你肯定会思考该怎样使未抢到锁的线程挂起的问题。

我猜你或许会相当如下方式:

  • (1)什么挂起不挂起啊,直接将未抢到锁的线程在lock()方法里进行while(true)死循环,当锁标志state变为0时,就通过CAS抢锁,能抢到就执行同步方法,抢不到继续进行死循环 —> 可以实现,但性能差,浪费CPU资源
  • (2)没抢到锁的线程使用yield让出CPU资源 —> 不可行,多线程下,即使让出资源,下次还是有可能本线程抢到CPU执行权
  • (3)通过wait挂起线程,再通过notify/notifyAll唤醒线程 —> 不可行,因为wait、notify/notifyAll必须在synchronized关键字里执行,都有synchronized关键字了,还用你去实现啊???
  • (4)通过sleep让线程睡眠,睡一会,然后去抢,抢不到接着睡 —> 不可行,睡眠的时间没法控制

当把所有已知的知识回顾了一遍后,你会发现好像并没有哪个技术可以真正很好的去实现线程的挂起与唤醒功能。。。


这时候就不得不再次提起《【并发编程】 — Compare And Swap(CAS)原理分析》那篇文章中提到的Unsafe类,它里面有两个方法 — park & unpark。


这两个方法的作用正是让指定的线程立刻挂起 和 从挂起状态中立刻被唤醒 — 多么适合的两个方法啊。

不知道你会不会有这种感觉,要是你也知道有这么两个方法,要是你能早生n多年,或许JUC包就是你开发的了。


1.2.3 自旋 — 确保线程尽量不进行park

之后的文章再着重讲解。


1.3 AQS — 保证被挂起的线程按照一定的规则被唤醒

AQS(即AbstractQueuedSynchronizer类)中的数据结构如下:

private transient volatile Node head;//队首
private transient volatile Node tail;//队尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 释放锁则为0
//静态内部类
static final class Node{} //存储当前线程、下一个节点、上一个节点、线程状态等的内部类

将Node节点进行简化后,可以用下图表示AQS的内部结构:

在这里插入图片描述
该数据结构保证了被挂起的线程可以按照一定的规则被唤醒 — 之后的文章肯定还会对此进行细究。


1.4 AQS与Reentrantlock之间的关系

  • 首先来看一下Reentrantlock的内部结构:

在这里插入图片描述

  • 再来看一下Sync的继承关系图

在这里插入图片描述

由上面两幅图可以看出,其实Reentrantlock中真正进行加锁和解锁的执行类是Sync ;而且因为Sync继承了AQS类,所以被挂起线程的链表数据结构及其维护都是由Sync来具体实现的。


这种实现方式其实达到了一个很重要的目的,即对Reentrantlock的使用者(即我们这些Java开发人员)屏蔽掉该类的具体实现细节,让我们可以只关注于具体的业务逻辑 —》 非常重要的一个思想!!!


其实读过CountDownLatch或Semaphore源码的可以知道,它们内部也使用了相同的设计 —> 即也都有一个继承了AQS的静态内部类Sync —> 一个非常标准的模版+策略模式!!!

2 同步方法交替执行时Reentrantlock的处理逻辑

了解了以上前置知识后,我们接下来看看Reentrantlock在同步方法交替执行时的具体处理逻辑。

主要看看Reentrantlock是如何在同步方法未发生并发时不去调用内核方法的具体原理。


2.1 非公平锁的处理逻辑

非公平锁在同步方法交替执行、未出现并发时的处理逻辑大致如下:
在这里插入图片描述
由上图可以看出,当使用非公平锁时,多个线程交替执行同步方法,每个线程所要做的事就成了: (1)通过CAS把state的值变为1(当然还要把当前线程设置为锁拥有者,这里我就省略了) ---》 (2)执行同步方法 ---》 (3)将state的值再通过CAS改为0(还省略了锁重入的过程,有兴趣的可以再深究一下,源码其实很简单)。

这整个过程根本不会涉及到内核函数的调用 —> 因此Reentrantlock的非公平锁非常完美的解决了本文开篇提到的问题!!!


2.2 公平锁的处理逻辑

公平锁在同步方法交替执行、未出现并发时的处理逻辑大致如下:

在这里插入图片描述
由上图可以看出,当使用公平锁时,多个线程交替执行同步方法,每个线程所要做的事就成了下面的样子:

在这里插入图片描述
在这个过程根本也不会涉及到内核函数的调用 —> 因此Reentrantlock的公平锁也非常完美的解决了本文开篇提到的问题!!!


end

发布了225 篇原创文章 · 获赞 319 · 访问量 53万+

猜你喜欢

转载自blog.csdn.net/nrsc272420199/article/details/105307025