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

我们知道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. 【Codeforces】Round #460 E - Congruence Equation 中国剩余定理+数论

    题意 求满足$na^n\equiv b \pmod p$的$n$的个数 因为$n \mod p ​$循环节为$p​$,$a^n\mod p​$循环节为$p-1​$,所以$na^n \mod p​$循环 ...

  2. kettle脚本定时任务不执行

    问题描述:在centos机器上部署了kettle脚本,每天定时跑一次,但是并没有成功跑,手动执行命令是可以的.而且写了一个测试的shell脚本也是可以执行的. 解决方案: 将2的错误输出,/usr/l ...

  3. 淘宝双十一页面(Flexible)

    以下demo点我下载 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...

  4. element el-input 自动获取焦点和IE下光标位置解决方法

    在实际开发中我们经常会碰到这样的场景,就是有input的地方都喜欢切换过去input自动获取焦点. 如果这个问题是在input中,很容易就实现了,但是element里面的el-input看源码,其实不 ...

  5. The specified named connection is either not found in the configuration, not intended to be used

    今天用EF遇到一个问题, The specified named connection is either not found in the configuration, not intended t ...

  6. 【转】Pro Android学习笔记(二):开发环境:基础概念、连接真实设备、生命周期

    在Android学习笔记(二):安装环境中已经有相应的内容.看看何为新.这是在source网站上的Android架构图,和标准图没有区别,只是这张图颜色好看多了,录之.本笔记主要讲述Android开发 ...

  7. JS---设置简易红绿灯

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

  8. 三台主机搭建LAMP(apache、mariadb、php)

    实验环境:均是CentOS7 httpd:172.16.254.88   2.4.6 PHP:172.16.250.140 5.4.16 mariadb:172.16.250.94 5.5.52 第三 ...

  9. JVM StackOverflowError vs. OutOfMemoryError

    if the computation in a thread needs a larger Java Virtual Machine stack than is permitted, the Java ...

  10. SSE优化指令集编译错误: inlining failed in call to always_inline 'xxx': target specific option mismatch xxx

    在用QtCreator编译SSE优化指令的时候,出现了如下错误, inlining failed in call to always_inline '__m128i _mm_packus_epi32( ...