1.简介

可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似。所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。ReentrantLock 的主要功能和 synchronized 关键字一致,均是用于多线程的同步。但除此之外,ReentrantLock 在功能上比 synchronized 更为丰富。比如 ReentrantLock 在加锁期间,可响应中断,可设置超时等。

ReentrantLock 是我们日常使用很频繁的一种锁,所以在使用之余,我们也应该去了解一下它的内部实现原理。ReentrantLock 内部是基于 AbstractQueuedSynchronizer(以下简称AQS)实现的。所以要想理解 ReentrantLock,应先去 AQS 相关原理。我在之前的文章 AbstractQueuedSynchronizer 原理分析 - 独占/共享模式 中,已经详细分析过 AQS 原理,有兴趣的朋友可以去看看。本文仅会在需要的时候对 AQS 相关原理进行简要说明,更详细的说明请参考我的其他文章。

2.原理

本章将会简单介绍重入锁 ReentrantLock 中的一些概念和相关原理,包括可重入、公平和非公平锁等原理。在介绍这些原理前,首先我会介绍 ReentrantLock 与 synchronized 关键字的相同和不同之处。在此之后才回去介绍重入、公平和非公平等原理。

2.1 与 synchronized 的异同

ReentrantLock 和 synchronized 都是用于线程的同步控制,但它们在功能上来说差别还是很大的。对比下来 ReentrantLock 功能明显要丰富的多。下面简单列举一下两者之间的差异,如下:

特性 synchronized ReentrantLock 相同
可重入
响应中断
超时等待
公平锁
非公平锁
是否可尝试加锁
是否是Java内置特性
自动获取/释放锁
对异常的处理 自动释放锁 需手动释放锁

除此之外,ReentrantLock 提供了丰富的接口用于获取锁的状态,比如可以通过isLocked()查询 ReentrantLock 对象是否处于锁定状态, 也可以通过getHoldCount()获取 ReentrantLock 的加锁次数,也就是重入次数等。而 synchronized 仅支持通过Thread.holdsLock查询当前线程是否持有锁。另外,synchronized 使用的是对象或类进行加锁,而 ReentrantLock 内部是通过 AQS 中的同步队列进行加锁,这一点和 synchronized 也是不一样的。

这里列举了不少两者的相同和不同之处,暂时这能想到这些。如果还有其他的区别,欢迎补充。

2.2 可重入

可重入这个概念并不难理解,本节通过一个例子简单说明一下。

现在有方法 m1 和 m2,两个方法均使用了同一把锁对方法进行同步控制,同时方法 m1 会调用 m2。线程 t 进入方法 m1 成功获得了锁,此时线程 t 要在没有释放锁的情况下,调用 m2 方法。由于 m1 和 m2 使用的是同一把可重入锁,所以线程 t 可以进入方法 m2,并再次获得锁,而不会被阻塞住。示例代码大致如下:

void m1() {
lock.lock();
try {
// 调用 m2,因为可重入,所以并不会被阻塞
m2();
} finally {
lock.unlock()
}
} void m2() {
lock.lock();
try {
// do something
} finally {
lock.unlock()
}
}

假如 lock 是不可重入锁,那么上面的示例代码必然会引起死锁情况的发生。这里请大家思考一个问题,ReentrantLock 的可重入特性是怎样实现的呢?简单说一下,ReentrantLock 内部是通过 AQS 实现同步控制的,AQS 有一个变量 state 用于记录同步状态。初始情况下,state = 0,表示 ReentrantLock 目前处于解锁状态。如果有线程调用 lock 方法进行加锁,state 就由0变为1,如果该线程再次调用 lock 方法加锁,就让其自增,即 state++。线程每调用一次 unlock 方法释放锁,会让 state--。通过查询 state 的数值,即可知道 ReentrantLock 被重入的次数了。这就是可重复特性的大致实现流程。

2.3 公平与非公平

公平与非公平指的是线程获取锁的方式。公平模式下,线程在同步队列中通过 FIFO 的方式获取锁,每个线程最终都能获取锁。在非公平模式下,线程会通过“插队”的方式去抢占锁,抢不到的则进入同步队列进行排队。默认情况下,ReentrantLock 使用的是非公平模式获取锁,而不是公平模式。不过我们也可通过 ReentrantLock 构造方法ReentrantLock(boolean fair)调整加锁的模式。

