「这是我参与2022首次更文挑战的第28天,活动详情查看:2022首次更文挑战」。
详细介绍了AQS中的同步状态的共享式获取、释放的原理,以及独占锁和共享锁的简单实现。
AQS相关文章:
AQS(AbstractQueuedSynchronizer)源码深度解析(1)—AQS的设计与总体结构
AQS(AbstractQueuedSynchronizer)源码深度解析(2)—Lock接口以及自定义锁的实现
AQS(AbstractQueuedSynchronizer)源码深度解析(3)—同步队列以及独占式获取锁、释放锁的原理【一万字】
AQS(AbstractQueuedSynchronizer)源码深度解析(4)—共享式获取锁、释放锁的原理【一万字】
AQS(AbstractQueuedSynchronizer)源码深度解析(5)—条件队列的等待、通知的实现以及AQS的总结【一万字】
上篇文章中我们介绍了同步队列以及独占式获取锁、释放锁的原理,下面我们来看看共享式获取锁、释放锁的原理,以及如何实现一个更加高级的自定义的独占锁和共享锁。
1 acquireShared共享式获取锁
共享式获取与独占式获取的区别就是同一时刻是否可以多个线程同时获取到锁。
在独占锁的实现中会使用一个exclusiveOwnerThread属性,用来记录当前持有锁的线程。当独占锁已经被某个线程持有时,其他线程只能等待它被释放后,才能去争锁,并且同一时刻只有一个线程能争锁成功。
对于共享锁来说,如果一个线程成功获取了共享锁,那么其他等待在这个共享锁上的线程就也可以尝试去获取锁,并且极有可能获取成功。基于共享式实现的组件有CountDownLatch、Semaphore等。
通过调用AQS的acquireShared模版方法方法可以共享式地获取锁,同样该方法不响应中断。实际上如果看懂了独占式获取锁的源码,那么看共享式获取锁的源码就非常简单了。
大概步骤如下:
- 首先使用tryAcquireShared尝试获取锁,获取成功(返回值大于等于0)则直接返回;
- 否则,调用doAcquireShared将当前线程封装为Node.SHARED模式的Node 结点后加入到AQS 同步队列的尾部,然后"自旋"尝试获取锁,如果还是获取不到,那么最终使用park方法挂起自己等待被唤醒。
/**
* 共享式获取锁的模版方法,不响应中断
*
* @param arg 参数
*/
public final void acquireShared(int arg) {
//尝试调用tryAcquireShared方法获取锁
//获取成功(返回值大于等于0)则直接返回;
if (tryAcquireShared(arg) < 0)
//失败则调用doAcquireShared方法将当前线程封装为Node.SHARED类型的Node 结点后加入到AQS 同步队列的尾部,
//然后"自旋"尝试获取同步状态,如果还是获取不到,那么最终使用park方法挂起自己。
doAcquireShared(arg);
}
1.1 tryAcquireShared尝试获取共享锁
熟悉的tryAcquireShared方法,这个方法我们在最开头讲“AQS的设计”时就提到过,该方法是AQS的子类即我们自己实现的,用于尝试获取共享锁,一般来说就是对state的改变、或者重入锁的检查等等,不同的锁有自己相应的逻辑判断,这里不多讲,后面讲具体锁的实现的时候(比如CountDownLatch)会讲到。
返回int类型的值(比如返回剩余的state状态值-资源数量),一般的理解为:
- 如果返回值小于0,表示当前线程共享锁失败;
- 如果返回值大于0,表示当前线程共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功;
- 如果返回值等于0,表示当前线程共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败。
- 实际上在AQS的实际实现中,即使某时刻返回值等于0,接下来其他线程尝试获取共享锁的行为也可能会成功。即某线程获取锁并且返回值等于0之后,马上又有线程释放了锁,导致实际上可获取锁数量大于0,此时后继还是可以尝试获取锁的。
在AQS的中tryAcquireShared的实现为抛出异常,因此需要子类重写:
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
1.2 doAcquireShared自旋获取共享锁
首次调用tryAcquireShared方法获取锁失败之后,会调用doAcquireShared方法。类似于独占式获取锁acquire方法中的addWaiter和acquireQueued方法的组合版本!
大概步骤如下:
- 调用addWaiter方法,将当前线程封装为Node.SHARED模式的Node结点后加入到AQS 同步队列的尾部,即表示共享模式。
- 后面就是类似于acquireQueued方法的逻辑,结点自旋尝试获取共享锁。如果还是获取不到,那么最终使用park方法挂起自己等待被唤醒。
每个结点可以尝试获取锁的要求是前驱结点是头结点,那么它本身就是整个队列中的第二个结点,每个获得锁的结点都一定是成为过头结点。那么如果某第二个结点因为不满足条件没有获取到共享锁而被挂起,那么即使后续结点满足条件也一定不能获取到共享锁。
/**
* 自旋尝试共享式获取锁,一段时间后可能会挂起
* 和独占式获取的区别:
* 1 以共享模式Node.SHARED添加结点
* 2 获取到锁之后,修改当前的头结点,并将信息传播到后续的结点队列中
*
* @param arg 参数
*/
private void doAcquireShared(int arg) {
/*1 addWaiter方法逻辑,和独占式获取的区别1 :以共享模式Node.SHARED添加结点*/
final Node node = addWaiter(Node.SHARED);
/*2 下面就是类似于acquireQueued方法的逻辑
* 区别在于获取到锁之后acquireQueued调用setHead方法,这里调用setHeadAndPropagate方法
* */
//当前线程获取锁失败的标志
boolean failed = true;
try {
//当前线程的中断标志
boolean interrupted = false;
for (; ; ) {
//获取前驱结点
final Node p = node.predecessor();
/*当前驱结点是头结点的时候就会以共享的方式去尝试获取锁*/
if (p == head) {
int r = tryAcquireShared(arg);
/*返回值如果大于等于0,则表示获取到了锁*/
if (r >= 0) {
/*和独占式获取的区别2 :修改当前的头结点,根据传播状态判断是否要唤醒后继结点。*/
setHeadAndPropagate(node, r);
// 释放掉已经获取到锁的前驱结点
p.next = null;
/*检查设置中断标志*/
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
/*判断是否应该挂起,以及挂起的方法,和acquireQueued方法的逻辑完全一致,不会响应中断*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
从源码可以看出,和独占式获取的主要区别为:
- addWaiter以共享模式Node.SHARED添加结点。
- 获取到锁之后,调用setHeadAndPropagate设置行head结点,然后根据传播状态判断是否要唤醒后继结点。
1.2.1 setHeadAndPropagat设置结点并传播信息
在结点线程获取共享锁成功之后会调用setHeadAndPropagat方法,相比于setHead方法,在设置head之后多执行了一步propagate操作:
- 和setHead方法一样设置新head结点信息
- 根据传播状态判断是否要唤醒后继结点。
1.2.1.1 doReleaseShared唤醒后继结点
doReleaseShared用于在共享模式下唤醒后继结点。
关于Node.PROPAGATE的分析,将在下面总结部分列出!
/**
* 共享式获取锁的核心方法,尝试唤醒一个后继线程,被唤醒的线程会尝试获取共享锁,如果成功之后,则又会有可能调用setHeadAndPropagate,将唤醒传播下去。
* 独占锁只有在一个线程释放所之后才会唤醒下一个线程,而共享锁在一个线程在获取到锁和释放掉锁锁之后,都可能会调用这个方法唤醒下一个线程
* 因为在共享锁模式下,锁可以被多个线程所共同持有,既然当前线程已经拿到共享锁了,那么就可以直接通知后继结点来获取锁,而不必等待锁被释放的时候再通知。
*/
private void doReleaseShared() {
/*一个死循环,跳出循环的条件就是最下面的break*/
for (; ; ) {
//获取当前的head,每次循环读取最新的head
Node h = head;
//如果h不为null且h不为tail,表示队列至少有两个结点,那么尝试唤醒head后继结点线程
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果头结点的状态为SIGNAL,那么表示后继结点需要被唤醒
if (ws == Node.SIGNAL) {
//尝试CAS设置h的状态从Node.SIGNAL变成0
//可能存在多线程操作,但是只会有一条成功
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//失败的线程结束本次循环,继续下一次循环
continue; // loop to recheck cases
//成功的那一条线程会调用unparkSuccessor方法唤醒head的一个没有取消的后继结点
//对于一个head,只需要一条线程去唤醒该head的后继就行了。上面的CAS就是保证unparkSuccessor方法对于一个head只执行一次
unparkSuccessor(h);
}
/*
* 如果h状态为0,那说明后继结点线程已经是唤醒状态了或者将会被唤醒,不需要该线程来唤醒
* 那么尝试设置h状态从0变成PROPAGATE,如果失败则继续下一次循环,此时设置PROPAGATE状态能保证唤醒操作能够传播下去
* 因为后继结点成为头结点时,在setHeadAndPropagate方法中能够读取到原head结点的PROPAGATE状态<0,从而让它可以尝试唤醒后继结点(如果存在)
* */
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
//失败的线程结束本次循环,继续下一次循环
continue; // loop on failed CAS
}
// 执行到这一步说明在上面的判断中队列可能只有一个结点,或者unparkSuccessor方法调用完毕,或h状态为PROPAGATE(不需要继续唤醒后继)
// 再次检查h是否仍然是最新的head,如果不是的话需要再进行循环;如果是的话说明head没有变化,退出循环
if (h == head) // loop if head changed
break;
}
}
2 reaseShared共享式释放锁
共享锁的释放是通过调用releaseShared模版方法来实现的。大概步骤为:
- 调用tryReleaseShared尝试释放共享锁,这里必须实现为线程安全。
- 如果释放了锁,那么调用doReleaseShared方法唤醒后继结点,实现唤醒的传播。
对于支持共享式的同步组件(即多个线程同时访问),它们和独占式的主要区别就是tryReleaseShared方法必须确保锁的释放是线程安全的(因为既然是多个线程能够访问,那么释放的时候也会是多个线程的,就需要保证释放时候的线程安全)。
由于tryReleaseShared方法也是我们自己实现的,因此需要我们自己实现线程安全,所以常常采用CAS的方式来释放同步状态。
/**
* 共享模式下释放锁的模版方法。
* ,如果成功释放则会调用
*/
public final boolean releaseShared(int arg) {
//tryReleaseShared释放锁资源,该方法由子类自己实现
if (tryReleaseShared(arg)) {
//释放成功,必定调用doReleaseShared尝试唤醒后继结点
doReleaseShared();
return true;
}
return false;
}
3 acquireSharedInterruptibly共享式可中断获取锁
上面分析的独占式获取锁的方法acquireShared是不会响应中断的。但是AQS提供了另外一个acquireSharedInterruptibly模版方法,调用该方法的线程在等待获取锁时,如果当前线程被中断,会立刻返回,并抛出InterruptedException。
/**
* 共享式可中断获取锁模版方法
*
* @param arg 参数
* @throws InterruptedException 线程处于中断状态,抛出此异常
*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//最开始就检查一次,如果当前线程是被中断状态,则清除已中断状态,并抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取锁
if (tryAcquireShared(arg) < 0)
//获取不到就执行doAcquireSharedInterruptibly方法
doAcquireSharedInterruptibly(arg);
}
3.1 doAcquireSharedInterruptibly共享式可中断获取锁
该方法内部操作和doAcquireShared差不多,都是自旋获取共享锁,有些许区别,就是在后续挂起的线程因为线程被中断而返回时的处理方式不一样。
共享式不可中断获取锁仅仅是记录该状态,interrupted = true,紧接着又继续循环获取锁;共享式可中断获取锁则直接抛出异常,因此会直接跳出循环去执行finally代码块。
/**
* 以共享可中断模式获取。
*
* @param arg 参数
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
/*内部操作和doAcquireShared差不多,都是自旋获取共享锁,有些许区别*/
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (; ; ) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
/*
* 这里就是区别所在,共享不可中断式方法doAcquireShared中
* 如果线程被中断,此处仅仅会记录该状态,interrupted = true,紧接着又继续循环获取锁
*
* 但是在该共享可中断式的锁获取方法中
* 如果线程被中断,此处直接抛出异常,因此会直接跳出循环去执行finally代码块
* */
throw new InterruptedException();
}
}
/*获取到锁或者抛出异常都会执行finally代码块*/
finally {
/*如果获取锁失败。那么是发生异常的情况,可能就是线程被中断了,执行cancelAcquire方法取消该结点对锁的请求,该线程结束*/
if (failed)
cancelAcquire(node);
}
}
4 tryAcquireSharedNanos共享式超时获取锁
共享式超时获取锁tryAcquireSharedNanos模版方法可以被视作共享式响应中断获取锁acquireSharedInterruptibly方法的“增强版”,支持中断,支持超时时间!
/**
* 共享式超时获取锁,支持中断
*
* @param arg 参数
* @param nanosTimeout 超时时间,纳秒
* @return 是否获取锁成功
* @throws InterruptedException 如果被中断,则抛出InterruptedException异常
*/
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
//最开始就检查一次,如果当前线程是被中断状态,则清除已中断状态,并抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//下面是一个||运算进行短路连接的代码
//tryAcquireShared尝试获取锁,获取到了直接返回true
//获取不到(左边表达式为false) 就执行doAcquireSharedNanos方法
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
4.1 doAcquireSharedNanos共享式超时获取锁
doAcquireSharedNanos (int arg,long nanosTimeout)方法在支持响应中断的基础上, 增加了超时获取的特性。
该方法在自旋过程中,当结点的前驱结点为头结点时尝试获取锁,如果获取成功则从该方法返回,这个过程和共享式式同步获取的过程类似,但是在锁获取失败的处理上有所不同。
如果当前线程获取锁失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。
因此,在超时非常短的场景下,AQS会进入无条件的快速自旋而不是挂起线程。
static final long spinForTimeoutThreshold = 1000L;
/**
* 以共享超时模式获取。
*
* @param arg 参数
* @param nanosTimeout 剩余超时时间,纳秒
* @return true 成功 ;false 失败
* @throws InterruptedException 如果被中断,则抛出InterruptedException异常
*/
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
//剩余超时时间小于等于0的,直接返回
if (nanosTimeout <= 0L)
return false;
//能够等待获取的最后纳秒时间
final long deadline = System.nanoTime() + nanosTimeout;
//同样调用addWaiter将当前线程构造成结点加入到同步队列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
/*和共享式式不可中断方法doAcquireShared一样,自旋获取锁*/
for (; ; ) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return true;
}
}
/*这里就是区别所在*/
//如果新的剩余超时时间小于0,则退出循环,返回false,表示没获取到锁
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
//如果需要挂起 并且 剩余nanosTimeout大于spinForTimeoutThreshold,即大于1000纳秒
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
//那么调用LockSupport.parkNanos方法将当前线程挂起nanosTimeout
LockSupport.parkNanos(this, nanosTimeout);
//如果线程被中断了,那么直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
}
}
/*获取到锁、超时时间到了、抛出异常都会执行finally代码块*/
finally {
/*如果获取锁失败。可能就是线程被中断了,那么执行cancelAcquire方法取消该结点对锁的请求,该线程结束
* 或者是超时时间到了,那么执行cancelAcquire方法取消该结点对锁的请求,将返回false
* */
if (failed)
cancelAcquire(node);
}
}
5 共享式获取/释放锁总结
我们可以调用acquireShared 模版方法来获取不可中断的共享锁,可以调用acquireSharedInterruptibly模版方法来可中断的获取共享锁,可以调用tryAcquireSharedNanos模版方法来可中断可超时的获取共享锁,在此之前需要重写tryAcquireShared方法;还可以调用releaseShared模版方法来释放共享锁,在此之前需要重写tryReleaseShared方法。
对于共享锁来说,由于锁是可以多个线程同时获取的。那么如果一个线程成功获取了共享锁,那么其他等待在这个共享锁上的线程就也可以尝试去获取锁,并且极有可能获取成功。因此在一个结点线程释放共享锁成功时,必定调用doReleaseShared尝试唤醒后继结点,而在一个结点线程获取共享锁成功时,也可能会调用doReleaseShared尝试唤醒后继结点。
基于共享式实现的组件有CountDownLatch、Semaphore、ReentrantReadWriteLock等。
5.1. Node.PROPAGATE简析
5.1.1 出现时机
doReleaseShared方法在线程获取共享锁成功之后可能执行,在线程释放共享锁成功之后必定执行。
在doReleaseShared方法中,可能会存在将线程状态设置为Node.PROPAGATE的情况,然而,整个AQS类中也只有这一处直接涉及到Node.PROPAGATE状态,并且仅仅是设置,在其他地方却再也没见到对该状态的直接使用。由于该状态值为-3,因此可能是在其他方法中对waitStatus大小范围的判断的时候将这种情况包括进去了(猜测)!
关于Node.PROPAGATE的直接代码如下:
else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
首先是需要进入到else if分支,然后需要此时ws(源码中最开始获取的head结点的引用的状态—不一定是最新的)状态为0,然后尝试CAS设置该结点的状态为Node.PROPAGATE,并且可能失败,失败之后直接continue继续下一次循环。
对于这个Node.PROPAGATE状态的作用,众说纷纭,笔者看了很多文章,很多看起来都有道理,但是仔细想想又有些差错,在此,笔者不做过多个人分析,首先来看看进入else if分支并且ws为0的情况有哪些!
初始情况
假设某个共享锁的实现允许最多三个线程持有锁,此时有线程A、B、C均获取到了锁,同步队列中还有一个被挂起的结点线程D在等待锁的释放,此时队列结构如下:
如果此时线程A释放了锁,那么A将会调用doReleaseShared方法,但是明显A将会进入if代码块中,将head的状态改为0,同时调用unparkSuccessor唤醒一个后继线程,这里明显是D。
此时同步队列结构为:
情形1
如果此时线程B、C都释放了锁,那么B、C都将会调用doReleaseShared方法,假设它们执行速度差不多,那么它们都将会进入到else if中,因为此时head的状态变成了0,然后它们都会调用CAS将0改成Node.PROPAGATE,此时只会有一条线程成功,另一条会失败。
这就是 释放锁时,进入到else if的一种情况。即多个释放锁的结点操作同一个head,那么最终只有一个结点能够在if中成功调用unparkSuccessor唤醒后继,另外的结点都将失败并最终都会走到else if中去。同理,获取锁时也可能由于上面的原因而进入到else if。
情形2
如果此时又来了一个新结点E,由于同样没有获取到锁那么会调用addWaiter添加到D结点后面成为新tail结点。
然后结点E会在shouldParkAfterFailedAcquire方法中尝试将没取消的前驱D的waitStatus修改为Node.SIGNAL,然后挂起。
那么在新结点E执行addWaiter之后,执行shouldParkAfterFailedAcquire之前,此时同步队列结构为:
由于A释放了锁,那么线程D会被唤醒,并调用tryAcquireShared获取了锁,那么将会返回0(常见的共享锁获取锁的实现是使用state减去需要获取的资源数量,这里A释放了一把锁,D又获取一把锁,此时剩余资源—锁数量剩余0)。
此时,如果B再释放锁,这就出现了“即使某时刻返回值等于0,接下来其他线程尝试获取共享锁的行为也可能会成功”的情况。即 某线程获取共享锁并且返回值等于0之后,马上又有其他持有锁的线程释放了锁,导致实际上可获取锁数量大于0,此时后继还是可以尝试获取锁的。
上面的是题外话,我们回到正文。如果此时B释放了锁,那么肯定还是会走doReleaseShared方法,由于在初始情形中,head的状态已经被A修改为0,此时B还是会走else if ,将状态改为Node.PROPAGATE。
我们回到线程D,此时线程D获取锁之后会走到setHeadAndPropagate方法中,在进行sheHead方法调用之后,此时结构如下(假设线程E由于资源分配的原因,在此期间效率低下,还没有将前驱D的状态改为-1,或者由于单核CPU线程切换导致线程E一直没有分配到时间片):
sheHead之后,就会判断是否需要调用doReleaseShared方法唤醒后继线程,这里的判断条件是:
propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null ||
h.waitStatus < 0
根据结构,只有第三个条件h.waitStatus<0满足,此时线程D就可以调用doReleaseShared唤醒后继结点,在这个过程中,关键的就是线程B将老head的状态设置为Node.PROPAGATE,即-2,小于0,此时可以将唤醒传播下去,否则被唤醒的线程A将因为不满足条件而不会调用doReleaseShared方法!
或许这就是所谓的Node.PROPAGATE可能将唤醒传播下去的考虑到的情况之一?
而在此时获取锁的线程D调用doReleaseShared方法时,由于此时head状态本来就是0,因此直接进入else if将状态改为Node.PROPAGATE,表示此时后继结点不需要唤醒,但是需要将唤醒操作继续传播下去。
这也是在获取锁时,在doReleaseShared方法中第一次出现某结点作为head就直接进入到else if的一种情况。
情形3
由于A释放了锁,那么如果D的获取了锁,并且方法执行完毕,那么此时同步队列结构如下:
此时又来了一个新结点E,由于同样没有获取到锁那么会调用addWaiter添加到head结点后面成为新tail结点。
然后结点E会在shouldParkAfterFailedAcquire方法中尝试将没取消的前驱head的waitStatus修改为Node.SIGNAL,然后挂起。
那么在新结点E执行addWaiter之后,执行shouldParkAfterFailedAcquire之前,此时同步队列结构为:
此时线程A尝试释放锁,释放锁成功后一定会都调用doReleaseShared方法时,由于此时head状态本来就是0,因此直接进入else if将状态改为Node.PROPAGATE,表示此时后继结点不需要唤醒,但是需要将唤醒操作继续传播下去。
这也是在释放锁的时候,在doReleaseShared方法中第一次出现某结点作为head就直接进入到else if的一种情况。
5.1.2 总结
下面总结了会走到else if的几种情况,可能还有更多情形这里分有分析出来:
- 多线程并发的在doReleaseShared方法中操作同一个head,并且这段时间head没发生改变。那么先进来的一条线程能够将if执行成功,即将状态置为0,然后调用unparkSuccessor唤醒后,后续进来的线程由于状态为0,那么只能执行else if。这种情况对于获取锁或者释放锁的doReleaseShared方法都可能存在!这种情况发生时,在doReleaseShared方法中第一次出现某结点作为head时,不会进入else if,一定是后续其他线程以同样的结点作为头结点时,才会进入else if!
- 对于获取锁的doReleaseShared方法,有一种在doReleaseShared方法中第一次出现某结点作为head就直接进入到else if的一种情况。设结点D作为原队列的尾结点,状态值为0,然后又来了新结点E,在新结点E的线程调用addWaiter之后(加入队列成为新tail),shouldParkAfterFailedAcquire之前(没来得及修改前驱D的状态为-1)的这段特殊时间范围之内,此时结点D的线程获取到了锁成为新头结点,并且原头结点状态值小于0,那么就会出现 在获取锁时调用doReleaseShared并直接进入else if的情况,这种情况的要求极为苛刻。或许本就不存在,只是本人哪里的分析出问题了?
- 对于释放锁的doReleaseShared方法,有一种在doReleaseShared方法中第一次出现结点某作为head就直接进入到else if的一种情况。设结点D作为原队列的尾结点,此时状态值为0,并且已经获取到了锁;然后又来了新结点E,在新结点E的线程调用addWaiter之后(加入队列成为新tail),shouldParkAfterFailedAcquire之前(没来得及修改前驱D的状态为-1)的这段特殊时间范围之内,此时结点D的线程释放了锁,那么就会出现 在释放锁时调用doReleaseShared并直接进入else if的情况,这种情况的要求极为苛刻。或许本就不存在,只是本人哪里的分析出问题了?
那么根据上面的情况来看,就算没有else if这个判断或者如果没有Node.PROPAGATE这个状态的设置,最终对于后续结点的唤醒并没有什么大的问题,也并不会导致队列失活。
加上Node.PROPAGATE这个状态的设置,导致的直接结果是可能会增加doReleaseShared方法调用的次数,但是也会增加无效、无意义唤醒的次数。 在setHeadAndPropagate方法中,判断是否需要唤醒后继的源码注释中我们能找到这样的描述:
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.
意思就是,这些判断可能会造成无意义的唤醒,但如果doReleaseShared方法调用的次数比较多的话,相当于多线程争抢着去唤醒后继线程,或许可以提升锁的获取速度?或者这里的代码只是一种更加通用的保证正确的做法?实际上AQS中还有许多这样可能会造成无意义调用的代码!
6 锁的简单实现
6.1 可重入独占锁的实现
在最开始我们实现了简单的不可重入独占锁,现在我们尝试实现可重入的独占锁,实际上也比较简单!
AQS 的state 状态值表示线程获取该锁的重入次数, 在默认情况下,state的值为0 表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state 的值为l ,如果CAS 成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是重入次数为2。在该线程释放该锁时,会尝试使用CAS 让状态值减1,如果减l 后状态值为0,则当前线程释放该锁。
对于可重入独占锁,获取了几次锁就需要释放几次锁,否则由于锁释放不完全而阻塞其他线程!
/**
* @author lx
*/
public class ReentrantExclusiveLock implements Lock {
/**
* 将AQS的实现组合到锁的实现内部
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 重写isHeldExclusively方法
*
* @return 是否处于锁占用状态
*/
@Override
protected boolean isHeldExclusively() {
//state是否等于1
return getState() == 1;
}
/**
* 重写tryAcquire方法,可重入的尝试获取锁
*
* @param acquires 参数,这里我们没用到
* @return 获取成功返回true,失败返回false
*/
@Override
public boolean tryAcquire(int acquires) {
/*尝试获取锁*/
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
/*获取失败,判断当前获取锁的线程是不是本线程*/
else if (getExclusiveOwnerThread() == Thread.currentThread()) {
//如果是,那么state+1,表示锁重入了
setState(getState() + 1);
return true;
}
return false;
}
/**
* 重写tryRelease方法,可重入的尝试释放锁
*
* @param releases 参数,这里我们没用到
* @return 释放成功返回true,失败返回false
*/
@Override
protected boolean tryRelease(int releases) {
//如果尝试解锁的线程不是加锁的线程,那么抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean flag = false;
int oldState = getState();
int newState = oldState - 1;
//如果state变成0,设置当前拥有独占访问权限的线程为null,返回true
if (newState == 0) {
setExclusiveOwnerThread(null);
flag = true;
}
//重入锁的释放,释放一次state减去1
setState(newState);
return flag;
}
/**
* 返回一个Condition,每个condition都包含了一个condition队列
* 用于实现线程在指定条件队列上的主动等待和唤醒
*
* @return 每次调用返回一个新的ConditionObject
*/
Condition newCondition() {
return new ConditionObject();
}
}
/**
* 仅需要将操作代理到Sync实例上即可
*/
private final Sync sync = new Sync();
/**
* lock接口的lock方法
*/
@Override
public void lock() {
sync.acquire(1);
}
/**
* lock接口的tryLock方法
*/
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* lock接口的unlock方法
*/
@Override
public void unlock() {
sync.release(1);
}
/**
* lock接口的newCondition方法
*/
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
}
6.1.1 测试
/**
* @author lx
*/
public class ReentrantExclusiveLockTest {
/**
* 创建锁
*/
static ReentrantExclusiveLock reentrantExclusiveLock = new ReentrantExclusiveLock();
/**
* 自增变量
*/
static int i;
public static void main(String[] args) throws InterruptedException {
//三条线程
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());
Runa runa = new Runa();
for (int i1 = 0; i1 < 3; i1++) {
threadPoolExecutor.execute(runa);
}
threadPoolExecutor.shutdown();
while (!threadPoolExecutor.isTerminated()) {
}
//三条线程执行完毕,输出最终结果
System.out.println(i);
}
/**
* 线程任务,循环50000次,每次i自增1
*/
public static class Runa implements Runnable {
@Override
public void run() {
// lock与unlock注释时,可能会得到错误的结果
// 开启时每次都会得到正确的结果150000
//支持多次获取锁(重入)
reentrantExclusiveLock.lock();
reentrantExclusiveLock.lock();
for (int i1 = 0; i1 < 50000; i1++) {
i++;
}
//获取了多少次必须释放多少次
reentrantExclusiveLock.unlock();
reentrantExclusiveLock.unlock();
}
}
}
6.2 可重入共享锁的实现
自定义一个共享锁,共享锁的数量可以自己指定。默认构造情况下,在同一时刻,最多允许三条线程同时获取锁,超过三个线程的访问将被阻塞。
我们必须重写tryAcquireShared(int args)方法和tryReleaseShared(int args)方法。由于是共享式的获取,那么在对同步状态state更新时,两个方法中都需要使用CAS方法compareAndSet(int expect,int update)做原子性保障。
假设一条线程一次只需要获取一个资源即表示获取到锁。由于同一时刻允许至多三个线程的同时访问,表明同步资源数为3,这样可以设置初始状态state为3来代表同步资源,当一个线程进行获取,status减1,该线程释放,则status加1,状态的合法范围为0、1和2,其中0表示当前已经有两个线程获取了同步资源,此时再有其他线程对同步状态进行获取,该线程可能会被阻塞。
最后,将自定义的AQS实现通过内部类的方法聚合到自定义锁中,自定义锁还需要实现Lock接口,外部方法的内部实现直接调用对应的模版方法即可。
这里一条线程可以获取多次共享锁,但是同时必须释放多次共享锁,否则可能由于锁资源的减少,导致效率低下甚至死锁(可以使用tryLock避免)!
public class ShareLock implements Lock {
/**
* 默认构造器,默认共享资源3个
*/
public ShareLock() {
sync = new Sync(3);
}
/**
* 指定资源数量的构造器
*/
public ShareLock(int num) {
sync = new Sync(num);
}
private static class Sync extends AbstractQueuedSynchronizer {
Sync(int num) {
if (num <= 0) {
throw new RuntimeException("锁资源数量需要大于0");
}
setState(num);
}
/**
* 重写tryAcquireShared获取共享锁
*/
@Override
protected int tryAcquireShared(int arg) {
/*一般的思想*/
/*//获取此时state
int currentState = getState();
//获取剩余state
int newState = currentState - arg;
//如果剩余state小于0则直接返回负数
//否则尝试更新state,更新成功就说明获取成功,返回大于等于0的数
return newState < 0 ? newState : compareAndSetState(currentState, newState) ? newState : -1;*/
/*更好的思想
* 在上面的实现中,如果剩余state值大于0,那么只尝试CAS一次,如果失败就算没有获取到锁,此时该线程会进入同步队列
* 在下面的实现中,如果剩余state值大于0,那么如果尝试CAS更新不成功,会在for循环中重试,直到剩余state值小于0或者更新成功
*
* 两种方法的不同之处在于,对CAS操作是否进行重试,这里建议第二种
* 因为可能会有多个线程同时获取多把锁,但是由于CAS只能保证一次只有一个线程成功,因此其他线程必定失败
* 但此时,实际上还是存在剩余的锁没有被获取完毕的,因此让其他线程重试,相比于直接加入到同步队列中,对于锁的利用率更高!
* */
for (; ; ) {
int currentState = getState();
int newState = currentState - arg;
if (newState < 0 || compareAndSetState(currentState, newState)) {
return newState;
}
}
}
/**
* 重写tryReleaseShared释放共享锁
*
* @param arg 参数
* @return 成功返回true 失败返回false
*/
@Override
protected boolean tryReleaseShared(int arg) {
//只能成功
for (; ; ) {
int currentState = getState();
int newState = currentState + arg;
if (compareAndSetState(currentState, newState)) {
return true;
}
}
}
}
/**
* 内部初始化一个sync对象,此后仅需要将操作代理到这个Sync对象上即可
*/
private final Sync sync;
/*下面都是调用模版方法*/
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquireShared(1) >= 0;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.releaseShared(1);
}
/**
* @return 没有实现自定义Condition,单纯依靠原始Condition实现是不支持共享锁的
*/
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
6.2.1 测试
public class ShareLockTest {
static final ShareLock lock = new ShareLock();
public static void main(String[] args) {
/*启动10个线程*/
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.setDaemon(true);
w.start();
}
ShareLockTest.sleep(20);
}
/**
* 睡眠
*
* @param seconds 时间,秒
*/
public static void sleep(long seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class Worker extends Thread {
@Override
public void run() {
/*不停的获取锁,释放锁
* 最开始获取了几个锁,那么最后必须释放几个锁
* 否则可能由于锁资源的减少,导致效率低下甚至死锁(可以使用tryLock避免)!
* */
while (true) {
/*tryLock测试*/
if (lock.tryLock()) {
System.out.println(Thread.currentThread().getName());
/*获得锁之后都会休眠2秒
那么可以想象,控制台将很有可能会出现连续三个一起输出,然后等待2秒,再连续三个一起输出,然后2秒……*/
ShareLockTest.sleep(2);
lock.unlock();
}
/*lock测试,或许总会出现固定线程获取锁,因为AQS默认是实现是非公平锁*/
/*lock.lock();
System.out.println(Thread.currentThread().getName());
ShareLockTest.sleep(2);
lock.unlock();*/
}
}
}
}
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!