1、序
并发编程在Java实际开发中,占有举足轻重的地位,在接下来的篇幅中,以java.util.concurrent包下重要的、常用的接口、实现类为切入点,逐步分析并发编程。
java.util.concurrent包下的类相对来讲较难,需要反复的分析、调试。每次分析的过程中都会有不同的心得体会,所以本系列的代码分析,力求细致;语言描述,力求简洁。
其中的一些插图借鉴了《Java并发编程的艺术》一书,部分接口、类、方法、以及变量的定义摘抄自JDK的源码注释。当然鉴于水平有限,也希望大家多多指正其中的不足。
2、Lock 接口
Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition。
锁是用于通过多个线程控制对共享资源的访问的工具。通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读锁。
使用synchronized方法或语句提供对与每个对象相关联的隐式监视器锁的访问,但是强制所有锁获取和释放以块结构的方式发生:当获取多个锁时,它们必须以相反的顺序被释放,并且所有的锁都必须被释放在与它们相同的词汇范围内。
虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。 例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。所述的实施方式中Lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。
Lock实现提供了使用synchronized方法和语句的附加功能,通过提供非阻塞尝试来获取锁( tryLock() ),尝试获取可被中断的锁( lockInterruptibly()) ,以及尝试获取可以超时( tryLock(long, TimeUnit) )。
内存同步 所有Lock实施必须执行与内置监视器锁相同的内存同步语义,如The Java Language Specification (17.4 Memory Model) 所述:
- 成功的lock操作具有与成功锁定动作相同的内存同步效果。
- 成功的unlock操作具有与成功解锁动作相同的内存同步效果。
- 不成功的锁定和解锁操作以及重入锁定/解锁操作,不需要任何内存同步效果。
虽然JDK的注释通过翻译软件直译过来有些拗口,但是没有比官方文档更为好的解释。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock接口一共提供了6个方法。虽然只是接口定义,但是对我们了解其实现原理极为重要,因为接口的实现,也是依据其定义而展开。若不对其定义有充分的了解,当分析到源码细节的时候,将举步维艰,不知所云。
- lock()
- 若锁可用,立刻获得锁
- 若锁不可用,阻塞当前线程直至获取到锁
- lockInterruptibly()
- 若锁可用,立刻获得锁
- 若锁不可用,当前线程将被阻塞,直至发生以下情况
- 当前线程获得锁
- 当前线程被其它线程中断
- tryLock()
- 若锁可用,立刻获得锁,并返回true
- 锁锁不可用,立刻返回false,而不阻塞当前线程
- 该方法可以理解为尝试获取锁、或快速获取锁,因为该方法不会阻塞线程
- tryLock(long time, TimeUnit unit)
- 若锁可用,立刻获得锁,并返回true
- 若锁不可用,当前线程将被阻塞,直至发生以下情况
- 当前线程获的锁
- 当前线程被其它线程中断
- 超时
- unlock()
- 释放锁
- 释放锁的线程必须是持有当前锁的线程
- newCondition()
- 创建一个与当前锁绑定的Condition(条件)
- 在条件满足之前,锁将被持有
- Condition一般可用于有界操作,如大小固定的缓冲区
3、AbstractQueuedSynchronizer
Lock接口最为典型的实现类是ReentrantLock,可重入锁。在介绍ReentrantLock之前,必须先了解一下AbstractQueuedSynchronizer,队列同步器,也就是我们平时所说的AQS。AbstractQueuedSynchronizer提供了一个框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。对于大多数依赖单个原子int值来表示状态的同步器,这个类被设计为一个有用的基础。此类支持默认独占模式和共享模式。
3.1 双端队列
AbstractQueuedSynchronizer提供了一个静态内部类Node,用来实现一个双端队列。Node类代表了双端队列中的一个节点,其中waitStatus变量以及其不同的值的含义,至关重要。
static final class Node {
// 标记节点以共享模式阻塞
static final Node SHARED = new Node();
// 标记节点以独占形式阻塞
static final Node EXCLUSIVE = null;
// 以下四个枚举值表示了 waitStatus 不同状态
// 节点被取消
static final int CANCELLED = 1;
// 后继节点等待被唤醒
static final int SIGNAL = -1;
// 节点在“条件”上等待
static final int CONDITION = -2;
// 下一次获取共享同步状态将被无条件传递下去
static final int PROPAGATE = -3;
// 节点在队列中的状态,默认初始值为0
volatile int waitStatus;
// 上一个节点
volatile Node prev;
// 下一个节点
volatile Node next;
// 获取锁的线程
volatile Thread thread;
}
名称 | 值 | 说明 |
---|---|---|
CANCELLED | 1 | 表示由于超时或者中断,当前节点被取消。被取消后,当前节点的状态将不再发生任何变化。 |
SIGNAL | -1 | 表示后继节点等待获取同步状态,当前节点释放同步状态、中断、取消则唤醒其后继节点,以继续获取同步状态。 |
CONDITION | -2 | 表示当前节点在等待condition |
PROPAGATE | -3 | 表示下一次获取共享同步状态将被无条件传递下去 |
0 | 初始状态 |
有了节点以后,再来看AbstractQueuedSynchronizer类中其他几个重要的变量:
// 等待队列的头,被惰性地初始化。除了初始化之外,它只通过方法setHead进行修改。注意:如果head存在,它的等待状态保证不会被取消。
private transient volatile Node head;
// 等待队列的尾部,延迟初始化。仅通过enq()方法修改以添加新的等待节点。
private transient volatile Node tail;
// 同步状态
private volatile int state;
AbstractQueuedSynchronizer持有了头节点和尾节点,并分别指向队列中的第一个节点和最后一个节点。最终形成的队列如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h5vpXrzG-1605858563162)(media/16057705056625/16057803571571.jpg)]
3.2 state变量
state变量可以说是AbstractQueuedSynchronizer中最为重要的变量。jdk的注释:The synchronization state。直译过来即同步状态。
对于独占锁,state变量即可以理解为锁。当state为0时,表示锁空闲,当前线程可以获得锁;当state为1时,表示锁被其它线程占有,当前线程无法获取锁(假如不允许重入)。那么当前线程可以被构造为Node节点,加入到队列中、并阻塞。当持有锁的线程释放锁之后,将state变量重新设置为0,并唤醒阻塞的线程,继续获得锁。
对state变量的操作,可以通过以下三个方法进行:
// 获取同步状态值,该方法具有volatile读的内存语义
protected final int getState() {
return state;
}
// 设置同步状态值,该方法具有volatile写的内存语义
protected final void setState(int newState) {
state = newState;
}
// 如果当前状态值等于预期值,则自动将同步状态设置为给定的更新值。
// 该方法兼顾volatile读、写的内存语义
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
关于AbstractQueuedSynchronizer就先介绍这么多,在后续的代码分析中,结合实际的例子来分析该类的其它方法。
4、ReentrantLock简介以及其非公平锁模式
ReentrantLock可重入锁是Lock接口最为典型的一个实现类,它具有以下特点:
- 独占锁,即同一时刻,只能有一个线程持有锁
- 可重入,持有锁的线程可再次获得锁,而无需等待
- 提供公平锁、非公平锁两种模式,默认为非公平锁。对于公平锁,ReentrantLock保证先请求获取锁的线程,一定先获得锁,即“先到先得”
4.1、lock()
// 案例:
public void testReentrantLock() {
Lock lock = new ReentrantLock();
// 注意:加锁不能写在try代码块,如果try代码块加锁未成功,则finally代码块释放锁会出现异常。
lock.lock();
try {
System.out.println("加锁");
} finally {
lock.unlock();
System.out.println("解锁");
}
}
通过lock接口获取锁,大体上可以分为以下三种情况:
- 锁空闲,加锁并将持有锁的计数(state)加1。
- 锁被当前线程持有(重入),将持有锁的计数加一。
- 锁被其它线程持有,则阻塞当前线程直至获取到锁,然后将持有锁的计数设置为一。
final void lock() {
// 无线程持有锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
// 已有线程持有锁
else
acquire(1);
}
lock()方法执行过程:
- 通过compareAndSetState()方法尝试立即获取同步状态
- 成功,返回
- 失败,调用acquire()方法继续获取同步状态
4.2、acquire()
public final void acquire(int arg) {
// 尝试快速获取同步状态,若失败,则阻塞该线程直至获取到同步状态
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 若线程在获取同步状态的过程中曾被其它线程中断,则再次中断该线程
// 注意:acquireQueued返回值标识该线程在阻塞期间是否被中断,而不是是否成功获取到同步状态
selfInterrupt();
}
- 以独占形式获取锁
- 忽略中断
- 假如T1进入lock()方法后即被其它线程中断,lock()方法不会抛出InterruptedException异常
- acquire()在方法执行过程中会清除、并记录线程是否被中断
- 否,不做任何操作
- 是,调用selfInterrupt()中断线程,此时已经获取到同步状态,线程得以继续被执行,线程调用者可以自行决定是否中断继续中断该线程。
- 至少调用一次tryAcquire()方法尝试快速获取同步状态(该方法不会阻塞)
- 成功,返回
- 失败
- 调用addWaiter()方法将线程构造为Node节点加入同步队列
- 阻塞线程直至获取到同步状态,在此期间,线程可能会反复被唤醒、阻塞,直至再次通过tryAcquire()方法获取到同步状态为止
下面逐个分析tryAcquire()、addWaiter()、以及acquireQueued()方法。
4.2.1、tryAcquire()
tryAcquire()方法可以理解为“尝试获取同步状态”或“快速获取同步状态”。该方法最大的特点是立刻返回是否成功获取同步状态,而不阻塞线程。对于独占锁,获取同步状态的情况可以分为两种:锁空闲或线程重入,可以立即获取;锁被占用,以阻塞的形式获取。因为当一个线程获取锁时,无法确定锁是否被持有,可以先通过tryAcquire()快速获取锁;若失败则将线程构造为Node节点加入阻塞队列,当其满足出队条件后,将被唤醒,然后再次调用tryAcquire()方法获取同步状态。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// 以独占的形式快速获取同步状态
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 同步状态为0,锁空闲,可以立即获取锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 同步状态非0,锁被持有,判断是否重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- 以独占的形式获取锁
- 成功,返回true
- 失败,判断是否重入
- 是,将同步状态加一并更新,返回true
- 否,锁被其它线程持有,返回false
- 该方法不会阻塞线程,而是立刻返回结果
4.2.2、addWaiter()
若addWaiter()方法得以被执行,说明锁被其它线程持有,则将当前获取锁的线程构造成Node节点加入到同步队列中。
- 快速入队:
private Node addWaiter(Node mode) {
// 将当前线程构造为Node节点
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速在队列尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 快速添加失败,调用全量方法将节点加入队列
enq(node);
return node;
}
- 全量方法入队:
// 节点入队全量方法
private Node enq(final Node node) {
// 自旋
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter()方法涉及到一些队列知识,比较简单,不多赘述。
4.2.3、acquireQueued()
加入到同步队列中的节点,将以“自旋”的方式继续尝试获取同步状态。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
// ①
// 为保证队列“先进先出”的特性。当前驱节点为头节点时,可尝试获取同步状态,其它节点依然阻塞
// 注意:若某个节点的前驱节点为头节点,则说明它是队列中的第一个节点,符合“先进先出”原则
if (p == head && tryAcquire(arg)) {
// 获取同步状态成功,更新头结点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// ②
// 若前驱节点非头结点、或者获取同步状态失败,则阻塞当前节点
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 第一个if,判断前驱节点是否为head节点(保证队列“先进先出”的原则)
- 是,尝试获取同步状态
- 获取成功,返回true
- 获取失败,进行第二个if判断
- 否,进行第二个if判断
- 是,尝试获取同步状态
- 第二个if,执行到此,要么前驱节点非头节点,不符合出队原则;要么前驱节点是头结点,但获取同步状态失败(持有锁的线程依然未释放锁)
- 判断当前节点尝试获取同步状态失败后是否应当阻塞、并更新节点状态
- 是,阻塞当前节点,清除、并记录当前线程中断标识
- 否,自旋,进行下一轮判断
- 判断当前节点尝试获取同步状态失败后是否应当阻塞、并更新节点状态
第一个if判断非常简单,不多介绍,下面看第二个if语句中的两个判断条件:
4.2.3.1 shouldParkAfterFailedAcquire()
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱节点的状态
int ws = pred.waitStatus;
// 前驱节点的状态为SIGNAL,则后继节点应被阻塞,返回true
if (ws == Node.SIGNAL)
return true;
// 前驱节点状态为CANCELLED,则清除队列中已经被取消的节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}
// 运行至此,前驱节点的状态要么为初始状态(0)、要么为PROPAGATE。
// 此时,将前驱节点的状态改为 SIGNAL,以便在下一轮自旋中阻塞当前节点
else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
假设T1持有锁且不释放,T2再次获取锁,则acquireQueued()方法将被调用。因为T2的前驱节点即为头节点,其符合出队条件。那么再次调用tryAcquire()方法获取锁,因为T1不释放锁,tryAcquire()返回false,所以shouldParkAfterFailedAcquire()方法将被调用。
进入到shouldParkAfterFailedAcquire()方法后(注意该方法参数一个是前驱节点、一个是当前节点),前驱节点的状态为0,将前驱节点状态改为SIGNAL,返回false。
因为acquireQueued()方法以自旋的方式进行,所以上面的步骤被重复,当获取同步状态再次进入shouldParkAfterFailedAcquire()之后,前驱节点的状态已经变为SIGNAL,而且两次通过tryAcquire()方法获取同步状态均失败,则当前线程应被阻塞。
如果此时T3也来获取锁,则重复上面的步骤,当然此时的T3是不符合出队条件的,总而言之,通过acquireQueued()方法自旋调用,加上shouldParkAfterFailedAcquire()方法,保证了在必要时候将前驱节点的状态设置为SIGNAL。
4.2.3.2 parkAndCheckInterrupt()
// 阻塞当前节点,清除并返回当前线程中断标识
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
LockSupport.park(this);
// 清除并返回线程中断标识
return Thread.interrupted();
}
该方法很简单,不多赘述。线程被阻塞之后,则等待前驱节点释放同步状态之后将其唤醒,继续获取同步状态即可。代码分析至此,lock()方法就结束了。
4.3、unlock()
解锁流程如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n12lMThi-1605858563164)(media/16057705056625/16058499546947.jpg)]
// 解锁
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 释放同步状态
if (tryRelease(arg)) {
// 唤醒后继节点
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- 释放同步状态
- 唤醒后继节点
4.3.1、tryRelease()
// 释放同步状态
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 释放锁的线程必须与AQS中持有同步状态的线程相同
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 完全释放同步状态,清空独占线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 更新state值
setState(c);
return free;
}
tryRelease()方法主要是为了更新AQS中state变量的值。前文已经介绍过,对于独占锁来讲,state等于0可以表示锁空闲,state大于0可以表示锁被持有。同时要考虑锁被重入的情况,所以讲state值递减,直至为0,则同步状态完全被释放。
4.3.2、unparkSuccessor()
// 唤醒后继节点
private void unparkSuccessor(Node node) {
// 如果节点的waitStatus值小于0,则更新为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 回溯,保证后继节点及其状态的合法性
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒后继节点
if (s != null)
LockSupport.unpark(s.thread);
}
unparkSuccessor()方法顾名思义为唤醒后继节点,但是这个说法不太准确,应该叫唤醒有效的后继节点更为贴合代码语义。
- 如果节点的waitStatus值小于0,则更新为0。todo 这一步的目的不太清楚
- 回溯,保证队列节点的合法性,防止节点状态为取消、节点为空的情况
- 唤醒第一个可用的后继节点
到这里,基于lock()、unlock()方法的加锁、解锁过程就分析完毕了。
4.4、lockInterruptibly()
相较于lock()接口lockInterruptibly()在获取锁的过程中,会响应中断。因为前文已经对lock()接口做了较为详细的分析,所以这里我们只简单的分析一下lockInterruptibly()是如何响应中断的。
// 以响应中断的模式获取同步状态
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg) throws InterruptedException {
// ①
// 快速检查线程是否被中断
if (Thread.interrupted())
throw new InterruptedException();
// 调用tryAcquire()方法获取同步状态。注意:该方法不响应中断
if (!tryAcquire(arg))
// 以响应中断的模式获取同步状态
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// ② 响应中断
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- lockInterruptibly()对中断的响应分别标记在代码的①、②处:
- 快速检查线程是否被中断
- 是,抛出InterruptedException异常
- 否,继续获取同步同步状态
- 判断线程在获取锁的过程中是否被中断
- 否,返回
- 是,抛出InterruptedException异常
- 快速检查线程是否被中断
- lock()方法和lockInterruptibly()对线程中断的处理方式区别:
- lock方法不响应中断,但是会记录中断状态,开发者需要自己去判断、并响应中断
- lockInterruptibly()方法响应中断,若线程被中断,抛出InterruptedException异常
4.5、tryLock()
tryLock()方法以非阻塞的形式获取锁。若获取到锁,返回true;否则,返回false,而不会阻塞获取锁的线程。tryLock()方法的具体代码,前文都有介绍,不多赘述。
4.6、tryLock(long time, TimeUnit unit)
前文已经介绍过了lock()、lockInterruptibly()、tryLock()三种获取同步状态的方式。这三种方式个有优缺点。
- lock() 以阻塞的形式获取锁,不响应中断
- lockInterruptibly() 以阻塞的形式获取锁,响应中断
- tryLock() 以非阻塞的形式获取锁,不响应中断
综合以上各个方法的特性,lock()、lockInterruptibly()虽然能获取到锁,但是调用者不知道会阻塞多久;tryLock()方法虽然能快速返回是否获取到锁,但是又不会阻塞。tryLock(long time, TimeUnit unit)方法正好综合了以上特点。以带超时阻塞的形式获取锁。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 响应中断
if (Thread.interrupted())
throw new InterruptedException();
// 调用tryAcquire()方法尝试快速获取同步状态
// 若tryAcquire()方法未能获取到同步状态,则调用doAcquireNanos以带超时阻塞的形式再次获取同步状态
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
tryLock(long timeout, TimeUnit unit)方法中的大部分代码均已分析过,不在赘述,这里只分析doAcquireNanos()方法。
// 以超时模式获取同步状态
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 超时
if (nanosTimeout <= 0L)
return false;
// 计算超时到期时间
final long deadline = System.nanoTime() + nanosTimeout;
// 线程构造为AQS节点、入队
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 符合出队条件,再次获取同步状态
if (p == head && tryAcquire(arg)) {
// 获取同步状态成功,重新设置头节点、清空释放同步状态的节点
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 计算需要阻塞的时长
nanosTimeout = deadline - System.nanoTime();
// 超时
if (nanosTimeout <= 0L)
return false;
// spinForTimeoutThreshold:自旋超时阈值,1000纳秒(1秒=1000毫秒;1毫秒=1000微秒;1微秒=1000纳秒)
// 如果nanosTimeout大于spinForTimeoutThreshold,则将线程阻塞nanosTimeout纳秒
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
// 阻塞nanosTimeout纳秒,到期唤醒后,再次以自旋的形式获取锁
LockSupport.parkNanos(this, nanosTimeout);
// 响应中断
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
到这里,基于ReentrantLock可重入锁的非公平模式下的lock()、lockInterruptibly()、tryLock()、tryLock(long time, TimeUnit unit)、以及unlock()方法都以分析完毕。这一部分内容相对来讲较难,需要多多分析、多调试才能有更为深刻的了解。笔者水平有限,还望多多指正。
5、ReentrantLock的公平锁模式
公平锁模式保证先获取锁的线程一定能够先获得锁,简单理解就是“先到先得”。前文已经分析过非公平锁模式,下文的分析我们着重分析两者之间的区别,而不再逐个分析每个方法。
5.1、lock()
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors() 查询是否有线程等待获取的时间长于当前线程。
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
针对于lock()方法,非公平锁和公平锁的区别就在于在tryAcquire()方法中多了一个hasQueuedPredecessors()判断。即判断是否有线程等待获取的时间长于当前线程。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
5.2、tryLock()
tryLock()方法会破坏锁的公平性。若能立即获取锁,返回true;否则返回false。而不会考虑是否有其他线程先于此线程获取锁。
5.3、tryLock(long time, TimeUnit unit)
tryLock(long time, TimeUnit unit)保持锁的公平性与lock()方法一致,不多赘述。