ReentrantLock是Concurrent包提供的一种可重入锁。可重入锁是指当一个线程获取锁进入互斥区后可以再次拿到该锁,而不需要释放之前获取的锁。通常锁都需要设计成可重入的,否则很容易发生死锁。
ReentrantLock 是基于AQS来实现的,AQS也叫抽象同步队列器,是Concurrent包提供的一种实现同步队列的框架。AQS提供了两种模式:共享模式和排它模式,ReentrantLock基于排它模式实现的一种可重入锁。
AQS基本原理
AQS类结构如下:
AbstractOwnableSynchronizer 是一个抽象类,只有一个成员变量exclusiveOwnerThread,表示持有锁的线程
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
...
private transient Thread exclusiveOwnerThread;
...
}
AbstractQueuedSynchronizer 中的成员变量很多,这些成员是实现队列同步的关键。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
private volatile int state;
private transient volatile Node head; //双端队列的队头
private transient volatile Node tail; //双端队列的队尾
//一个节点代表一个线程
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
//表示当前线程取消排队
static final int CANCELLED = 1;
//表示当前节点的下一个节点,等待被唤醒
static final int SIGNAL = -1;
//表示当前节点在等待condition,也就是在condition队列中
static final int CONDITION = -2;
//表示当前场景下后续的acquireShared能够得以执行;
static final int PROPAGATE = -3;
//等待的状态
volatile int waitStatus;
//上一个节点
volatile Node prev;
//下一个节点
volatile Node next;
//当前节点代表的线程
volatile Thread thread;
Node nextWaiter;
...
}
//条件队列
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
...
}
...
}
state:表示一种状态,当state的值为0时表示当前没有线程获取到锁,当一个线程获取到锁时,会利用cas操作将state的值从0变为1。对于可重入锁,一个线程多次获取锁,state的值会继续增长,表示重入的次数。
exclusiveOwnerThread:从AbstractOwnableSynchronizer 继承的而来,表示当前获取到锁的线程,初始时为NULL。
同步队列:没有获取到锁的线程会放到同步队列的队尾进入阻塞状态,等待被同步队列前面的元素唤醒。这个队列是由双向链表实现的双端队列。
双向链表中每一个Node节点代表一个等待的线程。节点的waitStatus表示等待的状态,初始值为0,其状态值可以为CANCELLED(1) 、SIGNA(-1)L、CONDITION(-2) 、PROPAGATE(-3)。
waitStatus的值为SIGNAL(-1)时,表示当前节点的下一个节点在等待唤醒。
waitStatus的值为CANCELLED (1)时,表示当前线程已经取消排队了。就好比你去排队购物,你发现前面排队的人太多了,干脆不排了,改天再来。
条件队列:调用Condition的awaite 方法,会使获取到锁的线程释放锁,并加入到条件等待队列中挂起。条件等待队列是一个单向链表。当其它线程调用Condition的singal方法会将条件队列中的头节点移动到同步队列的队尾,并唤醒。
ReentrantLock 类图如下:
ReentrantLock 本身并没有代码逻辑,实现都在其内部类Sync中,Sync是一个抽象类继承了AQS,它有两个子类FairSync、NonfairSync对应公平锁和非公平锁两种实现方式。
Lock()方法公平锁与非公平锁实现
非公平锁,不管队列是是否有线程在排队等锁,直接尝试获取锁。
final void lock() {
//一上来就使用cas操作来修改state的值,也就是抢锁,不考虑同步队列中是否有其它线程在排队
if (compareAndSetState(0, 1))
//设置当前线程为独占锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
公平锁,没有直接抢锁。
final void lock() {
acquire(1);
}
acquire 是AQS提供的一个模板方法,tryAcquire 再次尝试获取锁,被NonfairSync与FairSync分别实现。获取失败时,addWaiter 以给定模式为当前线程创建节点并将节点其放入等待排队。acquireQueued 阻塞线程,selfInterrupt 用于重置中断标识
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
nonfairTryAcquire 是 tryAcquire 的非公平锁实现:不管同步队列中是否有线程在等待获取锁,直接去尝试获取锁
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;
}
tryAcquire 的公平锁实现:先检查队头是否有其它线程在排队等锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors 检查队列前面是否有线程在排队等锁,有返回true,没有返回false
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;
}
//主要是用来判断线程需不需要排队,因为队列是FIFO的,
//所以需要判断队列中有没有相关线程的节点已经在排队了。
//有则返回true表示线程需要排队,没有则返回false则表示线程无需排队
public final boolean hasQueuedPredecessors() {
//读取头节点
Node t = tail;
//读取尾节点
Node h = head;
//s是首节点h的后继节点
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
addWaiter 以给定的模式(共享或独占)为当前线程创建节点,并将节点加入到同步队列。
private Node addWaiter(Node mode) {
//以给定的模式创建节点
Node node = new Node(Thread.currentThread(), mode);
//尝试快速添加节点
//获取同步队列的尾节点
Node pred = tail;
//尾节点不为空,利用cas操作将当前节点设置为尾节点,并调整指针
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//cas操作失败或者尾节点为空
enq(node);
return node;
}
enq 利用自旋锁尝试将节点加入到队列,当前头尾节点为null时,需要初始化头尾节点,将节点添加到尾部,会出现并发问题,AQS采用CAS+自旋锁的方式来处理并发问题。
同时需要关注的是,AQS同步队列的头节点是一个空节点,不代表线程。
//将node节点加入到同步队列的尾部,并返回该节点的前序节点
private Node enq(final Node node) {
//自旋锁
for (;;) {
//获取最新的尾节点
Node t = tail;
//尾节点为空,初始化头尾节点
if (t == null) { // Must initialize
//并发时,多个线程可能同时得到尾节点为空,cas操作只会有一个线程初始化成功,其他线程进入下一次循环。
if (compareAndSetHead(new Node()))
tail = head;
//完成初始化后进入下一次循环
} else {
//在本次循环中,头尾节点已经初始化,利用cas尝试将节点加入到尾部
//并发时,只有一个线程操作成功,其他线程进入下一次循环,重复上述操作:获取最新的尾节点,将当前线程对应的节点添加到尾部。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
阻塞队列示意图
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不使用cas操作是因为tryAcquire返回true,表示当前线程持有锁,才会进入到此逻辑
setHead(node);
//将原来的头节点的next指针置为null,原来的头节点就失去了引用,会被GC回收
p.next = null; // help GC
failed = false;
return interrupted;
}
//在获取锁失败后, 判断是否需要把当前线程挂起
//如果需要挂起,parkAndCheckInterrupt挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire 方法在获取锁失败的时候,根据当前节点的前驱节点状态来判断当前线程是否需要被挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态,初始时节点的状态值为0
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
如果前驱节点的状态为SIGNAL,说明当前节点需要阻塞
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
//此时前驱节点的状态是CANCELLED,因此需要跳过该节点,从新设置当前节点的有效前驱节点
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
//循环找到有效前驱节点为止,CANCELLED状态的节点从队列中被移除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//设置前驱节点的状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
前面提到了waitStatus的几种状态的含义,对于排它锁,我们只需要关心SIGNAL和CANCELLED 这两种状态。
这里再次强调:SIGNAL不是当前节点的状态,而是代表当前节点的下一个节点是否已经被挂起或者将要被挂起,从而需要被唤醒。CANCELLED 则表示当前节点已经取消排队
总结一下shouldParkAfterFailedAcquire 的主要作用:
当前驱节点的状态值是SIGNAL返回true
当前驱节点的状态值>0 也就是CANCELLED时,会从新设置当前节点的有效前驱节点,状态为CANCELLED的节点从队列中移除,并返回false
其他情况,将前驱节点的状态值,置为SIGNAL,返回false
parkAndCheckInterrupt 使用LockSupport.park 挂起线程的
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //线程在这里被挂起,直到被唤醒
return Thread.interrupted(); //当线程被唤醒时,执行Thread.interrupted()判断线程是否被中断,并擦除中断标识
}
线程调用LockSupport.park()进入阻塞状态,其他线程可以使用LockSupport.unpark(Thread t) 来唤醒该线程,同时LockSupport.park() 响应中断,也就是t.interrupt()。因此当线程被唤醒的时候,会调用Thread.interrupted()来判断线程是由于哪种情况被唤醒的
如果是被中段唤醒,由于Thread.interrupted() 会擦除中断标识,因此当线程被中断唤醒获取到锁后,会重新设置中断标识。用于后续可中断方法或者其他中断处理。
unlock() 方法源码
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
//锁释放完成后唤醒队列中的下一个节点
//从lock方法中我们可以知道,线程获取到锁后,代表该线程的节点是head
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//减少重入次数
int c = getState() - releases;
//如果当前节点不是持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//重入次数为0,释放锁,将持有锁的线程置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//重新设置状态值,这里没有使用cas,
//调用unlock方法前,必须调用lock()方法获取锁,同时对于独占锁,锁由线程独占,不存在竞争,所以不需要使用cas来设置状态
setState(c);
return free;
}
lockInterruptibly() 源码分析
该方法会响应中断,抛出中断异常,与lock()的区别主要体现在doAcquireInterruptibly 中,收到中断信号之后,直接抛出异常,并返回
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
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);
}
}