关于AQS——独占锁特性+共享锁实现(二)
五、可中断获取锁的实现(独占锁的特性之一)
我们知道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); }
该方法(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共享模式》