ReentrantLock是JUC中最常用的一种重入锁,其内部实现原理是通过一种叫AQS的队列来控制并发。
我们先来讨论一下锁的基本原理,多线程环境下,如果要共同访问一个资源,有这么两种情况:
- 线程先后交替执行,不存在竞争,一个线程拿到同步代码的执行权(即获取锁)直接执行即可,无需将自己挂起。
- 多个线程同时执行,存在竞争,只有一个线程可以拿到锁,而其他线程就乖乖排队,并且把自己挂起,等那个线程执行完了再从队列中唤醒一个排队的线程。
根据上面的分析,我们可以得出实现锁需要一个排队的队列,以及线程的挂起、唤醒机制。然后我们再来看看ReentrantLock是怎样实现这些机制的。
我们先看加锁过程,它的lock方法:
public void lock() {
sync.lock();
}
它调用了sync的lock方法,这个sync是什么:
由上面的层次结构图可知,Sync继承了AbstractQueuedSynchronizer(简称AQS),Sync之下还有两个子类FairSync和NonfairSync(公平锁和非公平锁)。AQS顾名思义是抽象队列同步器,也就是我们上文提到的排队队列,公平锁和非公平锁我稍后再做解释。
sync的lock方法是抽象的,具体实现交由它的两个子类,我们先以FairSync的实现为例:
final void lock() {
acquire(1); //记住这里传入了1
}
AQS中的acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法是获取锁的总体方法,我们来看它做了哪些步骤,首先调用tryAcquire方法,不过这个tryAcquire在AQS中只是抛了一个异常,并没有具体的实现,所以还得看具体子类的实现,还拿FairSync为例:
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取一个state值,这个state可以看作当前锁是否被持有的状态,
//如果是0则说明没有线程持有锁
int c = getState();
//无锁状态
if (c == 0) {
if (!hasQueuedPredecessors() && //判断自己是否需要排队
compareAndSetState(0, acquires)) { //如果不需要排队则通过CAS获取锁
setExclusiveOwnerThread(current); //获取成功则将独占线程设置为当前线程
return true;
}
}
//如果已有线程获取锁,则判断当前独占线程是不是自己
else if (current == getExclusiveOwnerThread()) {
//如果是自己,则将当前state值+1(acquires传入了1)
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//重新设置state值,
//这里体现了“可重入”的概念,当同一个线程再次获取锁时,不需要挂起,只需要将state+1即可
setState(nextc);
return true;
}
return false;
}
在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());
}
这里首先会获取一个头节点和尾节点,这两个节点是什么?我们需要先看看AQS到底是个什么结构:
它有两个Node类型的成员变量head和tail,这个Node类型又包括如下内容:
其中prev表示当前node的前置节点,next是后置节点,thread表示当前node绑定的线程对象,可见AQS是一种双向链表的结构。
然后回到hasQueuedPredecessors方法,在线程先后交替执行的时候,并没有线程需要排队,所以当前队列是空的,head和tail都为null,这时h != t为false,不会再执行后面的判断,该方法返回。
然后又调用compareAndSetState方法,通过CAS设置state值,CAS如果不了解的话自行百度吧,网上关于它的讲解太多了。如果CAS成功,再将当前AQS中的独占线程设置为当前线程,即获取锁成功。
刚才讨论的是线程交替执行,没有初始化队列的情况,如果是多个线程同时执行,存在竞争,这里又该怎样判断呢?
如果有多个线程竞争,就会形成队列,此时h!=t成立,这时就会进入后面的判断,首先判断(s = h.next) == null,也就是头结点的下一个是否为null,很明显不是,然后又判断s.thread != Thread.currentThread(),也就是头结点的下一个节点是否为当前线程,如果是则返回true,否则返回false。
说明:
- 为什么要判断头结点的下一个节点,而不是判断头结点?
因为在AQS中,头结点永远是持有锁的节点,该节点绑定的线程处于运行状态。只有头节点之后的线程才是真正被挂起的排队线程。- 如果队列刚被初始化,第一个参与排队的线程是头结点吗?
不是,队列初始化时,会先new一个thread为null的虚拟节点为头结点,因为头结点永远是持有锁的线程,不能让排队的线程当作头节点。- 为什么最后要判断s是否等于当前线程?
如果s等于当前线程,则说明当前尝试获取锁的线程是第一个排队的线程,方法会返回false,然后会尝试CAS获取锁,如果此时头节点刚好释放锁,则直接获取成功,不需要再进入队列。
就好比你去火车站买票,如果窗口后面第二个人是你的好朋友,你可以直接让他帮你买,刚好这时前面那个人买完票了,就轮到好朋友给你买票了。
分析完tryAcquire方法,再回到acquire,看剩下的逻辑:
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
如果tryAcquire失败,则调用acquireQueued,同时里面又调用了addWaiter方法,这个addWaiter方法就是用来把线程放到队列里的。
private Node addWaiter(Node mode) {
//new一个node,并且绑定线程为当前线程
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//如果是第一次调用,pred==null,会直接调用后面的enq方法
if (pred != null) {
node.prev = pred;
//如果不是第一次调用,利用CAS将尾节点替换为node,如果替换不成功,说明有竞争,再进入enq方法
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
//如果是第一次调用,tail为null,此时会new一个新的node设为head
//这里就是上文提到的:队列初始化时的虚拟头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//否则利用CAS将尾节点替换为node
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();
//如果前一个节点是head,这里还可以再尝试获取锁
if (p == head && tryAcquire(arg)) {
//如果获取成功则将头节点设为node
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//判断自己是否应该被挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这里看一下shouldParkAfterFailedAcquire方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
在Node中,有这样几个状态:
//表示该节点被取消
static final int CANCELLED = 1;
//表示该节点需要被唤醒
static final int SIGNAL = -1;
//表示该节点需要等待条件唤醒
static final int CONDITION = -2;
//向后传播
static final int PROPAGATE = -3;
节点在初始化的时候,waitStatus是等于0的,所以在第一次执行shouldParkAfterFailedAcquire方法是,会进入else分支,这时会用CAS将前一个节点状态设置为SIGNAL,然后返回false;
接着进入下一轮循环,同样还是获取它前一个节点,再次进入shouldParkAfterFailedAcquire,这次ws==Node.SIGNAL,返回true,
为什么这里要循环两次?
第一次是为了将前一个节点状态设为SIGNAL,第二次确认一下确实置为SIGNAL了就返回。
为什么要设置前一个节点状态,而不是自己?
前一个节点的线程当前处于挂起状态,需要后一个节点辅助。
然后执行parkAndCheckInterrupt:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
注意,这里调用的park方法,就是挂起线程用的,LockSupport其实是个工具类,真正提供挂起功能的,是Unsafe类的park方法。(Unsafe类如果不了解,自行百度吧,总之是JDK里很底层,很牛逼的一个类)
至此,加锁过程分析完了,然后我们再来看解锁过程。
unlock方法:
public void unlock() {
sync.release(1);
}
sync的release方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
进入tryRelease:
protected final boolean tryRelease(int releases) {
//当前state-1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//如果c等于0,表示当前没有线程持有锁
//因为是可重入的,所以同一个线程可能需要解锁多次才能减为0
free = true;
//将当前独占线程清空
setExclusiveOwnerThread(null);
}
//重新设置state值
setState(c);
return free;
}
tryRelease成功后,获取当前头节点,也就是正在解锁的线程,此时正常情况下h!=null成立,h.waitStatus应该为SIGNAL,不等于0,执行unparkSuccessor唤醒后面的线程:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
s被赋值为头节点的下一个节点,如果还有线程在排队,则s==null不成立,判断s.waitStatus > 0,只有状态为CANCELLED时才大于0,也就是被取消,这时需要从尾节点开始,从后往前循环,找到第一个状态小于0的节点,赋值给s。
然后,如果s不为null,说明还有可唤醒的节点,再调用unpark方法解锁,同样该方法是Unsafe类提供的。
释放锁的过程分析完毕。
然后现在还有一个问题,被挂起的线程,当被唤醒后,从哪儿开始执行?
当然是在哪儿挂起的,在哪儿恢复。它是在acquireQueued方法中,调用parkAndCheckInterrupt方法时挂起的,为了方便,再贴一次代码:
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);
}
}
parkAndCheckInterrupt执行完成后,进入下一轮循环,又进行了tryAcquire操作,此时它可以成功获取锁,因为头节点已经释放锁了,然后将头节点设为自己,如此往复。
还没完,刚才我们一直分析的都是公平锁,那么非公平锁又是怎样一个操作?
直接看NonfairSync的lock方法:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
可以看出非公平锁上来就进行一次CAS操作获取锁,如果成功则直接将当前独占线程设为自己。否则再调用acquire方法。
我们看看非公平锁的tryAcquire实现:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
里面调用的是nonfairTryAcquire:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//与公平锁不同的是,这里直接CAS设置状态,不需要调用hasQueuedPredecessors
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
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;
}
至于入队列的操作,就和公平锁一样了。
所以简单总结:公平锁是严格遵守先来后到的秩序,而非公平锁上来先插个队,如果插队不成再乖乖排队。