既然既然有两种不同的加锁模式,那么他们有什么优缺点呢?答案如下:

公平模式下,可保证每个线程最终都能获得锁,但效率相对比较较低。非公平模式下,效率比较高,但可能会导致线程出现饥饿的情况。即一些线程迟迟得不到锁,每次即将到手的锁都有可能被其他线程抢了。这里再提个问题,为啥非公平模式抢了其他线程获取锁的机会,而整个程序的运行效率会更高呢?说实话,开始我也不明白。不过好在《Java并发编程实战》第13.3节 公平性(p232)说明了具体的原因,这里引用一下:

在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于这个线程已经被线程 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此会再次尝试获取锁。与此同时,如果 C 也请求这个锁,那么 C 很有可能会在 B 被完全唤醒前获得、使用以及释放这个锁。这样的情况时一种“双赢”的局面:B 获得锁的时刻并没有推迟,C 更早的获得了锁,并且吞吐量也获得了提高。

上面的原因大家看懂了吗?下面配个图辅助说明一下:

如上图,线程 C 在线程 B 苏醒阶段内获取和使用锁,并在线程 B 获取锁前释放了锁,所以线程 B 可以顺利获得锁。线程 C 在抢占锁的情况下,仍未影响线程 B 获取锁,因此是个“双赢”的局面。

除了上面的原因外,《Java并发编程的艺术》在其5.3.2 公平与非公平锁的区别(p137)分析了另一个可能的原因。即公平锁线程切换次数要比非公平锁线程切换次数多得多,因此效率上要低一些。更多的细节,可以参考作者的论述,这里不展开说明了。

本节最后说一下公平锁和非公平锁的使用场景。如果线程持锁时间短,则应使用非公平锁,可通过“插队”提升效率。如果线程持锁时间长,“插队”带来的效率提升可能会比较小,此时应使用公平锁。

3. 源码分析

3.1 代码结构

前面说到 ReentrantLock 是基于 AQS 实现的,AQS 很好的封装了同步队列的管理,线程的阻塞与唤醒等基础操作。基于 AQS 的同步组件,推荐的使用方式是通过内部非 public 静态类继承 AQS,并重写部分抽象方法。其代码结构大致如下:

上图中,Sync是一个静态抽象类,继承了 AbstractQueuedSynchronizer。公平和非公平锁的实现类NonfairSyncFairSync则继承自 Sync 。至于 ReentrantLock 中的其他一些方法,主要逻辑基本上都在几个内部类中实现的。

3.2 获取锁

在分析 ReentrantLock 加锁的代码前,下来简单介绍一下 AQS 同步队列的一些知识。AQS 维护了一个基于双向链表的同步队列,线程在获取同步状态失败的情况下,都会被封装成节点,然后加入队列中。同步队列大致示意图如下:

在同步队列中,头结点是获取同步状态的节点。其他节点在尝试获取同步状态失败后,会被阻塞住,暂停运行。当头结点释放同步状态后,会唤醒其后继节点。后继节点会将自己设为头节点,并将原头节点从队列中移除。大致示意图如下:

介绍完 AQS 同步队列,以及节点线程获取同步状态的过程。下面来分析一下 ReentrantLock 中获取锁方法的源码,如下:

public void lock() {
sync.lock();
} abstract static class Sync extends AbstractQueuedSynchronizer {
// 这里的 lock 是抽象方法,具体的实现在两个子类中
abstract void lock(); // 省略其他无关代码
}

lock 方法的实现很简单,不过这里的 lock 方法只是一个壳子而已。由于获取锁的方式有公平和非公平之分,所以具体的实现是在NonfairSyncFairSync两个类中。那么我们继续往下分析一下这两个类的实现。

3.2.1 公平锁

公平锁对应的逻辑是 ReentrantLock 内部静态类 FairSync,我们沿着上面的 lock 方法往下分析,如下:

