哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。
手撸过JVM、内存池、垃圾回收算法、synchronized、线程池、NIO、三色标记算法…
今天分享一篇JUC包中AQS的一个方法的深度解读,可能你都没有关注过这个细节吧。
这篇文章适合对AQS有一定基础的童鞋学习。学完本篇文章就可以对AQS高级部分有更深入的理解:比如AQS中的节点什么时候会修改自己的waitStatus、Node.CANCELLED状态有何意义、cancelAcquire何时会运行……
尤其是cancelAcquire何时会运行,最近讲完AQS课被问得比较多。今天抽个空写篇文章分享下。cancelAcquire这个方法内部做了什么,后面写文章分享,这个方法的代码也很难理解。
国际惯例,先上代码。
这两段代码是AQS的源码,因为AQS被很多类共用,本篇文章是从ReentrantLock角度进行分析。
分析问题
当failed为true时才会执行方法cancelAcquire,那什么情况下failed为true呢?try代码段执行过程中出现异常。
那什么情况下try代码段执行过程中会出现异常呢?整个流程看下来,只有两个地方有可能:predecessor、tryAcquire。看下源码,都会抛出异常。
那真正导致try代码段执行过程中出现异常是因为哪一个呢?我们来逐个分析下。
如果是因为tryAcquire
肯定不可能。为什么这么说呢?看代码
如果tryAcquire没有被重写,程序根本执行不到方法acquireQueued。因为不会有线程拿到锁,不会有线程因抢不到锁入队列。
如果是因为predecessor
其实也不可能。虽然这里有抛出异常的代码,但是这段代码永远不会执行到。所以注释里有这句话。
The null check could be elided, but is present to help the VM
空检查可以省略,但存在是为了帮助VM
为什么说抛出异常那段代码永远不会执行到呢?方法predecessor是入队后执行的,AQS队列有这样的特点:入队后至少有两个节点,第一个节点永远是占有锁的线程对应的节点。
第三种可能
刚刚分析完我们好不容易找到的两条线索,经过缜密的分析,得出结论都不可能。于是有了第三种可能:第二个节点加锁失败,没有机会将failed改为true。有没有可能呢?来分析下
先回答第一个问题:什么时候第二个节点加锁失败呢?答案是非公平锁的时候。即占有锁的线程刚释放完锁,刚唤醒第二个节点或者,这时候恰好来了一个线程拿到了锁,这时候唤醒的第二个节点来抢锁就会抢锁失败。
虽然抢锁失败了,但是也不会执行到finally代码段,而是重新自旋设置闹钟,然后调用park阻塞自己等待再次唤醒。
结论一
你还能想到其他可能吗?我是想不到了。那cancelAcquire永远不会执行吗?
在ReentrantLock中,不管是公平锁还是非公平锁,cancelAcquire都不会运行。那道格李为什么这样写呢?这个有点像编译器编译sync时会生成两个monitorenter,一种保险策略吧。
还有一种可能:有可能是保持代码统一,反正用到的时候会用到,用不到的时候也不会被执行到。有的童鞋可能说,道格李这样的大神不可能犯这种低级错误吧。我倒不觉得这是一种低级错误,保持统一反映了代码洁癖、反映了先有思想后有代码。
结论二
那什么情况下方法cancelAcquire会运行呢?响应中断的程序中,句式如
我是子牙老师,喜欢钻研底层,深入研究Windows、Linux内核、JVM。如果你也喜欢研究底层,欢迎关注我的公众号【硬核子牙】