五、可中断获取锁的实现(独占锁的特性之一)

我们知道lock相较于synchronized有一些更方便的特性,比如能响应中断以及超时等待等特性,现在我们依旧采用通过学习源码的方式来看看能够响应中断是怎么实现的。可响应中断式锁可调用方法lock.lockInterruptibly();而该方法其底层会调用AQS的acquireInterruptibly方法

注意哦,这个独占锁的一个模式来的。

5.1 acquireInterruptibly源码:

 /**
* Acquires in exclusive mode, aborting if interrupted.
* Implemented by first checking interrupt status, then invoking
* at least once {@link #tryAcquire}, returning on
* success. Otherwise the thread is queued, possibly repeatedly
* blocking and unblocking, invoking {@link #tryAcquire}
* until success or the thread is interrupted. This method can be
* used to implement method {@link Lock#lockInterruptibly}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
* @throws InterruptedException if the current thread is interrupted
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}

这个代码很容易懂,一开始就判断线程的interrupt状态位,有没interrupted,有的话抛出异常,并清楚interrupt的标志(interrupted()方法的作用);如果没有堵塞,就调用用户重写的tryAcquire方法看看能不能获得锁现在,不能的话就调用下面的doAcquireInterruptibly(arg),类比acquire方法中的acquireQueue(但这里少了一个addWaiter方法),所以这猜测,这个doAcquireINterruptibly方法呢应该也是创建一个waiter结点去排队,然后如果还没排队到第二个就不断自旋等待?然后能够相应interrupt?

来看看它的源码一探究竟!

5.11 doAcquireInterruptibly(arg)源码分析

 /**
* Acquires in exclusive interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);//为这个无法获得锁的线程创建node结点并安排在同步队列中。这个EXCLUESIVE也告诉我们是
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

可以发现,这个代码步就是在acquire方法中的addWaiter加上acquireQueued嘛,但有个不同,就是如果parkAndCheckInterrupt()方法返回了true,也就是线程被interrupt了,那么就抛出异常,然后在你的主业务逻辑代码中就可以catch异常然后做点什么了。

我们顺便回忆一下,这个parkAndCheckInterrupt()方法:

 private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();//注意哦,这个方法如果刚刚是interrupt的,会返回true,然后清除当前线程interrupt的状态
}

我们知道,wait的状态下,interrupt是会抛出异常的,然后我百度了下,这个LockSupport的park方法,线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException。也就是在调用了堵塞线程的interrupt后,这个park方法会接触block的状态,但不会抛出异常,就继续运行的意思吧。

 

所以我觉得这个相应interrupt的锁的意思应该是,一般的Lock的话,你interrupt的话应该就会结束block的状态,但不会抛异常;而这个相应interrupt版本的lock的话,检查到block解除后,就会检查thread的interrupt标志,被interrupt的话就抛出异常,这样你就可以在业务逻辑代码中catch这个异常然后做出反应了。

六、超时等待式获取锁的实现(独占锁的特性之一)

通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回:

在超时时间内,当前线程成功获取了锁;
当前线程在超时时间内被中断;
超时时间结束,仍未获得锁返回false。

我们仍然通过采取阅读源码的方式来学习底层具体是怎么实现的,该方法会调用AQS的方法tryAcquireNanos()

6.1 ryAcquireNanos()源码分析

/**
* Attempts to acquire in exclusive mode, aborting if interrupted,
* and failing if the given timeout elapses. Implemented by first
* checking interrupt status, then invoking at least once {@link
* #tryAcquire}, returning on success. Otherwise, the thread is
* queued, possibly repeatedly blocking and unblocking, invoking
* {@link #tryAcquire} until success or the thread is interrupted
* or the timeout elapses. This method can be used to implement
* method {@link Lock#tryLock(long, TimeUnit)}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
* @param nanosTimeout the maximum number of nanoseconds to wait
* @return {@code true} if acquired; {@code false} if timed out
* @throws InterruptedException if the current thread is interrupted
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();//看起来这个方法也能相应interrupt?
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);////
}

从流程来看,这个方法是可以相应interrupt的,注解中也说了interrupt会退出。

总之,这个方法有三种情况结束:1. 获得锁成功了; 2. 被调用了interrupt; 3. 等待超时

和其他的获得锁方法一样,都是先去调用tryAcquire看看能不能获得锁,不能的话进入这个doAcquireNanos(arg, nanosTimeout)方法,这个方法根据以前的经验,想也知道是要创建waiter,插入同步队列,如果还没排队到第二个并获得锁,就继续堵塞,堵塞唤醒就自旋再次判断排队位置,但一定是加了算时间的机制,来看看doAcquireNanos的源码吧。

6.11 doAcquireNanos(arg, nanosTimeout)源码分析

/**
* Acquires in exclusive timed mode.
*
* @param arg the acquire argument
* @param nanosTimeout max wait time
* @return {@code true} if acquired
*/
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;//记录一个final的ddl
final Node node = addWaiter(Node.EXCLUSIVE);//创建node,在同步队列中排队
boolean failed = true;
try {
for (;;) {//进入自旋死循环,三个情况出来:1. interrupt了,会抛出异常; 2. 等待超时了返回false,代表没获得锁; 3. 获得锁了,设置队头并返回true
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();//离ddl还有多少时间
if (nanosTimeout <= 0L)
return false;//超时
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
//这个spinxxxx的意思是,剩下的需要等待的时间很短了,那还去park就很损耗性能嘛,那不如就在这里自旋了 LockSupport.parkNanos(this, nanosTimeout);//底层是UNSAFE的park,接受时间参数的那个版本
if (Thread.interrupted())
//可见是会多interrupt做出响应的
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

流程简单分析:

1. 对时间参数做健壮性判断,并算一个ddl

2. 创建线程的node,插入等待队列。

3. 进入自旋死循环,三个情况出来:1. interrupt了,会抛出异常; 2. 等待超时了返回false,代表没获得锁; 3. 获得锁了,设置队头并返回true

4. 如果运行出错进入finally,cancelAcquire方法,大概是把结点状态设为CANCELLED然后再处理cancel这个node后的等待队列。

七、共享锁的实现分析

(这段源码老实说有点懵,试着总结下……)

在聊完AQS对独占锁的实现后,我们继续一鼓作气的来看看共享锁是怎样实现的?共享锁的意思应该就是,不止一个线程可以获得这个锁,那么是怎么实现的呢?共享锁的获取方法为acquireShared。

7.1 acquireShared方法源码分析

/**
* Acquires in shared mode, ignoring interrupts. Implemented by
* first invoking at least once {@link #tryAcquireShared},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquireShared} until success.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquireShared} but is otherwise uninterpreted
* and can represent anything you like.
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
注解说了,这个方法是共享模式,忽视interrupt。
 
这段源码的逻辑很容易理解,在该方法中会首先调用tryAcquireShared方法,tryAcquireShared返回值是一个int类型,

该方法(tryAcqureShared)返回值是个重点。其一、由上面的源码片段可以看出返回值小于0表示获取锁失败,需要进入等待队列。其二、如果返回值等于0表示当前线程获取共享锁成功,但它后续的线程是无法继续获取的,也就是不需要把它后面等待的节点唤醒。最后、如果返回值大于0,表示当前线程获取共享锁成功且它后续等待的节点也有可能继续获取共享锁成功,也就是说此时需要把后续节点唤醒让它们去尝试获取共享锁。

7.11 doAcqurieShared()源码分析

 /**
* Acquires in shared uninterruptible mode.
* @param arg the acquire argument
*/
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//一样新建一个node,但这个node是个SHARED模式的
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {//负数说明获取失败
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

1. 新建node,但模式是SHARED的,并进入同步队列。(同步队列可能混着shared的和exclusive的node);

2. 无限循环,如果下一个不是head的话,就还没排队到第二个的话,就把node前面的那个的结点的状态设为SIGNAL;然后堵塞这个node;

3. 无限循环中,如果下一个是head,则尝试再次获得共享锁,如果结果>=0就成功,调用setHeadAndPropagate()方法。

4. 运行出错,进入finally中执行cancel,这个之前讲过不讲了。

所以其实除了排队到了它后,要执行setHeadAndPropagate而不是仅仅改变头节点,这个是和独占锁的recquire不同的地方。

看名字,应该是设置新的头节点,然后传递unpark??

7.111 setHeadAndPropagate(node, r)源码分析

//两个入参,一个是当前成功获取共享锁的节点,一个就是tryAcquireShared方法的返回值,注意上面说的,它可能大于0也可能等于0
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; //记录当前头节点
//设置新的头节点,即把当前获取到锁的节点设置为头节点
//注:这里是获取到锁之后的操作,不需要并发控制
setHead(node);
//这里意思有两种情况是需要执行唤醒操作
//1.propagate > 0 表示调用方指明了后继节点需要被唤醒
//2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果当前节点的后继节点是共享类型或者没有后继节点,则进行唤醒
//这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
if (s == null || s.isShared())
//后面详细说
doReleaseShared();
}
}

这段老实说我真滴有点懵,特别那个if???h == null???

这里大概我就理解成,emm如果h状态正常然后下一个是SHared的node,就调用doReleaseShared()吧。

7.112 doReleaseShared()方法

private void doReleaseShared() {
for (;;) {
//唤醒操作由头结点开始,注意这里的头节点已经是上面新设置的头结点了
//其实就是唤醒上面新获取到共享锁的节点的后继节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//表示后继节点需要被唤醒
if (ws == Node.SIGNAL) {
//这里需要控制并发,因为入口有setHeadAndPropagate跟release两个,避免两次unpark
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//执行唤醒操作
unparkSuccessor(h);
}
//如果后继节点暂时不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//如果头结点没有发生变化,表示设置完成,退出循环
//如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试
if (h == head)
break;
}
}

emmm只看懂就head是SIGNAL的话就要唤醒,为什么head是0要把head的状态设为PROPAGATE,怎么传播下去??

最后那个if == hewad更是emmm

还是以后有用到或有看到相关应用再回来看看吧。

想了下,可能是就如果是signal,意味着head(新的)后面那个结点也是等这个共享锁的,所以释放了它,释放了一个后,它也会进行tryAcquireShare等操作,也可能释放下一个,就有点传递的意思。

然后如果head是0,就赋值为PROPAGATE,大概是在上面<0会进入doReleaseShared所以有传递作用?

然后最后的if(h == head),好像是因为这个方法是可以多线程并发访问的,因为共享锁嘛,肯定有共享释放过程,所以这里要比较下,如果变了的话,要自旋,不变就退出,所以这个操作其实最多也就unpark一个node吧。

7.2 共享锁的释放解析

/**
* Releases in shared mode. Implemented by unblocking one or more
* threads if {@link #tryReleaseShared} returns true.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryReleaseShared} but is otherwise uninterpreted
* and can represent anything you like.
* @return the value returned from {@link #tryReleaseShared}
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

这里很简单,主要是调用doReleaseShared()的方法,上面讲了不讲了

参考文章:

https://www.jianshu.com/p/cc308d82cc71——《深入理解AbstractQueuedSynchronizer(AQS)》

https://www.jianshu.com/p/1161d33fc1d0——《深入浅出AQS之共享锁模式》

https://www.jianshu.com/p/6b8579280475——《Java并发源码剖析(二)——AbstractQueuedSynchronizer共享模式》

关于AQS——独占锁特性+共享锁实现(二)的更多相关文章

  1. 013-并发编程-java.util.concurrent.locks之-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架、独占锁与共享锁的获取与释放

    一.概述 AbstractQueuedSynchronizer (简称AQS),位于java.util.concurrent.locks.AbstractQueuedSynchronizer包下, A ...

  2. 关于AQS——独占锁的相关方法(一)

    一.序言 Lock接口是juc包下一个非常好用的锁,其方便和强大的功能让他成为synchronized的一个很好的替代品. 我们常用的一个Lock的实现类(好像也是唯一一个只实现了Lock接口的类) ...

  3. Java中的常见锁(公平和非公平锁、可重入锁和不可重入锁、自旋锁、独占锁和共享锁)

    公平和非公平锁 公平锁:是指多个线程按照申请的顺序来获取值.在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否者就会加入到等待队列中,以 ...

  4. 深入浅出AQS之独占锁模式

    每一个Java工程师应该都或多或少了解过AQS,我自己也是前前后后,反反复复研究了很久,看了忘,忘了再看,每次都有不一样的体会.这次趁着写博客,打算重新拿出来系统的研究下它的源码,总结成文章,便于以后 ...

  5. AQS详解之独占锁模式

    AQS介绍 AbstractQueuedSynchronizer简称AQS,即队列同步器.它是JUC包下面的核心组件,它的主要使用方式是继承,子类通过继承AQS,并实现它的抽象方法来管理同步状态,它分 ...

  6. [Java并发] AQS抽象队列同步器源码解析--独占锁释放过程

    [Java并发] AQS抽象队列同步器源码解析--独占锁获取过程 上一篇已经讲解了AQS独占锁的获取过程,接下来就是对AQS独占锁的释放过程进行详细的分析说明,废话不多说,直接进入正文... 锁释放入 ...

  7. 基于AQS的锁

    锁分为独占锁和共享锁,它们的主要实现都是依靠AbstractQueuedSynchronizer,这个类只提供一系列公共的方法,让子类来调用.基于我了解不深,从这个类的属性,方法,和独占锁的获取方式去 ...

  8. AQS源码深入分析之独占模式-ReentrantLock锁特性详解

    本文基于JDK-8u261源码分析 相信大部分人知道AQS是因为ReentrantLock,ReentrantLock的底层是使用AQS来实现的.还有一部分人知道共享锁(Semaphore/Count ...

  9. 自定义AQS独占模式下的同步器来实现独享锁

    自定义AQS独占模式下的同步器来实现独享锁 /** * 自定义AQS独占模式下的同步器来实现独享锁 */ public class Mutex implements Lock, java.io.Ser ...

随机推荐

  1. OP趋势系统

    经过3年多时间的摸索,经历过熊市牛市的历练,终于完成坚持已久的OP趋势系统的实践,接下来,我将在股灾后,每天都分享OP趋势系统的信号,可以很负责任的说,经过10年历史数据的测试,加上3年的实盘,更加坚 ...

  2. C++中两个类相互包含引用问题

    在构造自己的类时,有可能会碰到两个类之间的相互引用问题,例如:定义了类A类B,A中使用了B定义的类型,B中也使用了A定义的类型 class A { int i; B b; } class B { in ...

  3. 集训Day2

    雅礼集训2017Day2 T1 给你一个水箱,水箱里有n-1个挡板,水遵循物理定律 给你m个条件,表示第i个格子上面y+1高度的地方有或没有水 现在给你无限的水从任意地方往下倒,问最多满足多少条件 n ...

  4. 主备角色switch

    理论知识:Switchover 切换允许primary 和一个备库进行切换,并且这种切换没有数据丢失. 前提条件: 1) 主备库相关参数 fal_client.fal_server .standby_ ...

  5. Lagom学习 (三)

    lagom代码中有大量的Lambda表达式,首先补习一下lambda表达式和函数式接口的相关知识. 一: 函数式接口: 函数式接口其实本质上还是一个接口,但是它是一种特殊的接口: 这种类型的接口,使得 ...

  6. stm32之复位与待机唤醒

    一.复位 stm32复位有三种类型,分别为系统复位.电源复位和备份域复位. 其中系统复位又分为: NRST引脚低电平(外部复位) 窗口看门狗计数结束 独立看门狗计数结束 软件复位 低功耗管理复位 二. ...

  7. Python的subprocess子进程和管道进行交互

    在很久以前,我写了一个系列,Python和C和C++的交互,如下 http://blog.csdn.net/marising/archive/2008/08/28/2845339.aspx 目的是解决 ...

  8. HDU - 6383 百度之星2018初赛B 1004 p1m2(二分答案)

    p1m2  Accepts: 1003  Submissions: 4595  Time Limit: 2000/1000 MS (Java/Others)  Memory Limit: 131072 ...

  9. 小议IT公司的组织架构

    IT公司的组织结构还是很相似的,常见的部门也不多.我简单地总结了下,分享给各位.每个公司都有自己独特的组织架构,本文仅供参考.很多部门和职位的职责和权力,我也不甚了解.简单地写写,有兴趣的同学可以补充 ...

  10. 多版本Shader与multi_compile

    多版本Shader与multi_compile   https://docs.unity3d.com/Manual/SL-MultipleProgramVariants.html   #pragma ...