+--- ReentrantLock.FairSync.java
final void lock() {
// 调用 AQS acquire 获取锁
acquire(1);
} +--- AbstractQueuedSynchronizer.java
/**
* 该方法主要做了三件事情:
* 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回
* 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点,
* 并将节点放入同步队列尾部
* 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁
*/
public final void acquire(int arg) {
// acquireQueued 和 addWaiter 属于 AQS 中的方法,这里不展开分析了
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} +--- ReentrantLock.FairSync.java
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取同步状态
int c = getState();
// 如果同步状态 c 为0,表示锁暂时没被其他线程获取
if (c == 0) {
/*
* 判断是否有其他线程等待的时间更长。如果有,应该先让等待时间更长的节点先获取锁。
* 如果没有,调用 compareAndSetState 尝试设置同步状态。
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 将当前线程设置为持有锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程为持有锁的线程,则执行重入逻辑
else if (current == getExclusiveOwnerThread()) {
// 计算重入后的同步状态,acquires 一般为1
int nextc = c + acquires;
// 如果重入次数超过限制,这里会抛出异常
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置重入后的同步状态
setState(nextc);
return true;
}
return false;
} +--- AbstractQueuedSynchronizer.java
/** 该方法用于判断同步队列中有比当前线程等待时间更长的线程 */
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
/*
* 在同步队列中,头结点是已经获取了锁的节点,头结点的后继节点则是即将获取锁的节点。
* 如果有节点对应的线程等待的时间比当前线程长,则返回 true,否则返回 false
*/
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

ReentrantLock 中获取锁的流程并不是很复杂,上面的代码执行流程如下:

  1. 调用 acquire 方法,将线程放入同步队列中进行等待
  2. 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
  3. 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑

3.2.2 非公平锁

分析完公平锁相关代码,下面再来看看非公平锁的源码分析,如下:

+--- ReentrantLock.NonfairSync
final void lock() {
/*
* 这里调用直接 CAS 设置 state 变量,如果设置成功,表明加锁成功。这里并没有像公平锁
* 那样调用 acquire 方法让线程进入同步队列进行排队,而是直接调用 CAS 抢占锁。抢占失败
* 再调用 acquire 方法将线程置于队列尾部排队。
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
} +--- AbstractQueuedSynchronizer
/** 参考上一节的分析 */
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} +--- ReentrantLock.NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
} +--- ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取同步状态
int c = getState(); // 如果同步状态 c = 0,表明锁当前没有线程获得,此时可加锁。
if (c == 0) {
// 调用 CAS 加锁,如果失败,则说明有其他线程在竞争获取锁
if (compareAndSetState(0, acquires)) {
// 设置当前线程为锁的持有线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程已经持有锁,此处条件为 true,表明线程需再次获取锁,也就是重入
else if (current == getExclusiveOwnerThread()) {
// 计算重入后的同步状态值,acquires 一般为1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置新的同步状态值
setState(nextc);
return true;
}
return false;
}

非公平锁的实现也不是很复杂,其加锁的步骤大致如下:

  1. 调用 compareAndSetState 方法抢占式加锁,加锁成功则将自己设为持锁线程,并返回
  2. 若加锁失败,则调用 acquire 方法,将线程置于同步队列尾部进行等待
  3. 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
  4. 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑

3.2.3 公平和非公平细节对比

如果大家之前阅读过公平锁和非公平锁的源码,会发现两者之间的差别不是很大。为了找出它们之间的差异,这里我将两者的对比代码放在一起,大家可以比较一下,如下:

从上面的源码对比图中,可以看出两种的差异并不大。那么现在请大家思考一个问题:在代码差异不大情况下,是什么差异导致了公平锁和非公平锁的产生呢?大家先思考一下,答案将会在下面展开说明。

在上面的源码对比图中,左边是非公平锁的实现,右边是公平锁的实现。从对比图中可看出,两者的 lock 方法有明显区别。非公平锁的 lock 方法会首先尝试去抢占设置同步状态,而不是直接调用 acquire 将线程放入同步队列中等待获取锁。除此之外,tryAcquire 方法实现上也有差异。由于非公平锁的 tryAcquire 逻辑主要封装在 Sync 中的 nonfairTryAcquire 方法里,所以我们直接对比这个方法即可。由上图可以看出,Sync 中的 nonfairTryAcquire 与公平锁中的 tryAcquire 实现上差异并不大,唯一的差异在第18行,这里我用一条红线标注了出来。公平锁的 tryAcquire 在第18行多出了一个条件,即!hasQueuedPredecessors()。这个方法的目的是判断是否有其他线程比当前线程在同步队列中等待的时间更长。有的话,返回 true,否则返回 false。比如下图:

node1 对应的线程比 node2 对应的线程在队列中等待的时间更长,如果 node2 线程调用 hasQueuedPredecessors 方法,则会返回 true。如果 node1 调用此方法,则会返回 false。因为 node1 前面只有一个头结点,但头结点已经获取同步状态,不处于等待状态。所以在所有处于等待状态的节点中,没有节点比它等待的更长了。理解了 hasQueuedPredecessors 方法的用途后,那么现在请大家思考个问题,假如把条件去掉对公平锁会有什么影响呢?答案在 lock 所调用的 acquire 方法中,再来看一遍 acquire 方法源码:

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

acquire 方法先调用子类实现的 tryAcquire 方法,用于尝试获取同步状态,调用成功则直接返回。若调用失败,则应将线程插入到同步队列尾部,按照 FIFO 原则获取锁。如果我们把 tryAcquire 中的条件!hasQueuedPredecessors()去掉,公平锁将不再那么“谦让”,它将会像非公平锁那样抢占获取锁,抢占失败才会入队。若如此,公平锁将不再公平。

3.3 释放锁

分析完了获取锁的相关逻辑,接下来再来分析一下释放锁的逻辑。与获取锁相比,释放锁的逻辑会简单一些,因为释放锁的过程没有公平和非公平之分。好了,下面开始分析 unlock 的逻辑:

+--- ReentrantLock
public void unlock() {
// 调用 AQS 中的 release 方法
sync.release(1);
} +--- AbstractQueuedSynchronizer
public final boolean release(int arg) {
// 调用 ReentrantLock.Sync 中的 tryRelease 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
/*
* 如果头结点的等待状态不为0,则应该唤醒头结点的后继节点。
* 这里简单说个结论:
* 头结点的等待状态为0,表示头节点的后继节点线程还是活跃的,无需唤醒
*/
if (h != null && h.waitStatus != 0)
// 唤醒头结点的后继节点,该方法的分析请参考我写的关于 AQS 的文章
unparkSuccessor(h);
return true;
}
return false;
} +--- ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
/*
* 用同步状态量 state 减去释放量 releases,得到本次释放锁后的同步状态量。
* 当将 state 为 0,锁才能被完全释放
*/
int c = getState() - releases;
// 检测当前线程是否已经持有锁,仅允许持有锁的线程执行锁释放逻辑
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); boolean free = false;
// 如果 c 为0,则表示完全释放锁了,此时将持锁线程设为 null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
} // 设置新的同步状态
setState(c);
return free;
}

