重入锁的实现一个比较复杂的过程,涉及多个类和方法。而这还只是AQS的一小部分,要真正理解JUC体系 ,我们必须先梳理清楚AQS的问题。 AQS, Abstract Queued Synchronizer ,即抽象队列同步器。JUC里提供的线程工具中,很大一部分都基于AQS来实现的,甚至可以说AQS是整个JUC体系的根基。
1 如何设计一把锁
如果我们要自己设计一个锁,应该考虑哪些问题呢?
一个完整的锁,要保证多个线程访问时能正常访问和处理临界资源,如下图所示,此时至少应该满足如下条件:
1.满足互斥功能,也即竞争同一个共享变量时能保证安全的访问。
2.竞争失败的线程应该阻塞,并且能在资源被释放之后能被唤醒。这也要求,如果有N个线程被阻塞,则应该有一个容器来管理这些线程。
3.不同线程之间能够调整优先级(也称为公平非公平锁问题)。
4.要能满足重入的要求。
如果只有两三个线程 ,上面大部分操作还是比较好实现的,关键是多个线程同时竞争时该如何实现上述功能呢?我们该用哪种数据结构才能方便的满足该要求呢?答案是带头结点的双向链表:
在双链表中,我们定义了头结点head和tail用来快速访问首尾元素。获得资源的就是head指向的结点,执行完成后就可以被释放,对应的链表操作就是删除thread1对应的结点。如果有线程被阻塞,就将其打包成一个node连接到tail指向的位置上去。这样如果竞争线程比较多,就可以都连到队列上进行等待,这就实现了阻塞并等待的功能。
那如何实现优先级策略呢?假如某个新来的线程优先级高,要马上执行,此时我们只要将其连接到head位置就可以了,在上图对应的就是thread1执行完成之后马上执行这个优先级高的线程。(注意,这里是已经抢占资源的要先执行完,而不是直接中断正在执行的线程。)
那如何实现重入呢?这个更简单,我们只要在临界资源上增加一个state字段即可。如果当前资源空闲,那么state=0,否则就代表资源被某个线程抢占了还没释放。如果是重入,则继续将state的值增加即可,当然临界资源还会记录自己被哪个线程占用了。
这样,我们就大致设计了一个锁的基本结构,里面还有很多问题要进一步探讨,例如每个结点是如何等待的,如何唤醒的等等。接下来我们就详细看AQS是如何做的。
2 AQS的基本工作过程
2.1 AQS 基本结构
AQS是JUC的核心,无论是信号量还是可重入锁,背后都有AQS的影子。这些类的同步过程一般如下:
tryAcquire和tryRelease过程很好理解,就是CAS地修改AQS的state值,关键是doAcquire和doRelease如何管理众多线程的状态,又如何决定哪个线程可以获得锁。答案就是,AQS在其内部管理了一个链表,所有的线程都会被添加到这个链表中进行管理,这个链表在公平实现中又具有先进先出特性(非公平实现当然就是优先队列),这种思想也叫做CLH(三个人的姓名简称),而AQS在实现过程中体现了该思想,因此该队列也被称为CLH队列。
首先回顾一下doAcquireInterruptibly方法的代码:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
...
}
可以看到,在方法最开始,首先调用了addWaiter方法,这个方法就是将当前线程添加到等待队列的过程,对链表都所了解的都能轻易看懂:
private Node addWaiter(Node mode) {
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;
}
可见AQS是将线程包装成Node再入队的。Node是AQS里的一个内部类。从addWaiter可以看到,CLH队列的入队实际上就是把当前节点设置为队尾,那么相应的,出队就是处理队首节点。
在这里还有对前驱节点、后继节点的引用。前驱结点主要用在取消等待的场景:当前节点出队后,需要把后继结点连接到前驱结点后面;后继结点的作用是避免竞争,在doRelease方法中,当前节点出队后,会让自己的后继结点接替自己获取锁,有了明确的继承关系,就不会出现竞争了。
由此可见AQS是通过队列操作实现任务的管理,而且更方便:
-
同步操作只涉及头节点、尾节点、当前节点、前驱结点、后继结点,对整个队列影响很小,明显优于整体加锁或者分段加锁。
-
通过后继结点机制,明确了获得锁的顺序(除非出现信号量申请许可过多无法获取锁这种情况,此时只需要传播状态,继续向下寻找合适节点继承锁即可),避免了竞争。
2.2 核心结点Node
既然任务是通过队列来管理的,那每个任务自然就是队列中的一个个结点,那这些结点又是如何工作的呢?
AbstractQueuedSynchronizer类中的内容非常多,我们首先注意到内部有两个静态内部类Node和ConditionObject,这两个分别构造出了同步队列和条件队列。条件队列的问题我们后面再看,这里先看同步相关问题。
同步队列的基本结构如下,每个结点对应的就是一个被阻塞的线程,每个结点能够实现自旋等待的功能。
上一节说,在同步队列中,如果当前线程获取资源失败,就会通过addWaiter()方法将当前线程放入队列的尾部,并且保持自选等待的状态,不断判断自己所在的结点是否是队列的头结点。如果自己所在的节点是头结点,那么就会不断尝试获取资源,如果成功,则通过acquire()方法退出同步队列,并在CPU里执行。以doAcquireInterruptibly为例,parkAndCheckInterrupt方法使用LockSupport令当前线程阻塞,直到收到信号被唤醒后,进入下一轮自旋:
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);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
那每个任务被封装成的结点,也就 静态内部类Node又是什么呢?Node的定义如下:
static final class Node {
// 共享模式
static final Node SHARED = new Node();
// 独占(排它)模式
static final Node EXCLUSIVE = null;
// 线程已被取消,当首节点释放锁后,
// 开始查找下一个 waitStatus < 0 的节点,
// 如果遇到已取消的线程,则移除
static final int CANCELLED = 1;
// 当前线程的后继线程需要被unpark(唤醒)
// 后继节点处于等待状态,当前节点(为-1)被取消或者中断时会通知后继节点,
// 使后继节点的线程得以运行
static final int SIGNAL = -1;
// 当前节点处于等待队列,节点线程等待在Condition上,
// 当其他线程对condition执行signall方法时,
// 等待队列转移到同步队列,加入到对同步状态的获取
static final int CONDITION = -2;
// 与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态
static final int PROPAGATE = -3;
// 当前节点的状态,默认是 0
volatile int waitStatus;
//双向链表
volatile Node prev;
volatile Node next;
// 等待获取锁而自旋的线程
volatile Thread thread;
// Node既可以作为同步队列节点使用,也可以作为Condition的等待队列节点使用(将会在后面讲Condition时讲到)。
// 在作为同步队列节点时,nextWaiter可能有两个值:EXCLUSIVE、SHARED标识当前节点是独占模式还是共享模式;
// 在作为等待队列节点使用时,nextWaiter保存后继节点。
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
}
从上面可以看到Node类的几个关键定义:
第一个是:Node类型的 prev和next说明该结构是一个双向链表,prev和next分别是前驱和后继结点。
第二个是:Node 类型的SHARED和EXCLUSIVE表示共享还是独占类型的结点。
第三个是:定义了四个常量CANCELLED、SIGNAL、CONDITION和PROPAGATE四个表示状态的常量。
-
CANCELLED:表示当前节点中的线程已经被取消。
-
SIGNAL:表示后继结点中的线程处于等待状态。
-
CONDITION:表示当前结点的线程在等待某个条件,也就是当前结点处于Condition队列中。
-
PROPAGATE:表示当前场景下能够执行后续的acquireShared操作。
-
在默认情况下,waitStatus的取值为0,表示当前节点在sync队列中,等待获取锁。
第四个是:Node类中存在一个volatile类型的成员变量waitStatus,其取值就是上面的几个常量值。
AQS的实现类有好几种 ,这些类与AQS搭配能够实现很多强大的功能,例如:
后面我们就分专题详细分析。
从本小节的分析可以看到,AQS本身是通过队列来管理线程,而每个结点在等待机制上是“自旋+阻塞+唤醒”的机制,相比于纯自旋,这种方式能够很好地兼顾性能与效率。