AbstractQueuedSynchronizer(AQS)
- AbstractQueuedSynchronizer简称AQS,提供FIFO(先进先出)的队列,像之前的ReentrantLock、Semaphore、CounDownLatch等就使用到了该队列,AQS是一个抽象的类,下面先看看其子类的实现
- 我们先看关于AQS的内部部分源码:
- AQS中的常量:
- 同步对列头引用head和同步队列尾引用tail
- state:重入锁的关键之处也就在于这个state变量了,根据上面的介绍我们直到了重入锁(递归锁),就是可以对已经持有锁的代码块内再次加锁,对应的每次加锁就会使state自增1,所以在释放锁的时候就根据你加了多少次锁而进行释放了,否则锁会一直存在。
- 静态内部类Node
可以理解为这个Node其实就是放入队列中的线程对象,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; static final int CONDITION = -2; static final int PROPAGATE = -3; //当前等待状态 volatile int waitStatus; //上一节点引用 volatile Node prev; //下一节点引用 volatile Node next; //当前线程对象 volatile Thread thread; Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } 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; } }
- nextWaiter:当前等待节点的下一等待节点,如果当前节点是共享的,那么这个字段就是一个SHARED常量。就能直接的获知当前节点和下一节点是排他还是共享的。
- waitStatus:当前节点的等待状态
- 常量CANCELLED:值为1,证明当前节点被标记为取消,并不会在参与调度。
- 常量SIGNAL:值为-1,在当前节点被加入同步队列时,会将前一节点的等待状态更改为SIGNAL,保证前一节点在取消或释放锁时会唤醒当前节点,可以理解为打了个标记,证明我后面有人排队。
- 常量CONDITION:值为-2,证明当前节点在等待队列中。
- 常量PROPAGATE:值为-3,在共享模式下,证明当前的共享状态会被无条件传播下去,也就是当前节点为共享时,其接下来的所有下一节点都是共享的。
- INITIAL:值为0,初始状态
- 小结:从Node内部的属性在结合AQS持有链表的头节点引用和尾节点引用,AQS中的同步队列其实是一个带头带尾的双向链表。
- 内部类ConditionObject,这个类是干嘛的?记得我们之前提到的Condition类吗,ConditionObject就实现于Condition;该类的使用类似于Object.wait()和Object.notify(),简单的说就是让线程处于挂起状态,这一部分挂起的线程会存入Condition队列中,也就是等待队列,所以说AQS中是存在有两个队列的一个是同步队列一个就是等待队列,上面我们提到Node类的时候有一个nextWaiter属性,就是用于在等待队列中使用的,等待队列是单向链表,并且没有列头列尾指向。
- AQS中的常量:
- AQS具有两种锁模式
- 排他锁(互斥锁),需要实现AQS的tryAcquire和tryRelease方法,这里以ReentrantLock来说明AQS的排他锁实现。
- 然后我们从上面的AQS实现子类可以看出,ReetrantLook首先有一个Sync继承自AQS,之后又有FairSync和NonfairSync继承自Sync类,也就是AQS的间接子类;那首先看看Sync实现了什么
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; abstract void lock(); //非公平锁的直接实现 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //state初始化值为0,被别的线程加锁后会大于0,故c == 0,表明没有线程加锁 if (c == 0) { //通过cas操作对state进行复制,成功代表代表当前已获取锁,否则有其他线程已经获取锁 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; } protected final boolean tryRelease(int releases) { //解锁的过程中,线程是持有锁的,故无需cas操作 //加锁次数递减 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //加锁次数为0,表明当前线程已经完全释放了锁 if (c == 0) { //返回解锁完成,AQS根据此返回值唤醒锁上的等待线程 free = true; setExclusiveOwnerThread(null); } //更新state setState(c); return free; } // 其他代码省略 }
- 根据Sync的实现可以看到Sync类重写了tryRelease方法,并且自定义方法nonfairTryAcquire,上面我们提到了要实现排他锁需要实现AQS的tryRelease和tryAcquire,那Sync类直接就重写了tryRelease,那是不是说明公平和非公平都是调用的Sync的tryRelease方法,
- 首先我们直到创建ReentrantLock的时候默认无参时创建的是非公平锁,而在有参构造传入true的时候才会创建公平锁,我们先来看看实现:
根据上面的就可以看出公平锁是创建的是FairSync对象,非公平时创建的是NonfairSync的对象,也就是AQS的间接子类,然后通过sync静态常量保留其对象引用。 - 之后看一下主要实现lock方法和unlock方法:
- 看到ReentrantLock的lock方法实现,FairSync和NonfairSync都有自己的lock方法,那ReentrantLock的lock就是调用他们的lock方法了
那我们跟着看看FairSync和NonfairSync对lock方法的实现:- NonfairSync的lock方法实现过程如下:
通过cas操作将state更改为1,如果成功则证明之前没有线程获取到锁,将获取锁的线程更新为当前线程,否则执行acquire,acquire方法从属于AQS,上源码:final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
进入方法通过tryAcquire方法尝试获取锁,如果失败,则将线程以排他模式加入到队列中,如果不成功则调用selfInterrupt方法中断线程,这里我们就看到了上面说的要实现排他锁功能所要重写的方法之一tryAcquire方法,之后我们先细说下关于acquireQueued(addWaiter(Node.EXCLUSIVE), arg))这一块,先上一波源码:public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
- addWaiter()
addWaiter方法负责将当前线程包装程一个Node,而参数Node.EXCLUSIVE则说明该线程将以排他模式存放,之后拿到队尾指向,如果队尾不为空,则将当前节点放到队尾后面。否则执行enq方法通过不停尝试将当前节点放入队列中:private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //自旋将node放入队列 enq(node); return node; }
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
- acquireQueued()
之后进入acquireQueued中一探究竟:
进入acquireQueued方法后首先有两个变量failed和failed,暂且先不管,然后是一个死循环,通过获取当前Node的上一节点,如果上一节点是队列中的第一个节点就尝试获取获取锁, 如果获取到锁,将重置队列中的指向,并返回当前的当前的interrupted值,返回acquire是true就不会中断线程。而如果没有获取到锁,则会进入shouldParkAfterFailedAcquire(p, node) 方法: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); } }
- shouldParkAfterFailedAcquire()
a.进入方法后首先获取上一节点的等待状态,如果前一节点状态为SIGNAL,说明当前节点需要进行阻塞就会返回上一级调用parkAndCheckInterrupt方法对当前节点进行阻塞;private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
b.如果上一节点为CANCELLED状态,说明节点已取消不参与调度 则会重构队列将node的上一节点设置为上一节点的上一节点(有点绕口),之后返回false,借助上层for循环再次进入该方法;
c.否则会直接将上一节点设置为SIGNAL状态,之后跟着上层for循环递归进入返回true为止。 - cancelAcquire()
返回到acquireQueued中我们看到cancelAcquire方法,一般正常情况下根本不会进入该方法,因为我们看到如果当前线程获取到锁时会将failed变量改为false,那么什么情况会触发呢?我搜索了很多资料发现很多都有没有对该问题有解释,所以我猜测这几种情况可能会触发cancelAcquire的进行:
(1). 在当前节点的上一节点为空时会直接抛出异常;
(2). 在当前节点已经持有锁时,记录当前锁的个数时,state数小于零,抛出错误:“超过最大锁数”。
那我们看看出现以上问题后cancelAcquire进行了什么处理
我们看到了其实整个cancelAcquire方法都时在将当前节点移除队列,最后当条件都不满足时才会进入unparkSuccessor方法,那我们在看看unparkSuccessor方法内部如何实现的:private void cancelAcquire(Node node) { //判断node是否为空 if (node == null) return; node.thread = null; //我直接称其为上节点 Node pred = node.prev; //判断上一节点是否被取消,如果被取消则往上追溯一直到未被取消为止 while (pred.waitStatus > 0) node.prev = pred = pred.prev; //获取到某个上节点的下节点。。 Node predNext = pred.next; //将当前节点的状态更改为取消状态 node.waitStatus = Node.CANCELLED; //如果当前节点是个最后一个节点,就将最后一个节点更改为上节点 if (node == tail && compareAndSetTail(node, pred)) { //如果成功将上节点的next指向改为null compareAndSetNext(pred, predNext, null); } else { int ws; /* * * 否则判断上节点是否是头节点,如果不是则判断上节点的等待状态是否为SIGNAL , * 之后为了保证上节点没有每其他线程所操作,对上节点等待状态再设置为SIGNAL, * 最后再判断上节点中的线程对象不为空。 */ if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { //以上操作都成立后将当前节点的后一节点 Node next = node.next; //判断后一节点不为空且没有取消 if (next != null && next.waitStatus <= 0) //将后一节点设置为上节点的下一节点——维护队列 compareAndSetNext(pred, predNext, next); } else { /* * *那剩下的情况就是: * 1.其上节点本身就是头节点 * 2.其上节点不为SIGNAL状态 * 3.CAS设置上节点等待状态为SIGNAL时失败 * 4.上节点线程因为为null */ unparkSuccessor(node); } node.next = node; // help GC } }
讲了那么多其实unparkSuccessor的作用就是唤醒一个处于等待的线程,让他去获取锁.(我理解的时候绕进去了,我正向的在理一遍)private void unparkSuccessor(Node node) { //获取当前节点等待状态 int ws = node.waitStatus; //当等待状态小于0时,通过cas将等待状态设置为0 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); //获取当前节点的下一节点s Node s = node.next; //当s为null或s节点的等待状态为取消时进入 if (s == null || s.waitStatus > 0) { s = null; /** * 从队列尾部遍历获取不为空不为当前节点且等待状态不得为取消的。 * 那为什么从底部遍历呢? * 前面我们提到过的enq方法,大家可以往上翻翻看也就是当第一次没 * 有加入队列时,会不断的循环试图将当前节点加入到队列中,而当队 * 列中存在节点时,首先将当前节点的 prev引用当前的队尾节点,然后 * 再设置当前节点为队尾节点,当设置当前节点为队尾后,才会将之前 * 的队尾节点的next指向当前节点,但时在设置next指向之前,可能另 * 外一个线程对队列进行操作,如果通过next去取值就可能取到空值, * 并且在cancelAcquire方法的最后还将当前节点的next指向了自己, * 如果通过next遍历那不成了死循环了 */ for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) //唤醒该节点 LockSupport.unpark(s.thread); }
a.首先当前节点状态设置为初始状态;
b.获取当前节点的下一个节点,如果该节点不为空,且等待状态非取消,那么唤醒该节点
c.从后往前遍历,为什么从后往前前面已经说过了,每次等待状态不为取消的,都进行赋值,一直到拿到的节点等于当前节点或者为null,说明最后一个拿到的节点其实就是当前节点的下一非取消节点,然后对该节点进行唤醒就OK了。
- shouldParkAfterFailedAcquire()
- tryAcquire()
之后我们直接进入tryAcquire方法,发现try方法直接调用的就是nonfairTryAcquire方法,这就印证了Sync自定义方法就是用于非公平锁直接调用的
那我们就看看nonfairTryAcquire方法中的实现:
可以看到进入方法后首先获取当前线程和当前锁的一个计数值,当c等于0时证明当前没有持有锁的线程,通过cas操作让线程持有锁,如果持有成功则返回,否则说明有其他线程获取了锁。如果c是大于0的,并且持有锁的线程是当前线程,那对计数值执行加1的操作。这就是非公平锁的lock方法的实现过程了final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { 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; }
- addWaiter()
- FairSync的lock方法实现过程如下:
公平锁实现就会要直接一点了,因为是公平锁所以讲究FIFO原则,所以不会事先尝试锁,而是先去队列中是否有其他节点:
是不是感觉跟非公平锁一样,嘿嘿!那我们不多说直接看那里不一样了,进入tryAcquire方法,因为我们直到实现排他锁是需要重写这个方法的,非公平锁的直接调用了Sync自定义的nonfairTryAcquire方法,那公平锁就只能自己实现了:
第一眼看上去没什么不一样,不对多了个hasQueuedPredecessors方法,这是个什么玩意?
进入hasQueuedPredecessors,我们首先看到他获取了队列头和队列尾的Node对象:public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
a. 如果队列头和队列头相等,就直接返回false,此时要么队列中只有一个节点,要么就都为null,否则进入下一步;
b. 之后判断头节点的下一节点是否为null,如果为null直接返回ture,为什么会出现头节点和尾节点不相等,但是头节点的next又为null呢,因为可能此时另外一个线程正在进行第一次入队操作,入队时会先更新head=new Node(),而tail还未进行设置,所以二者不相等,而head又没有下节点所以为null,所以当前线程就不能去获取锁了,直接尝试锁失败;
c. 最后则判断当前队列的第二个节点的线程是否等于当前线程,如果相等则说明当前节点是早已入队的节点( 因为当前节点是处于队列中的第二个节点,也就就是说head节点很可能会释放锁,而未被阻塞的第二节点就需要不停的尝试获取锁):这也就是公平锁的原因了,掐灭了是个线程都能随便去尝试获取锁的功能。
- NonfairSync的lock方法实现过程如下:
- unlock方法的实现
直接调用了sync类的release方法,那我们就直接看release方法的内部代码了
这里直接调用了们提到的要实现排他模式的两个方法之一tryRelease,那我们还是先看看tryRelease内部做了那些处理public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
a.进入方法后我们看到先获取到持有锁的线程持有锁的数量减去要解锁的数量,之后判断当前线程是否是持有锁的线程;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; setExclusiveOwnerThread(null); } setState(c); return free; }
b.然后设置变量free为false,只有当前线程持有锁的数量为0时才将free改为true,将持有锁线程的引用置为null进行返回
c.否则就更新锁数量,直接返回free。
那我们回到release方法中,也就是说只有锁释放完才会进入if判断,在释放完成之后会获取到当前队列的头节点,如果节点不为空且等待状态非0,则调用unparkSuccessor唤醒下一个非取消节点。
- 看到ReentrantLock的lock方法实现,FairSync和NonfairSync都有自己的lock方法,那ReentrantLock的lock就是调用他们的lock方法了
- 首先我们直到创建ReentrantLock的时候默认无参时创建的是非公平锁,而在有参构造传入true的时候才会创建公平锁,我们先来看看实现:
- 排他锁(互斥锁),需要实现AQS的tryAcquire和tryRelease方法,这里以ReentrantLock来说明AQS的排他锁实现。