重入锁的释放逻辑并不复杂,这里就不多说了。

4.总结

本文分析了可重入锁 ReentrantLock 公平与非公平获取锁以及释放锁原理,并与 synchronized 关键字进行了类比。总体来说,ReentrantLock 的原理在熟悉 AQS 原理的情况下,理解并不是很复杂。ReentrantLock 是大家经常使用的一个同步组件,还是很有必要去弄懂它的原理的。

好了,本文到这里就结束了。谢谢大家的阅读,再见。

参考

本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处

作者:coolblog

本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

Java 重入锁 ReentrantLock 原理分析的更多相关文章

  1. Java中可重入锁ReentrantLock原理剖析

    本文由码农网 – 吴极心原创,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划! 一. 概述 本文首先介绍Lock接口.ReentrantLock的类层次结构以及锁功能模板类AbstractQue ...

  2. Java 重入锁 ReentrantLock

    本篇博客是转过来的. 但是略有改动感谢 http://my.oschina.net/noahxiao/blog/101558 摘要 从使用场景的角度出发来介绍对ReentrantLock的使用,相对来 ...

  3. 轻松学习java可重入锁(ReentrantLock)的实现原理

    转载自https://blog.csdn.net/yanyan19880509/article/details/52345422,(做了一些补充) 前言 相信学过java的人都知道 synchroni ...

  4. 轻松学习java可重入锁(ReentrantLock)的实现原理(转 图解)

    前言 相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么 ...

  5. java 可重入锁ReentrantLock的介绍

    一个小例子帮助理解(我们常用的synchronized也是可重入锁) 话说从前有一个村子,在这个村子中有一口水井,家家户户都需要到这口井里打水喝.由于井水有限,大家只能依次打水.为了实现家家有水喝,户 ...

  6. Java 显示锁 之 重入锁 ReentrantLock(七)

    ReentrantLock 重入锁简介 重入锁 ReentrantLock,顾名思义,就是支持同一个线程对资源的重复加锁.另外,该锁还支持获取锁时的公平与非公平性的选择. 重入锁 ReentrantL ...

  7. synchronized关键字,Lock接口以及可重入锁ReentrantLock

    多线程环境下,必须考虑线程同步的问题,这是因为多个线程同时访问变量或者资源时会有线程争用,比如A线程读取了一个变量,B线程也读取了这个变量,然后他们同时对这个变量做了修改,写回到内存中,由于是同时做修 ...

  8. 17_重入锁ReentrantLock

    [概述] 重入锁可以完全代替synchronized关键字. 与synchronized相比,重入锁ReentrantLock有着显示的操作过程,即开发人员必须手动指定何时加锁,何时释放锁,所以重入锁 ...

  9. Java 重入锁和读写锁

    本文部分摘自<Java 并发编程的艺术> 重入锁 重入锁 ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁.除此之外,该锁还支持获取锁时 ...

