AQS:AbstractQueuedSynchronizer,是一个依赖状态的同步器,定义了一套多线程访问共享资源的同步器框架。对于等待队列、条件队列、独占共享等行为进行一系列抽象。
ReentrantLock是一种基于AQS框架的实现,作用类似于synchronized,是一种互斥锁,且它具有比synchronized更多的特性,支持手动加锁和解锁,支持公平锁和非公平锁。
先看ReentrantLock的简单应用:
// false为非公平锁,true为公平锁,默认是公平锁 ReentrantLock lock = new ReentrantLock(true); // 加锁 lock.lock() // 具体的业务逻辑 // 解锁 lock.unlock()
我们进入到这个ReentrantLock这个类,ReentrantLock作为AQS的应用实现,通过内部类Sync继承AbstractQueuedSynchronizer,这个类尤其重要,是整个juc抽象的核心。
// 构造器,默认创建的是公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
FairSync和NonfairSync都是Sync的子类,而Sync继承了AbstractQueuedSynchronizer,先简单来看AbstractQueuedSynchronizer中的重要属性:
// 头节点 private transient volatile Node head; // 尾节点 private transient volatile Node tail; // 状态,AQS是基于状态的同步器,用的就是这个状态值 private volatile int state; // 同步队列使用双向链表来构建,使用内部类Node创建节点 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调用了signal()方法后,该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中 static final int CONDITION = -2; // 表示下一次共享式同步状态获取将会被无条件地传播下去 static final int PROPAGATE = -3; // 节点状态 volatile int waitStatus; // 前驱节点 volatile Node prev; // 后继节点 volatile Node next; // 节点对应的线程 volatile Thread thread; // 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量 Node nextWaiter; // 空节点,用于标记共享模式 Node() { } // 用于同步队列 Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } // 用于条件队列 Node(Thread thread, int waitStatus) { this.waitStatus = waitStatus; this.thread = thread; }
}
再回到ReentrantLock中,加锁的方法为Lock,调用的具体实现为FairSync的Lock():
final void lock() { // 调用AQS中的acquire方法 acquire(1); } // AQS中的acquire方法 public final void acquire(int arg) { // 先尝试获得锁,如果不能成功,进行队列入队 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } // tryAcquire调用的是子类具体实现:FairSync.tryAcquire() protected final boolean tryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); // 获取状态值 int c = getState(); // 如果为0,说明现在没有线程获得锁,尝试加锁 if (c == 0) { // hasQueuedPrecessors是判断是否需要等待,如果不需要,对state进行cas 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"); // 这里就是可重入的表现,不需要进行cas,因为当前线程已经获取过一次,不可能有其他线程拿到当前锁。多次加锁未来就会多次解锁。 setState(nextc); return true; } // 未能成功加锁 return false; } /** * 入队 * @param mode 表示的是锁的模式:独占或者共享,用Node节点表示,在Node注释中有提到。 */ private Node addWaiter(Node mode) { // 构造一个节点,构造参数中传入当前线程和锁模式, Node node = new Node(Thread.currentThread(), mode); // 把尾节点赋值给pred,当然,此时的tail有可能是为空的,因为之前有可能还没有初始化等待队列 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) { // 在AQS的设计思想中,构建队列之前一定保证了队列初始化过。头部放一个空节点,内部的线程属性不指向任何线程。队头和队尾同时指定这个空节点,这样队列初始化完成。 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; // 将当前节点入队,当前节点就变成了队列的尾节点。此处也存在竞争,为了保证所有阻塞线程对象能被唤醒,所以要保证安全入队。 if (compareAndSetTail(t, node)) { // 之前的尾部指针的next指向新节点 t.next = node; return t; } } } } // 节点入队之后的处理。主要进行节点阻塞 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; } // 尝试获取锁失败,判断是否需要阻塞 // 在这个方法中会调用LockSupport.park(this),进行线程阻塞 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } // 获取锁失败,是否需要阻塞 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // 前驱节点的状态 int ws = pred.waitStatus; // 前驱节点的状态是SIGNAL,表示当前节点是可被唤醒的。这个地方的设计比较奇特,使用前驱节点的状态来标记唤醒当前节点。A->B->C,A节点的状态表示B能否被唤醒 if (ws == Node.SIGNAL) return true; // 前驱节点的状态大于0,我们认为出现错误,将当前节点的前驱节点,和前驱节点都进行前移,直到前驱节点的状态正常 if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 将前驱节点的状态修改为SIGNAL,下次可唤醒 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
到此,加锁的代码处理完毕。未获得锁的线程是阻塞的,如何被唤醒?当然是在unlock方法,当前获取锁的线程释放锁之后,就会去唤醒下一个线程,unlock代码跟下去:
public final boolean release(int arg) { // 调用子类的实现 if (tryRelease(arg)) { // 获取头部节点 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; if (c == 0) { free = true; // 把当前AQS独占的线程置空 setExclusiveOwnerThread(null); } setState(c); return free; } // 线程唤醒 private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) // 再次将节点状态改为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); }
因为Doug Lea将太多的逻辑集成到AQS中,这个代码深入到每一行几乎时读不下去的,重点是学习他的设计思路,多线程之间互相阻塞和唤醒。