前言
本文直接进行源码解释,源码中运用了大量的短路判断逻辑,以下是AQS的几种步骤:
六个操作:
- 抢锁
- 释放锁
- 入队
- 出队
- 阻塞
- 唤醒
先列出队列节点的各个字段:
CANCELLED:在拿到锁(acquire)之前就取消了。
SIGNAL:下一个节点正在等待锁。
CONDITION:当前节点是条件队列的节点。
waitStatus字段初始化为0,在条件队列中初始化为CONDITION,要用CAS来原子性的改动。
此外还有前驱指针和后继指针来维护双向队列,以及表明当前线程的字段。
nextWaiter字段 在条件队列中,指向下一个等待的节点,在非条件队列中会为SHARED值。
ReentrantLock下的过程:
以及下面它的公平实现和不公平实现。
现在我们从reentractlock的lock方法来出发,当调用lock方法时,调用的是Sync的lock方法,但是这个方法是抽象的,由它的子类NonfairSync和FairSync来实现,默认情况下是非公平的,当传参为true时,调用带参的公平构造。
来看看非公平的lock方法:
先通过一个CAS操作去尝试将state从0变成1,也就是去尝试获取锁,修改成功后,将当前持有锁的线程信息中加入自己的信息。
如果修改失败,证明现在正在有线程持有锁,就会进入下面的acquire方法,这就进入了AQS里面:
在进入方法后,会先去尝试获取锁,执行tryAcquire方法,那么我们先来看看非公平的tryAcquire:
可以看到,在获取到线程和state信息后,判断state是否为0(也就是现在没有线程拥有锁),会让线程去尝试CAS操作获取锁,如果获取成功后,就会设置当前线程信息为本线程,否则将会到else if判断此线程是否为已经获取到锁的线程,如果是的话,那就是重入操作了,重入时,state状态会加一然后再去setState修改state值。
如果都不满足上述条件,也就是说没有抢到锁也不是重入线程,就会return false:
回到这里,当返回false时,不短路,然后就要进行addWaiter入队列的操作了:
这里,进入后,创建线程所对应的节点(封装),两个参数,其一是当前线程,其二是模式,这里用的就是EXCLUSIVE排他(独占)模式,
如果队列没有被初始化,进入上图的enq方法,进行初始化(第一个加入队列的负责初始化,创建头结点和自身节点),后续再来的线程会进行入队列操作,上面的if(pred != null)的判断也是入队列操作,enq也是入队列的操作,只不过上述效率较快,不用再去进入enq判断,直接改指针即可。
我们也可以发现,enq执行完后返回当前节点的前驱节点(node.prve = t),addWaiter返回的是当前节点。
在此之后,就要针对当前节点去执行acquireQueued方法来尝试入队列。
进入方法后,取当前节点的前驱节点,这里又用到了短路判断,判断如果是头结点的话,就会去tryAcquire,再次尝试去获取锁,(这里也就是说,我构建了队列后可以再去看看是不是可以拿到锁了,因为在此期间其他线程可能已经释放了锁),如果没有拿到锁,就会进入shouldParkAfterFailedAcquire方法:
首先会去拿到前驱节点的waitStatus等待状态,判断如果waitStatus是-1时,返回true,返回后,这里又用到了短路,会去执行parkAndInterrupt方法来挂起当前线程,线程被唤醒时,会继续向下执行,也就是继续循环判断自己为头结点的话就去征用锁。
但是waitstatus起始值都是0,一直往下判断,到else块里,会强制的CAS把watistatus设置成SIGNAL,也就是-1.
也就是说,当你初始化队列阶段,会有两次额外争取锁的机会。
unlock:
调用unlock方法时,会调用sync的release方法:
进入release方法:
它会先调用tryRelease方法:
先拿到state并且减去release(1),然后判断当前线程和持有锁的线程是否为同一个线程,不是则抛异常,线程匹配上之后,判断当c为0后,会释放锁,并且把当前持有锁的线程置为null。
然后返回free字段(如果是重入,c不为0,free字段就不为true,只会将重入等级降低)。
再回到我们的release方法中:
如果释放锁成功,就会进入if语句,首先会拿到头结点,内部判断头结点是否存在,并且头结点的waitStatus不为零的话(代表头节点后有节点在等待),就会执行unparkSuccessor方法,将要唤醒的节点的前驱节点作为参数传入(基本上都是将头节点传入,因为只有头节点之后的节点才能去申请锁)。
unparkSuccessor方法:
首先判断节点的waitstatus是不是小于零(-1),小于则设置成0,然后拿到next节点(也就是在等待的节点),将其取出,执行unpark方法,进行唤醒。
我们之前讲到,第一个节点被阻塞到了队列里,此时被唤醒后,将会去执行抢锁,抢锁成功就去执行,失败就继续park阻塞。
这里做一个小结:
当A线程正在持有锁,B线程过来,尝试获取锁失败,就会尝试去初始化一个队列,此时会有一次再申请锁的机会,如果还是失败,则去初始化队列,将自己封装成队列中的节点,在入队列时生成两个节点,一个头结点和自身节点,因为在初始化队列的过程中很有可能A已经释放锁了,所以B还有一次获取锁的机会,此时如果获取失败,就会park阻塞,当A进行release操作释放锁时,会去unpark来唤醒头结点的后继结点,也就是B节点,此时,B会去抢锁,但是,如果在此时有另一个C线程刚到这里,没有进队,他就会去抢锁,C抢锁成功,则B又park阻塞,C抢锁失败,会入队然后阻塞,B抢锁成功的话,就会出队并且头结点后指。
Fair公平锁下:
在tryAcquire方法下比非公平锁多了一个判断,判断前面是否在头部,返回true就代表不在头部,前面有等待节点,由此来保证公平。
总结:公平锁和非公平锁只有两处不同:
- 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在 CAS 失败后,和公平锁一样都会进入到
tryAcquire
方法,在tryAcquire
方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
CountDownLatch下AQS:
先来看看最重要的两个方法:
await阻塞,countDown计数器减一。
CountDownLatch是共享模式(node节点为SHARED,ReentrantLock的是EXECLUSIVE)。
现从await方法出发:
进入其中的acquireSharedInterruptibly方法,
这个方法是支持中断的,会对其他线程发出的中断做出响应。
总结:
CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用 countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state == 0,如果 state == 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行。