随机推荐

  1. 导航控制器&nbsp;UI_08(上)

    主要内容:UINavigationController 重要:属性传值(向后一个界面传值).代理传值(向前相邻的界面传值).单例传值(不相邻的界面向前传值) 1.UINavigationControl ...

  2. Chapter 2 User Authentication, Authorization, and Security(11):在已还原的数据库中修正登录映射错误

    原文出处:http://blog.csdn.net/dba_huangzj/article/details/39496517,专题目录:http://blog.csdn.net/dba_huangzj ...

  3. json进阶(一)js读取解析JSON类型数据

    js读取解析JSON类型数据 一.什么是JSON? JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,采用完全独立于语言的文本格式,是理想的数据交换格式,同 ...

  4. Linux 开发环境搭建

    本文多参考自网上资料,在此多谢这些资料的作者的辛勤劳动! 另外,本文所用 Linux 版本为 CentOS 7.1. 终端配置 安装及配置 Zsh 在默认情况下,Linux 下的终端是 bash,但其 ...

  5. Obj-C中内存的管理一瞥

    注意,ARC仅仅(自动)释放你手工管理的Objective-C类实例的内存, 但是不会释放由C函数或者Core Foundation(Cocoa的底层,C语言的变体)申请的内存.

  6. 【61】git项目实战的步骤总结

    1.新建分支的步骤 git pull git checkout -b 分支号(task的后面的代号) 2.提交代码到远程仓库的步骤 git add . git commit -m "分支号+ ...

  7. "《算法导论》之‘图’":单点最短路径(有向图)

    也许最直观的图处理问题就是你常常需要使用某种地图软件或者导航系统来获取从一个地方到另一个地方的路径.我们立即可以得到与之对应的图模型:顶点对应交叉路口,边对应公路,边的权重对应该路段的成本(时间或距离 ...

  8. Android回调详解

         很多时候开发遇到一些Ui更新 网络数据获取,或者方法方法传递的时候会借助回调函数,那么什么是回调函数 百度百科是这么解释的  转载请标注出处 http://blog.csdn.net/sk7 ...

  9. 在 Linux 下用 mkdir 命令来创建目录和子目录

    mkdir 是什么呢 Mkdir 是一个用来在 Linux 系统下创建目录的命令.此命令属于内建命令. 运行 mkdir 命令 你可以在你的控制台直接键入 mkdir 来使用它. $ mkdir 默认 ...

  10. 苹果新的编程语言 Swift 语言进阶(十)--类的继承

    一.类的继承 类能够从其它类继承方法.属性以及其它特性,当一个类从另外的类继承时,继承的类称为子类,它继承的类称为超类.在Swift中,继承是类区别与其它类型(结构.枚举)的基础行为. 1.1 .类的 ...