AQS本质是一个双向队列,线程被包装成了一个个节点,节点在队列里面通过自旋一直尝试获取资源
AQS有两个队列 一个同步队列,一个条件队列
其实我感觉这个队列框架 和重量级锁的框架很像 有entrylist 代表着正在等待资源的线程队列,是双向的,entrylist叫等待队列,然后当获得资源的线程,使用了wait以后就进入了waitset,这里面的等待线程,就要等待一定条件后,然后被唤醒,再去entrylist的头部或者尾部进行争夺资源。waitset又叫条件等待队列,条件满足了,就被唤醒去等待队列。
这些队列 本质都是双向链表
因为会有多个线程会同时竞争锁,所以搞了个 _cxq 这个单向链表基于 CAS 来 hold 住这些并发,然后另外搞一个 _EntryList 这个双向链表,来在每次唤醒的时候搬迁一些线程节点,降低 _cxq 的尾部竞争。
条件队列
条件等待队列,其实之前是获得了资源了的,但是由于条件不满足,被wait了,然后释放了资源,加入到了条件等待队列。 当条件再次满足后,会被另一个线程唤醒,加入到同步队列当中,参与资源竞争
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
当获取资源的线程,调用await方法后,它会释放资源,然后包装成条件线程节点,加入到条件 队列末端
当线程等待的条件满足后,由某个线程调用signal方法,唤醒头节点,然后头节点线程再加入到同步队列中,参与资源等待,或者说参与资源竞争, 可以有多个条件队列,不同的等待条件,不同的条件队列,一个队列里的线程,等待的条件一样
await方法
把当前线程加入到条件等待队列,支持响应中断
就是线程在等待队列中,被中断了,会立即响应中断,退出条件等待队列,不会延后处理中断。
public final void await() throws InterruptedException {
if (Thread.interrupted()) {
// 立即响应中断
throw new InterruptedException();
}
// 将当前线程添加到等待队列末尾,等待状态为 CONDITION
Node node = this.addConditionWaiter();
// 释放当前线程持有的资源
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // 如果当前结点位于条件队列中,则循环
// 阻塞当前线程
LockSupport.park(this);
// 如果线程在阻塞期间被中断,则退出循环
if ((interruptMode = this.checkInterruptWhileWaiting(node)) != 0) {
break;
}
}
// 如果在同步队列中等待期间被中断,且之前的中断状态不为 THROW_IE
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
if (node.nextWaiter != null) {
// 清除条件队列中所有状态不为 CONDITION 的结点
this.unlinkCancelledWaiters();
}
// 如果等待期间被中断,则响应中断
if (interruptMode != 0) {
this.reportInterruptAfterWait(interruptMode);
}
}
基于自旋的方式在条件队列中等待条件,直到它被放入到同步队列,或者被中断
条件线程支持两种中断REINTERRUPT和
THROW_IE
第一种是Thread#interrupt
方法中断自己,第二种是抛出异常
addConditionWaiter()把自己加入到条件队列末端,然后清楚所有不为condition状态的线程 调用unlinkCancelledWaiters
fullyRelease() 释放线程获取的资源,进入等待队列前 必须做的事情
如果资源释放失败,则上述方法会将当前线程的状态设置为 CANCELLED,以退出等待状态。
isOnSyncQueue() 用于判断线程是否在同步队列,如果线程状态为condition,则返回false
如果一个线程所等待的条件被满足,则触发条件满足的线程会将等待该条件的一个或全部线程结点从条件队列转移到同步队列,此时,这些线程将从 ConditionObject#await 方法中退出,以参与竞争资源
awaitNanos 设置一个等待超时时间,超时则加入同步队列
awaitUninterruptibly
等待期间 不响应中断
public final void awaitUninterruptibly() {
// 将当前线程添加到等待队列末尾,等待状态为 CONDITION
Node node = this.addConditionWaiter();
// 释放当前线程持有的资源
int savedState = fullyRelease(node);
boolean interrupted = false;
// 如果当前结点位于条件队列中,则循环
while (!isOnSyncQueue(node)) {
// 阻塞当前线程
LockSupport.park(this);
if (Thread.interrupted()) {
// 标识线程等待期间被中断,但不立即响应
interrupted = true;
}
}
// 自旋获取资源,返回 true 则说明等待期间被中断过
if (acquireQueued(node, savedState) || interrupted) {
// 响应中断
selfInterrupt();
}
}
这个方法,表明 线程被中断之后,不会立马中断,而是当他退出等待队列,在同步队列中,再去中断自己。
signal
该方法是把条件队列的头节点,转移到同步队列中,参与资源竞争。
public final void signal() {
// 先检测当前线程是否获取到了锁,否则不允许继续执行
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException();
}
// 获取条件队列头结点,即等待时间最长的结点
Node first = firstWaiter;
if (first != null) {
// 将头结点从条件队列转移到同步队列,参与竞争资源
this.doSignal(first);
}
}
调用singal的线程 必须持有资源,必须在临界区,也就是必须先持有独占锁isHeldExclusively就是来检测这个情况
doSignal方法,从前往后遍历找到第一个不为null的节点,然后用transferForSignal方法把他转移到同步队列的末端!!
private void doSignal(Node first) {
// 从前往后遍历,直到遇到第一个不为 null 的结点,并将其从条件队列转移到同步队列
do {
if ((firstWaiter = first.nextWaiter) == null) {
lastWaiter = null;
}
first.nextWaiter = null;
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
// 更新当前结点的等待状态:CONDITION -> 0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 更新失败,说明对应的结点上的线程已经被取消
return false;
}
/*
* Splice onto queue and try to set waitStatus of predecessor to indicate that thread is (probably) waiting.
* If cancelled or attempt to set waitStatus fails, wake up to resync (in which case the waitStatus can be transiently and harmlessly wrong).
*/
// 将结点添加到同步队列末端,并返回该结点的前驱结点
Node p = this.enq(node);
int ws = p.waitStatus;
// 如果前驱结点被取消,或者设置前驱结点的状态为 SIGNAL 失败,则唤醒当前结点上的线程 直接让该线程获得资源了
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {
LockSupport.unpark(node.thread);
}
return true;
}
transferForSignal 会找到第一个不为null的节点 尝试把他送往同步队列末端,送往之前 先用cas操作设置它的线程状态为0,如果设置失败说明线程被取消,就寻找下一个节点 再尝试
更新到同步队列的末端后,,还要把前驱节点的状态设置为singal
同步队列
同步队列就是线程等待队列,所有竞争资源失败的线程都在里面等待资源,线程以链表结点的形式进行组织,在等待期间相互独立的执行自旋,并不断轮询前驱结点的状态,如果发现前驱结点上的线程释放了资源则尝试获取
await
可中断的等待 分为唤醒前中断 和唤醒后中断 响应中断
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
long savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}acquireQueued如果获取锁成功就返回fasle,如果被中断,则返回true
interruptMode != THROW_IE 为真 则说明是唤醒后被中断
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
走到这一步 一定是唤醒后 去了同步队列被中断
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
如果是在唤醒前被中断,则应该清空它的后面节点,和同步队列断开关系
unlinkCancelledWaiters();
if (interruptMode != 0)如果发生中断 响应中断 补偿中断 reportInterruptAfterWait(interruptMode);
}
unlinkCancelledWaiters
移除同步队列。
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;
/** 对于独占模式而言,指向下一个处于 CONDITION 等待状态的结点;对于共享模式而言,则为 SHARED 结点 */
Node nextWaiter;
// ... 省略方法定义
}
节点线程的状态
- CANCELLED :表示当前线程处于取消状态,一般是因为等待超时或者被中断,处于取消状态的线程不会再参与到竞争中,并一直保持该状态。
- SIGNAL:表示当前结点后继结点上的线程正在等待被唤醒,如果当前线程释放了持有的资源或者被取消,需要唤醒后继结点上的线程。
- CONDITION :表示当前线程正在等待某个条件,当某个线程在调用了
Condition#signal
方法后,当前结点将会被从条件队列转移到同步队列中,参与竞争资源。- PROPAGATE :处于该状态的线程在释放共享资源,或接收到释放共享资源的信号时需要通知后继结点,以防止通知丢失。
当调用acquire 获取资源的时候,获取成功的线程,就成为头节点,失败就加入到同步队列的末端
同步队列的头节点 head 用于记录当前正在持有资源的线程节点,当他释放(release)资源的时候,会唤醒它的后继节点,由他获取资源
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GWwPLuTA-1617379506078)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20210331142820515.png)]
LockSupport
是一个工具类,里面有阻塞线程,和苏醒线程的方法
park,unpark方法
需要注意的一点是,如果事先针对某个线程调用了 unpark 方法,则该线程继续调用 park 方法并不会进入阻塞状态,而是会立即返回,并且 park 方法是不可重入的。
线程从阻塞状态苏醒的条件:
- 其它线程调用 unpark 方法唤醒当前线程。
- 其它线程中断了当前线程的阻塞状态。
- 方法 park 因为一些不合逻辑的原因退出。
资源的获取和释放
AQS 对应的 AbstractQueuedSynchronizer 实现类,在属性定义上主要包含 4 个字段(如下),其中 exclusiveOwnerThread 由父类 AbstractOwnableSynchronizer 继承而来,用于记录当前持有独占锁的线程对象,而 head 和 tail 字段分别指向同步队列的头结点和尾结点
字段 state 用于描述同步状态,对于不同的实现类来说具备不同的用途:
private transient Thread exclusiveOwnerThread;
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
字段 state 用于描述同步状态,对于不同的实现类来说具备不同的用途:
对于 ReentrantLock 而言,表示当前线程获取锁的重入次数。
对于 ReentrantReadWriteLock 而言,高 16 位表示获取读锁的重入次数,低 16 位表示获取写锁的重入次数。
对于 Semaphore 而言,表示当前可用的信号个数。
对于 CountDownLatch 而言,表示计数器当前的值。
AbstractQueuedSynchronizer 是一个抽象类,在方法设计上引入了模板方法设计模式,下面的代码块中列出了所有需要子类依据自身运行机制针对性实现的模板方法:
protected boolean tryAcquire(int arg)
protected boolean tryRelease(int arg)
protected int tryAcquireShared(int arg)
protected boolean tryReleaseShared(int arg)
protected boolean isHeldExclusively()
tryAcquire :尝试以独占模式获取资源,如果获取成功则返回 true,否则返回 false。
tryRelease :尝试以独占模式释放资源,如果释放成功则返回 true,否则返回 false。
tryAcquireShared :尝试以共享模式获取资源,如果返回正数则说明获取成功,且还有可用的剩余资源;如果返回 0 则说明获取成功,但是没有可用的剩余资源;如果返回负数则说明获取资源失败。
tryReleaseShared :尝试以共享模式释放资源,如果释放成功则返回 true,否则返回 false。
isHeldExclusively :判断当前线程是否正在独占资源,如果是则返回 true,否则返回 false。
独占获取资源
独占获取资源的三种方式 acquire acquireinterruptibly tryAcquireNanos
针对独占模式获取资源,AbstractQueuedSynchronizer 定义了多个版本的 acquire 方法实现,包括:
acquire、acquireInterruptibly,以及 tryAcquireNanos,其中
acquireInterruptibly 是 acquire 的中断版本,在等待获取资源期间支持响应中断请求,
tryAcquireNanos 除了支持响应中断以外,还引入了超时等待机制。
acquire
public final void acquire(int arg) {
if (!this.tryAcquire(arg) // 尝试获取资源
// 如果获取资源失败,则将当前线程加入到同步队列的末端(独占模式),并基于自旋机制等待获取资源
&& this.acquireQueued(this.addWaiter(Node.EXCLUSIVE), arg)) {
// 等待获取资源期间曾被中断过,在获取资源成功之后再响应中断
selfInterrupt(); //延迟响应中断!!!!!!!即使中断了 也必须等待线程获取资源成功后 再中断
}
}
解释:线程尝试获取独占资源,如果获取资源失败 把线程包装成等待节点,并设置独占模式,加入到同步队列的末端,
此方法不会立即响应中断,会延迟中断。
入队 第一次cas快速入队 如果快速入队失败,继续尝试入队eq方法,如果队列没初始化,则初始化再入队
addWaiter
加入同步队列末端。
private Node addWaiter(Node mode) {
// 为当前线程创建结点对象
Node node = new Node(Thread.currentThread(), mode);
// 基于 CAS 机制尝试快速添加结点到同步队列末端
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (this.compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 快速添加失败,继续尝试将该结点添加到同步队列末端,如果同步队列未被初始化则执行初始化
this.enq(node);
// 返回当前线程对应的结点对象
return node;
}
private Node enq(final Node node) {
for (; ; ) {
// 获取同步队列末尾结点
Node t = tail;
// 如果结点不存在,则初始化
if (t == null) { // Must initialize
if (this.compareAndSetHead(new Node())) {
tail = head;
}
} else {
// 往末尾追加
node.prev = t;
if (this.compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued
加入等待队列,然后自旋获取资源或者被阻塞。 有三次自旋机会。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false; // 标记自旋过程中是否被中断
// 基于自旋机制等待获取资源
for (; ; ) {
// 获取前驱结点
final Node p = node.predecessor();
// 如果前驱结点为头结点,说明当前结点是排在同步队列最前面,可以尝试获取资源
if (p == head && this.tryAcquire(arg)) {
// 获取资源成功,更新头结点
this.setHead(node); // 头结点一般记录持有资源的线程结点
p.next = null; // help GC
failed = false;
return interrupted; // 自旋过程中是否被中断
}
// 1.应该阻塞,调用parkAndCheckInterrupt阻塞线程
//2.不应该阻塞,再给一次抢锁的机会
if (shouldParkAfterFailedAcquire(p, node) // 判断是否需要阻塞当前线程
&& this.parkAndCheckInterrupt()) { // 如果需要,则进入阻塞状态,并在苏醒时检查中断状态
// 标识等待期间被中断
interrupted = true ;这里 看到 是延迟响应中断 只有获取资源成功 才会返回
}
}
} finally {
// 尝试获取资源失败,说明执行异常,取消当前结点获取资源的进程
if (failed) {
this.cancelAcquire(node);
}
}
}
如果节点上一个节点是头节点,说明该节点是最前端的节点,因此可以尝试获取资源,获取资源成功,则把他变成头节点,然后删除上一个头节点 gc, 头节点表示当前占有资源的线程,
非最前端的节点,如果没轮到自己享受资源,或者自旋获取资源失败,就会调用shouldParkAfterFailedAcquire 检测是否需要阻塞
shouldParkAfterFailedAcquire
shouldParkAfterFailedAcquire方法 判断当前节点是否需要被阻塞
1.如果前驱节点是singal状态 说明前驱节点需要唤醒后驱阻塞的节点,所以当前节点需要被阻塞 返回true
2.如果前驱节点的状态是取消状态,则把前驱节点移到当前节点的后面,再给一次自旋机会
3.如果前驱节点是0状态或者PROPAGATE,则把他们状态设置为singal,再给一次自旋机会,因为当前节点需要一个被唤醒的状态,下次自旋的时候,该节点可能就会被阻塞了。所以必须设置为singal 返回false 这一轮不阻塞,下一轮就要被阻塞了,这样就限制了自旋次数,提高性能。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱结点状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) {
// 前驱结点状态为 SIGNAL,说明当前结点需要被阻塞
return true;
}
if (ws > 0) {
// 前驱结点处于取消状态,则一直往前寻找处于等待状态的结点,并排在其后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 前驱结点的状态为 0 或 PROPAGATE,但是当前结点需要一个被唤醒的信号,
* 所以基于 CAS 将前驱结点等待状态设置为 SIGNAL,在阻塞之前,调用者需要重试以再次确认不能获取到资源。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt 该方法会调用locksuppor的park方法 阻塞线程,并检测是否被中断,线程阻塞期间被中断了的话,不会立即响应,而是记录下来,直到资源获取成功,或者发生异常退出自旋,acquireQueued 会在外围响应。
如果在自旋期间发生异常,则上述方法会执行 AbstractQueuedSynchronizer#cancelAcquire 以取消当前结点等待获取资源的进程,包括设置结点的等待状态为 CANCELLED,唤醒后继结点等。
parkAndCheckInterrupt
shouldParkAfterFailedAcquire判断应该阻塞线程,则调用parkAndCheckInterrupt,其内部调用LockSupport.park(this)阻塞当前线程。LockSupport对UNSAFE中的park、unpark进行了封装,其能精准阻塞一个线程,也能精准唤醒一个线程(不同于wait和notify)。阻塞唤醒会导致线程进行上下文切换。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
doAcquireNanos
可以设置超时时间,如果在规定时间内,可以自我唤醒,并且可以自旋,就是即使shouldParkAfterFailedAcquire判断应该阻塞了,也有可能不阻塞,而是自旋一段时间,这个自旋的时长有一个阈值spinForTimeoutThreshold = 1000L,
nanosTimeout > spinForTimeoutThreshold这个判断主要就是为了让线程超时的剩余时间小于1000纳秒,就不用阻塞,而是自旋去获取锁,这样大大提高了效率
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
//已经超时直接返回false,获取锁失败
return false;
//计算deadline
final long deadline = System.nanoTime() + nanosTimeout;
//入队列
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
//判断前驱是否是head
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
//超时返回false,获取锁失败
if (nanosTimeout <= 0L)
return false;
//1.应该阻塞,调用parkAndCheckInterrupt阻塞线程
//2.不应该阻塞,再给一次抢锁的机会
//3.如果设置的超时时长小于一个固定的自旋时间,那么就不让他进入阻塞,而是在一个很短的时间内自旋获取锁,保证可以最大自旋1000纳秒,前提是你设置的超时时间要小于等于1000纳秒!!!!!!如果你设置的超时时间大于1000纳秒,那它会先阻塞,时间到了,就会醒来,再去尝试获取锁
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
//阻塞一段时间
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
//响应中断
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
独占释放资源
release
针对独占模式释放资源,AbstractQueuedSynchronizer 定义了单一实现,即 `AbstractQueuedSynchronizer#release` 方法,该方法本质上是一个调度的过程,具体释放资源的操作交由 tryRelease 方法完成,由子类实现。方法 `AbstractQueuedSynchronizer#release` 实现如下:
如果释放资源成功 就要唤醒后驱节点 由他来继承资源
public final boolean release(int arg) {
// 尝试释放资源
if (this.tryRelease(arg)) {
Node h = head;
// 如果释放资源成功,则尝试唤醒后继结点
if (h != null && h.waitStatus != 0) {
this.unparkSuccessor(h);
}
return true;
}
return false;
}
unparkSuccessor
这个唤醒后驱节点 后驱节点不存在或者为取消状态 则从后往前遍历寻找后驱节点
private void unparkSuccessor(Node node) {
// 获取当前结点状态
int ws = node.waitStatus;
if (ws < 0) {
// 如果当前结点未被取消,则基于 CAS 更新结点等待状态为 0
compareAndSetWaitStatus(node, ws, 0);
}
/*
* Thread to unpark is held in successor, which is normally just the next node.
* But if cancelled or apparently null, traverse backwards from tail to find the actual non-cancelled successor.
*/
Node s = node.next; // 获取后继结点
// 如果后继结点为 null,或者被取消
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);
}
}
尾分叉
由于存在尾分叉 所以唤醒后继节点的时候 如果后继节点为null 就从tail开始遍历 找最近的后继节点
不加锁的双向链表的插入 不能保证线程安全
node.prev = pred;
if (this.compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
上述方法会将 node 结点的 prev 指针指向 pred 结点,而将 pred 的 next 指针指向 node 结点的过程需要建立在基于 CAS 成功将 node 设置为末端结点的基础之上,如果这一过程失败则 next 指针将会断掉,然后next指针会存在null的现象,而选择从后往前遍历则始终能够保证遍历到头结点。
所以在找后驱节点的时候,若后驱节点不存在,就需要从后往前遍历,找到离当前节点最近的后驱节点。
共享资源的获取
针对共享资源的获取 aquire也有多个实现。
acquireShared 不支持响应中断 获取资源后才中断
acquireSharedInterruptibly 可以在等待期间 响应中断
tryAcquireSharedNanos 除了响应中断,还支持超时等待
有一线程获取共享锁后唤醒后继节点(h.ws=-1--->h.ws=0),这时有另一个线程释放了共享锁(h.ws=0--->h.ws=-3)。传播(ReentrantReadWriteLock和Semaphore都可能有这种情况)
有一线程释放了共享锁(h.ws=-1--->h.ws=0)又有一线程释放了共享锁(h.ws=0--->h.ws=-3)。(Semaphore可能有这种情况,ReentrantReadWriteLock不可能,因为ReentrantReadWriteLock不是每次释放共享锁都会唤醒head后继节点,必须完全释放锁)
acquireShared
public final void acquireShared(int arg) {
// 返回负数表示获取资源失败
if (this.tryAcquireShared(arg) < 0) {
// 将当前线程添加到条件队列,基于自旋等待获取资源
this.doAcquireShared(arg);
}
}
doAcquireShared
private void doAcquireShared(int arg) {
// 将当前线程加入同步队列末端,并标记为共享模式
final Node node = this.addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false; // 标记自旋过程中是否被中断
for (; ; ) {
// 获取前驱结点
final Node p = node.predecessor();
// 如果前驱结点为头结点,说明当前结点是排在同步队列最前面,可以尝试获取资源
if (p == head) {
// 尝试获取资源
int r = this.tryAcquireShared(arg);
if (r >= 0) {
// 获取资源成功,设置自己为头结点,并尝试唤醒后继结点
this.setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted) {
selfInterrupt();
}
failed = false;
return;
}
}
// 如果还未轮到当前结点,或者获取资源失败
if (shouldParkAfterFailedAcquire(p, node) // 判断是否需要阻塞当前线程
&& this.parkAndCheckInterrupt()) { // 如果需要,则进入阻塞状态,并在苏醒时检查中断状态
// 标识等待期间被中断
interrupted = true;
}
}
} finally {
// 尝试获取资源失败,说明执行异常,取消当前结点获取资源的进程
if (failed) {
this.cancelAcquire(node);
}
}
}
共享资源获取的逻辑基本和独占获取资源一样,共享模式只是在获取资源成功后,会唤醒尝试后继节点
而独占获取,是在释放资源的时候,才唤醒后继节点。
setHeadAndPropagate
把当前节点设为头节点,并尝试唤醒后继节点
当结点上的线程成功获取到资源会触发执行该方法,以尝试唤醒后继结点
不过有条件:
1.存在剩余的可用资源
2.后继结点处于等待状态,或后继结点为空
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 记录之前的头结点
this.setHead(node); // 头结点一般记录持有资源的线程结点
/*
* 如果满足以下条件,尝试唤醒后继结点:
*
* 1. 存在剩余可用的资源;
* 2. 后继结点处于等待状态,或后继结点为空
*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause unnecessary wake-ups,
* but only when there are multiple racing acquires/releases, so most need signals now or soon anyway.
*/
if (propagate > 0 // 存在剩余可用的资源
|| h == null || h.waitStatus < 0 // 此时 h 是之前的头结点
|| (h = head) == null || h.waitStatus < 0) { // 此时 h 已经更新为当前头结点
Node s = node.nextb;
// 如果后继结点以共享模式在等待,或者后继结点未知,则尝试唤醒后继结点
if (s == null || s.isShared()) {
this.doReleaseShared();
}
}
}
共享资源释放
releaseShared
public final boolean releaseShared(int arg) {
// 尝试释放资源
if (this.tryReleaseShared(arg)) {
// 释放资源成功,唤醒后继结点
this.doReleaseShared();
return true;
}
return false;
}
doReleaseShared
AQS的设计,尽快唤醒其他等待线程体现在3个地方:
1共享锁的传播性。
2doReleaseShared()中head改变,会循环唤醒head的后继节点。
3线程获取锁失败后入队列并不会立刻阻塞,而是判断是否应该阻塞shouldParkAfterFailedAcquire,如果前继是head,会再给一次机会获取锁。
共享锁是多个线程可以共享一把锁
PROPAGATE 状态表示下一次获取共享锁应该无条件传播
释放资源后应该传播给其他节点,在调用doReleaseShared 时只为head设置PROPAGATE状态,为了确保继续传播,即使有其他操作介入。
这两种保守性的检查可能会导致不必要的唤醒,但只有在有多个竞争获取/释放时才会出现,所以大多数检查都需要现在或马上发出信号。
所以 只要释放了资源 就一定会传播释放资源这个信号 ,让更多的线程有机会获取资源
获得了资源,也要传播唤醒信号给后面的节点 虽然可能导致不必要的唤醒。
doReleaseShared 唤醒后续节点 实际利用unparkSuccessor
尝试唤醒后继节点 该方法保证后续节点可以收到唤醒通知,让后续线程更有可能的获取到资源
因为是共享资源,可以有多个线程获取资源,所以释放资源的时候,要把这个唤醒通知,通知给更多的线程。
PROPAGATE 状态表示下一次获取共享锁应该无条件传播
共享锁 要传播唤醒 以唤醒后面更多的节点 !!!!!!!! 具有唤醒传播性
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other in-progress acquires/releases.
* This proceeds in the usual way of trying to unparkSuccessor of head if it needs signal.
* But if it does not, status is set to PROPAGATE to ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added while we are doing this.
* Also, unlike other uses of unparkSuccessor, we need to know if CAS to reset status fails, if so rechecking.
*/
for (; ; ) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果头结点状态为 SIGNAL,则在唤醒后继结点之前尝试清除当前结点的状态
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
// loop to recheck cases 如果清除状态失败,跳过这个循环,再进行cas清除操作,自旋到成功
continue;
}
// 唤醒后继结点
this.unparkSuccessor(h);
}
/*
* 如果后继结点暂时不需要被唤醒,则基于 CAS 尝试将目标结点的 waitStatus 由 0 修改为 PROPAGATE,
* 以保证后续由唤醒通知到来时,能够将通知传递下去,!!
*传递唤醒通知,让后面的节点能够unpark!!!!!
/如果当前节点状态为0, 因为是共享锁,所以把唤醒通知应该传递下去,让后续节点中需要被唤醒的线程被唤醒*/
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
// loop on failed CAS
continue;
}
}
// 如果头结点未变更,则说明期间持有锁的线程未发生变化,能够走到这一步说明前面的操作已经成功完成
if (h == head) {
break;
}
// 如果头结点发生变更,则说明期间持有锁的线程发生了变化,需要重试以保证唤醒动作的成功执行
}
}
如果释放资源成功,需要依据头结点当下等待状态分别处理:
如果头结点的等待状态为 SIGNAL,则表明后继结点需要被唤醒,在执行唤醒操作之前需要清除等待状态。
如果头结点状态为 0,则表示后继结点不需要被唤醒,
此时需要将结点状态修改为 PROPAGATE,以保证后续接收到唤醒通知时能够将通知传递下去。
以便让唤醒通知传递下去,当遇到一个singal状态的节点,就会唤醒它的后驱节点!!!!!!!!1
ReentrantLock
ReentrantLock通过AQS实现了互斥锁的逻辑,核心模板代码都在AQS中,ReentrantLock中只需要实现tryAcquire和tryRelease即可。
ReentrantLock是互斥锁且可重入。
ReentrantLock实现了不可中断获取锁,可中断获取锁,限制时间获取锁。
超时获取锁的功能:
若一开始获取锁tryAcquire失败则进入AQS同步队列doAcquireNanos。
ReentrantLock获取锁有公平与非公平之分,释放锁没有。
可重入锁,独占锁,只能有一个线程获得同步状态
分为公平和非公平锁
nonfairTryAcquire
非公平的方式去获取同步状态,也叫独占锁 2次尝试cas,一次判断是否是重入
非公平的方式获取,当线程进入调用lock方法时,它准备获取独占锁。(当就是当一个新线程准备去获取同步状态时)它会直接越过同步队列,直接先去尝试获取独占锁,我称为一次cas获取,如果第一次获取失败
final void lock() {
if (compareAndSetState(0, 1)) 直接尝试cas获取 第一次cas
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); 调用nonfairTryAcquire
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { 第二次cas尝试获取
if (compareAndSetState(0, acquires)) { 不安全 所以需要cas
setExclusiveOwnerThread(current);
return true;
}
}不等于0
对于可重入锁, 这个时候检查占用锁的线程是不是就是当前线程,是的话,说明已经拿到了锁, 直接重入就行
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; 2次尝试cas,一次判断是否是重入。获取失败,说明有其他线程在用锁则加入同步队列
}
tryLock
尝试获取锁 nonfairTryAcquire 非公平的方式获取 获取不到 不会阻塞线程
公平锁的获取
公平锁的获取,会顺序的给队列里面的线程,新线程如果前驱节点存在,则进入等待队列、
不会先cas 会判断是否重入
final void lock() {
acquire(1); 调用tryAcquire 下面这个公平获取方法 会挨着挨着的给锁
}
hasQueuedPredecessors 如果有前驱节点,且当前线程又没获取到独占锁,那他就被 加入同步队列
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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");
setState(nextc);一定是安全的 因为当前线程已经获取到锁了
return true;
}
return false;
}
CountDownLatch
是一个闭锁,可以让一个线程等待其他线程执行完后,才进行,其他线程没执行完,就一直阻塞
只要每个同步状态的线程,执行完他的任务,就会把同步状态-1,当全部同步状态的线程执行完他们各自的任务的时候,就可以唤醒因为调用await方法而被阻塞的线程了。线程就会执行下去了。 同步状态间的线程互不干扰。
构造方法 传入一个资源数目,只有当资源数目为0的时候,调用await方法的线程,才会被唤醒,不然一直被阻塞。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
关键方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) 说明同步状态大于0,需要把当前线程放入同步队列,这时候,当前线程必定被阻塞,因为队列只有它一个,同步状态又不为0,所以必被s阻塞
doAcquireSharedInterruptibly(arg);
}
该方法很重要 状态为0,才会返回true,唤醒同步队列里面的线程,状态大于0,都是返回-1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
该方法是释放一个状态,当状态为0的时候才返回true,不为0则false
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1; 释放一个同步状态
if (compareAndSetState(c, nextc))
return nextc == 0;状态为0,才会返回true
}
}
该方法就是调用上面方法,tryReleaseShared释放同步状态,状态为0,才会返回true,资源大于0,返回false
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();只有状态为0,没有线程占用资源,才会唤醒同步队列里面的线程
唤醒后继节点,而后继节点当然只有调用await方法的那个线程,
return true;
}
return false;
}
public void countDown() {
sync.releaseShared(1); 释放一个同步状态,同步状态为0,说明没有其他线程了,可以会唤醒调用await方法的线程
}
CyclicBarrier
ReentrantReadWriteLock
共用一个aqs同步队列。一个state,但是可以表示两种状态,高16位记录读锁,低16位记录写锁
ReadLock和WriteLock,却共享一个同步队列控制器Sync,表面看是两把锁,实际上是一把锁,线程分为读线程和写线程,读读不互斥,读写互斥,写写互斥。既然共享一个Sync,那就是共享一个state。源码巧妙的用一个变量state表示两种锁的状态:
低16位记录写锁,高16位记录读锁。
当state=0时,读线程和写线程都不持有锁。
当state!=0,sharedCount©!=0时表示读线程持有锁。
当state!=0,exclusiveCount©!=0时表示写线程持有锁。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
/**
* 低16位写锁,高16位读锁
*/
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
//共享锁(读锁)持有锁的读线程数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
//独占锁(写锁)重入的次数 exclusiveCount==0的时候,才可以获取锁成功,不然就去同步队列把
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
NofairSync
写线程在抢锁之前,永远不会被阻塞,这就是非公平性。
final boolean writerShouldBlock() {
//写线程在抢锁之前永远不会阻塞-非公平锁
return false; // writers can always barge
}
读线程在抢锁之前,如果同步队列中第一个实质性节点存在,且是独占线程,那该读线程被阻塞。
final boolean readerShouldBlock() {
如果同步队列 第一个节点是以独占(写)模式等待 ,那么读锁被阻塞,这样也避免了写锁的饥饿现象
* 读线程抢锁的时候,如果队列第一个是实质性节点(head.next)是独占锁时阻塞
* 返回true是阻塞
*/
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
FairSync
在FairSync
,无论是写线程还是读线程,只要同步队列中第一个实质性节点不是当前线程,就阻塞,这就是公平性
public final boolean hasQueuedPredecessors() {
1只要队列中有后继节点,且后继节点不是当前准备获取锁的线程,那么当前线程必备阻塞。
2 队列为空,返回fasle 不会被阻塞。
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
读锁的获取
lock
尝试获取锁,获取锁失败 进入同步队列
public final void acquireShared(int arg) {
//tryAcquireShared 返回-1 获取锁失败,1获取锁成功
if (tryAcquireShared(arg) < 0)
//获取锁失败入同步队列
doAcquireShared(arg);
}
tryAquireShared
先尝试cas 获取锁 ,获取不到再 尝试自旋获取锁。
获取锁失败的情况:
(1)有线程持有写锁,且该线程不是当前线程,获取锁失败。
(2)写锁空闲 且 公平策略决定 读线程应当被阻塞,除了重入获取,其他获取锁失败。
(3)读锁数量达到最多,抛出异常
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//判断如果写锁被持有,且当前线程不是持有写锁的线程,那么直接返回
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//走到这一步,说明
//1.写锁空闲
//2.或者当前线程持有写锁。然后来准备获取读锁。锁 降级,写锁变成读锁。
int r = sharedCount(c);
/*
readerShouldBlock 分为公平和非公平模式
非公平模式,同步队列第一个节点是以独占模式等待,那么被阻塞。
公平模式,只要同步队列中存在等待线程,就被阻塞
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&//读线程不阻塞,且加锁次数不超过MAX_COUNT,尝试cas获取锁
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
//下面对firstReader的处理:firstReader是不会放到readHolds里的,这样,在读锁只有一个的情况下,就避免了查找readHolds。
//获取成功,如果没有线程持有锁,说明线程是第一个获取读锁的线程
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++; //第一个持有读锁的线程时当前线程,为重入
} else {
//HoldCounter 记录线程重入锁的次数
//读锁 可以多个读线程持有,所以会记录持有读锁的所有读线程和分别重入次数
//用tid记录线程,避免引用引起垃圾残留。
//每个线程都有一个记录器。
// 非 firstReader 读锁重入计数更新
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))//缓存不是该线程,则从thredlocal中读出 计数器
//把计数器设置为当前缓存
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)//计数器第一次计数的时候,保存起来
readHolds.set(rh);
rh.count++;
}
return 1;
}
//compareAndSetState(c, c + SHARED_UNIT) 失败后 自旋尝试获取锁
return fullTryAcquireShared(current);
}
HoldCounter
除了第一个获取读锁的线程外,每个线程都有这个保存器,保存线程对读锁的重入次数
底层用ThreadLocal 实现,每个线程都有一个holdcounter
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter
为了提高效率,所以又设置了缓存。它会存储最近一次获取读锁线程的锁计数。在线程争用不是特别频繁的情况下,直接读取缓存会比较高效。
Dough Lea 觉得使用 cachedHoldCounter 还是不够高效,所以又加了一层缓存记录 firstReader,记录第一个将读锁计数从 0 变成 1 的线程以及锁计数。当没有线程争用时,直接读取这两个字段会更加高效。
final int getReadHoldCount() {
// 先访问锁全局计数的读计数部分
if (getReadLockCount() == 0)
return 0;
// 再访问 firstReader
Thread current = Thread.currentThread();
if (firstReader == current)
return firstReaderHoldCount;
// 再访问最近的读线程锁计数
HoldCounter rh = cachedHoldCounter;
if (rh != null && rh.tid == LockSupport.getThreadId(current))
return rh.count;
// 无奈读 ThreadLocal 吧
int count = readHolds.get().count;
if (count == 0) readHolds.remove();
return count;
}
fullTryAcquireShared
自旋获取共享锁。获取成功返回1 获取失败返回-1;
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//进入这个方法 说明 该线程被判断为应该被阻塞,所以只能自旋获取读锁
if (exclusiveCount(c) != 0) {
//写锁有线程持有,但是持锁的线程不是当前线程,返回-1,结束自旋。然后进入同步队列
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
//判断为应该被阻塞,
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
//重入获取读锁,不应该阻塞,获取锁成功
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
//删除不持有该读锁的cachedHoldCounter
readHolds.remove();
}
}
if (rh.count == 0)
// 需要阻塞且是非重入(还未获取读锁的),获取失败
return -1;
}
}
//读线程不应该阻塞,判断state
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//以下就是cas自旋尝试获取锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
//下面就是记录重入的机制了
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
doAcquireShared
进入同步队列 自旋获取锁,自旋几次后会被阻塞。
private void doAcquireShared(int arg) {
//创建一个读节点,并入队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//如果前继节点是head,则尝试获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {
// 在ReetrantReadWriteLock中node获取锁成功只有可能是propagate > 0,所以后面新旧head判断会省略,可以暂时不用考虑。
//获取锁成功,node出队列,
//唤醒其后继共享节点的线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
/**
* p不是头结点 or 获取锁失败,判断是否应该被阻塞
* 前继节点的ws = SIGNAL 时应该被阻塞
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
tryLock
调用的是tryReadLock() 和lock 方法很像 只不过 获取失败 不会 进入同步队列,所以没有公平之分
本身一直获取读锁
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {s
int c = getState();
//判断是否有线程持有写锁
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//没有线程持有写锁or持有写锁的是当前线程,写锁-->读锁 锁降级
if (compareAndSetState(c, c + SHARED_UNIT)) {
//获取读锁成功
if (r == 0) {
//第一个读锁的线程
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//不是第一次获取读锁 但是firstReader是当前线程,重入
firstReaderHoldCount++;
} else {
//其他线程获取读锁,重入操作
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
unlock
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
//全部释放锁成功,才会唤醒后继节点。
if (tryReleaseShared(arg)) {
//读锁释放唤醒后继节点
doReleaseShared();
return true;
}
return false;
}
ReentrantReadWriteLock
中实现的tryReleaseShared
需要全部释放锁,才会返回true,才会调用doReleaseShared唤醒后继。
读锁必须全部线程都释放完后,才可以唤醒后继节点, 释放完后 ,此时写锁也可以进行抢锁了。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
/**
* 持有读锁的第一个线程是当前线程,且重入次数为1,释放锁将firstReader=null
* 否则 firstReaderHoldCount-1
*/
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
//读写都空闲了 才唤醒后面
return nextc == 0;
}
}
写锁的获取
public final void acquire(int arg) {
//若没有抢到锁,则进入等待队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//自己中断自己
selfInterrupt();
}
tryAcquire
只会尝试获取写锁一次,失败就进入同步队列,而读锁获取,会自旋尝试获取。可能自旋多次
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
//c!=,说明有读线程或者写线程持有锁
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// c != 0 and w == 0 说明 shared count != 0 肯定是读线程在拿着读锁,线程会进入等待队列
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//走到这步 说明 c!=0,w!=0,说明当前线程一定是重入获取到写锁!
//因为 c!=0 w!=0,表明写锁一定被获取着,而线程进到这里面来了,一定是重入
// Reentrant acquire
setState(c + acquires);
return true;
}
//这里说明c==0,表明没有线程获取到读锁或者写锁
/*
writerShouldBlock分为公平和非公平模式
非公平是 读线程获取线程前 永远不会被阻塞,直接cas抢锁
公平是 同步队列中,第一个实质节点不是当前线程,则被阻塞,就是只要队列有线程,那么就会被阻塞,。
*/
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false; //获取失败 直接进入同步队列,而不是自旋尝试获取
//走到这步 表示获取成功
setExclusiveOwnerThread(current);
return true;
}
tryLock
尝试获取锁,获取锁成功就返回true,失败返回false,不会进入队列操作,所以不需要考虑公平性。
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
//有线程还有锁
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
//w=0,读锁被持有,直接返回false
//w!=0 写锁持有,但不是当前线程还有
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
//没有线程持有锁,或者持有写锁的是当前线程,可能是重入锁,继续获取锁
if (!compareAndSetState(c, c + 1))
return false;
//设置当前线程为持有线程
setExclusiveOwnerThread(current);
return true;
}
unlock
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//释放锁成功后唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
读写锁总结
1ReentrantReadWriteLock读写锁基于AQS实现,读锁是共享锁,写锁是互斥锁。
2ReentrantReadWriteLock中的NonfairSync和FairSync分别实现了两种阻塞的策略,
writerShouldBlock和readerShouldBlock。
3ReentrantReadWriteLock中巧妙的用一个state变量记录两种锁的状态,低16位记录写锁,高16位记录读锁。
sharedCount(int c)==0判断线程是否持有读锁,exclusiveCount(int c)==0判断线程是否持有写锁。
4读锁无法通过state记录锁重入,需要一个工具(HoldCounter、ThreadLocalHoldCounter)专门记录持有读锁的各个线程的重入情况。写锁可以通过state记录重入次数。
5在ReadLock#lock中获取读锁时,一个线程持有写锁时还可以再获得读锁,称为锁降级,但是没有锁升级。
6读写锁都是悲观锁,在读多写少的情况下,可能会出现写线程“饿死”的情况,即写线程一直获取不到锁。
注意点
如果当前全局处于读锁状态,且等待队列中第一个等待线程想获取写锁,那么当前线程能够获取到读锁的条件为:当前线程获取了写锁,还未释放;当前线程获取了读锁,这一次只是重入读锁而已;其它情况当前线程入队尾。之所以这样处理一方面是为了效率,一方面是为了避免想获取写锁的线程饥饿,老是得不到执行的机会
final boolean readerShouldBlock() {
如果同步队列 第一个节点以独占模式等待锁 ,那么读锁被阻塞
*
* 返回true是阻塞
*/
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
例如:线程C请求一个写锁,由于当前其他两个线程拥有读锁,写锁获取失败,线程C入队列(根据规则i),
AQS初始化会创建一个空的头节点,C入队列,然后会休眠,等待其他线程释放锁唤醒。
此时线程D也来了,线程D想获取一个读锁,上面规则,队列中第一个等待线程C请求的是写锁,为避免写锁迟迟获取不到,并且线程D不是重入获取读锁,所以线程D也入队
(1)首先说一下公平锁和非公平锁的区别,
公平锁:当线程发现已经有线程在排队获取锁了,那么它必须排队,除了一种情况就是,线程已经占有锁,此次是重入,不用排队。
非公平锁:只有一种情况需排队,其他情况不用排队就可以尝试获取锁: 如果当前全局处于读锁状态,且等待队列中第一个等待线程想获取写锁,那么当前线程能够获取到读锁的条件为:
1当前线程获取了写锁,还未释放;又去获取读锁,这个时候叫做锁降级
2当前线程获取了读锁,这一次只是重入读锁而已;其它情况当前线程入队尾。重入获取读锁,可能导致写线程饿死
(2)读锁的时候 为什么要自旋获取锁
因为读锁需要使用 CAS 操作来修改底层锁的总读计数值,成功的才可以获得读锁,获取读锁的 CAS 操作失败只是意味着读锁之间存在 CAS 操作的竞争,并不意味着此刻锁被别人占据了自己不能获得。多试几次肯定可以加锁成功,这就是自旋的原因所在。同样在释放读锁的时候也有一个 CAS 操作的循环重试过程。
(3)作者设计了很多缓存 查看当前线程对读锁的重入次数 先判断是否是第一个 然后去最近缓存取,看是否是最近获得读锁的线程,如果不是 再去threadlocal本地线程中查找
final int getReadHoldCount() {
if (getReadLockCount() == 0)
return 0;
Thread current = Thread.currentThread();
if (firstReader == current)
return firstReaderHoldCount;
HoldCounter rh = cachedHoldCounter;
if (rh != null && rh.tid == getThreadId(current))
return rh.count;
int count = readHolds.get().count;
if (count == 0) readHolds.remove();
return count;
}