目录
AQS是AbstractQueuedSynchronizer的简称,它是java并发包很重要的一个工具类,像比较常见的ReentrantLock、CountDownLatch等都是在AQS的基础上建立的。本文将从ReentrantLock的源码开始分析AbstractQueuedSynchronizer和ReentrantLock的工作原理。
AbstractQueuedSynchronizer
我们先来看下AbstractQueuedSynchronizer这个类里面都有些啥东西
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
static final class Node {
/** 标记节点处于共享模式 */
static final Node SHARED = new Node();
/** 标记节点处于独占模式 */
static final Node EXCLUSIVE = null;
/** waitStatus 为1时,表明线程取消了获取锁 */
static final int CANCELLED = 1;
/** waitStatus 为-1时,表明当前节点的下一个节点对应的线程需要被唤醒 */
static final int SIGNAL = -1;
/** waitStatus 为-2时在condition的时候才使用,表名节点在等待某种条件*/
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
//取值为上面的1,-1,-2,-3和0
volatile int waitStatus;
//前一个节点
volatile Node prev;
//下一个节点
volatile Node next;
//当前节点代表的线程
volatile Thread thread;
//condition条件队列中的下一个节点
Node nextWaiter;
......
}
// 链表头节点,头节点属于当前持有锁的那个线程
private transient volatile Node head;
//链表尾节点,每个新的节点过来,都添加到链表的最后,成为尾节点
private transient volatile Node tail;
//关键性属性,0代表没有线程持有锁,大于0代表已经有线程持有了当前锁
//谁能把这个值修改为1,谁就算是持有了锁。
//当然锁重入时,这个值会加1,也就是说这个值可以大于1
private volatile int state;
// 继承自父类,表示当前持有锁的那个线程
private transient Thread exclusiveOwnerThread;
......
}
从AbstractQueuedSynchronizer类中的字段属性可以看出AQS队列的结构图如下:
AQS有四个重要的属性:head、tail、state、exclusiveOwnerThread。其本质上是一个双向链表,从head头节点到tail尾节点,链表中每个节点都是一个Node对象,每个Node对象有四个比较重要的属性:prev、next、waitstatus、thread。每个线程来了之后new node()添加到链表的最后面。
ReentrantLock
先来看一下ReentrantLock的构造方法
public ReentrantLock() {
// 默认无参构造,将sync对象初始化为非公平锁的实现
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
// 传入参数指定使用非公平锁实现 或者 公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
// sync 是啥?sync类继承自AQS,ReentrantLock的加锁和释放都是通过sync来实现的
abstract static class Sync extends AbstractQueuedSynchronizer {}
lock
我们平常在使用ReentrantLock的时候,一般都是先执行lock.lock()加锁,然后在finally中执行lock.unlock()方法解锁。
而ReentrantLock分公平锁和非公平锁。我们先来看一下公平锁的lock()方法
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}
// AQS中的方法
public final void acquire(int arg) {
//先执行tryAcquire(1),尝试直接获取锁,如果返回成功就结束了
//如果执行失败,acquireQueued将当前线程park挂起,进入阻塞队列排队等待
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 尝试获取锁,锁重入或者没有线程在等待或者是第一个节点可以成功获取到锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//没有线程获取到锁的时候为0
int c = getState();
if (c == 0) {
//hasQueuedPredecessors,由于是公平锁的实现,要先看看有没有线程在队列中等待,
//没有人在前面才去获取锁
if (!hasQueuedPredecessors() &&
//如果没有线程在等待,使用一次CAS尝试获取锁,获取失败说明被别的线程抢到了锁
compareAndSetState(0, acquires)) {
// 如果获取到锁,将exclusiveOwnerThread设置为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程就是持有锁的线程,说明是锁重入的场景
else if (current == getExclusiveOwnerThread()) {
//由于是重入锁,那么这个时候不需要考虑并发安全问题,直接对state进行+1赋值就好了
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
tryAcquire尝试获取锁成功lock方法就返回了,如果tryAcquire失败,那么就要执行acquireQueued将线程加入到队列中
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// 先看看addWaiter(Node.EXCLUSIVE)是用来干嘛的
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 用node节点封装线程加入到队列中
private Node addWaiter(Node mode) {
// mode 传入的是Node.EXCLUSIVE,代表当前处于独占锁模式,通过构造方法传入当前线程
Node node = new Node(Thread.currentThread(), mode);
// 下面的代码尝试将当前node添加到阻塞队列的最后
Node pred = tail;
// tail尾节点不为空,代表队列不是空的
if (pred != null) {
// 将tail节点设置为当前节点的 prev节点
node.prev = pred;
//使用一次CAS尝试将当前节点设置为尾节点(tail)
if (compareAndSetTail(pred, node)) {
// CAS成功,将之前尾节点的next指向当前节点,这样就构成了双向链表。然后返回
pred.next = node;
return node;
}
}
// 如果队列为空,或者有竞争导致的CAS入队失败,那么执行enq入队操作
enq(node);
return node;
}
// 进行入队操作,有必要的话对head进行初始化
private Node enq(final Node node) {
//使用CAS自旋插入队列的最后面,即设置当前线程为tail
for (;;) {
Node t = tail;
// 如果队列是空的
if (t == null) { // Must initialize
//对head进行初始化,这个时候head节点的waitStatus还是0
if (compareAndSetHead(new Node()))
//head初始化好了,将tail指向head
tail = head;
} else {// 将当前node添加到阻塞队列的最后
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
//只有入队成功,这个方法才会return
return t;
}
}
}
}
// 再回到acquireQueued方法,此时代表当前线程的node节点已经添加到队列中,
//接下来需要进行线程挂起,正常流程,这个方法应该返回false
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//死循环/自旋获取锁
for (;;) {
// 获取当前node的前一个节点
final Node p = node.predecessor();
//如果p == head,即前一个节点是head,说明当前节点在阻塞队列中排在第一个,
//head通常是当前持有锁的线程,但如果head是刚刚初始化才有的,
//那说明当前head没有不属于任何线程,那么这个时候可以尝试去获取一下锁
//或者说head节点释放了锁,那么这个时候也是有机会直接获取到锁的
if (p == head && tryAcquire(arg)) {
//将当前线程设置为head
setHead(node);
p.next = null; // help GC
failed = false;
//成功获取到锁直接返回
return interrupted;
}
//如果当前node不是阻塞队列的第一个节点 或者尝试获取锁失败,会执行下面的逻辑
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//判断当前线程没有获取到锁是否需要park挂起,注意:这个方法是在循环当中的,返回false,下次循环还会进来
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取当前节点 前一个节点的状态
int ws = pred.waitStatus;
//如果前一个节点的状态为-1,说明前节点正常,当前线程需要被挂起
//当前线程的唤醒依赖于前节点
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
//如果前一个节点的状态大于0,说明前一个节点取消了排队
//这个时候往前遍历,直到找到一个正常的前节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {//如果前节点的状态不是1和-1,那么状态可能为-2,-3,0
//正常情况下,每个线程进来new node,它的状态都是0,
//这个时候需要把它前一个节点状态设置为SIGNAL(-1),这样才可能从第一个分支返回
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
通过shouldParkAfterFailedAcquire来判断当前线程是否需要被挂起,如果返回false说明当前线程不需要被挂起,下次循环还会再次判断。只有在前一个节点状态正常的情况下,才会返回true,代表当前线程需要被挂起。
当前线程的唤醒操作是由它的前节点来完成的,当前线程挂起后,需要等待它的前节点来将它唤醒。继续回到前面的判断条件
// 继续回到前面的这个判断条件
if (shouldParkAfterFailedAcquire(p, node) &&
//如果当前线程需要被挂起,执行parkAndCheckInterrupt()
parkAndCheckInterrupt())
interrupted = true;
//使用LockSupport.park挂起当前线程
private final boolean parkAndCheckInterrupt() {
//当前线程挂起后,就暂停在这里额,等待被唤醒
LockSupport.park(this);
return Thread.interrupted();
}
到这里,没有获取到锁的线程就挂在这里了,等待被唤醒。接下来看一下它的unlock方法
unlock
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//执行的是tryRelease(1)
if (tryRelease(arg)) {
Node h = head;
//如果头节点不为空并且头节点状态不为0
if (h != null && h.waitStatus != 0)
//这个方法会唤醒head节点的下一个节点,也就是当前节点的下一个节点
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;
//如果c==0,说明锁已经完全释放了,否则还不能释放锁。
//因为可重入锁场景下,每次重入state都会+1。state要减到0才算是完全释放锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//这个方法就是在唤醒head的下一个节点
private void unparkSuccessor(Node node) {
//获得head的状态
int ws = node.waitStatus;
//如果head的waitStatus小于0,那么这个时候把它置为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//取出head的下一个节点
Node s = node.next;
//本来是要唤醒head的下一个节点,但是下一个节点可能已经取消了等待(waitStatus=1),
//所以需要找到阻塞队列(不包括head)中waitStatus<0的第一个节点
if (s == null || s.waitStatus > 0) {
s = null;
//从tail开始往前遍历,直到找出waitStatus<0排在等待队列最前面的那个节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//就是在这里唤醒的线程
LockSupport.unpark(s.thread);
}
之前没抢到锁被挂起的线程,被唤醒后继续执行。继续尝试获取锁
//使用LockSupport.park挂起当前线程
private final boolean parkAndCheckInterrupt() {
//当前线程挂起后,就暂停在这里额,等待被唤醒
LockSupport.park(this);
//唤醒后继续执行
return Thread.interrupted();
}
公平锁和非公平锁的区别
// 公平锁
static final class FairSync extends Sync {
final void lock() {
//公平锁这里是直接acquire
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//公平锁在tryAquire这里需要判断队列中是否有线程在等待,有就不会竞争锁,在后面等待。
//而非公平锁不判断是否有线程等待 ,直接竞争锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
......
}
}
// 非公平锁
static final class NonfairSync extends Sync {
// 非公平锁在lock方法中会先执行一次CAS尝试加锁,成功就直接返回
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 非公平锁这里不判断是否有线程等待
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
......
}
}
从代码中来看,公平锁和非公平锁有两处不同,一个是在lock()方法中,一个是在tryAcquire()中。
公平锁遵循先到先得,指的是先来的线程先获得锁。我们之所以说非公平锁相对于公平锁性能更好,就是因为公平锁多了些排队等候的操作。所以非公平锁的吞吐量要比公平锁高。但是非公平锁可能导致队列中的线程迟迟获取不到资源,造成线程饥饿。
总结
最后总结一下从加锁到释放锁的整个流程:
- 首先执行lock.lock(),加锁的操作其实就是对state进行+1,对应的解锁操作就是-1。
- tryAquire()获取锁成功直接就返回了,如果失败,将当前线程node放入阻塞队列的尾部,进入队列后,使用自璇的方式尝试获取锁,当然只有一个线程能够获取到锁。其他线程都通过park挂起。获取不到锁的线程都会被挂起。 等待被前一个节点唤醒。
- 接下来持有锁的线程执行unlock,释放锁的同时唤醒它的下一个节点。让它来持有锁。
所以AQS队列就是用来管理这些竞争线程的,所有的线程都在AQS链表中排好队,获取不到锁的线程通过park挂起,阻塞在那里。等待被前一个节点唤醒。