JUC AQS ReentrantLock源码分析

Java的内置锁一直都是备受争议的,在JDK

1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchronized还是存在一些缺陷的:虽然synchronized提供了便捷性的隐式获取锁释放锁机制(基于JVM机制),但是它却缺少了获取锁与释放锁的可操作性,可中断、超时获取锁,且它为独占式在高并发场景下性能大打折扣。

如何自己来实现一个同步

自旋实现一个同步

JUC AQS ReentrantLock源码分析

图1

缺点:耗费cpu资源。没有竞争到锁的线程会一直占用cpu资源进行cas操作,假如一个线程获得锁后要花费Ns处理业务逻辑,那另外一个线程就会白白的花费Ns的cpu资源
解决思路:让得不到锁的线程让出CPU

yield+自旋实现同步

JUC AQS ReentrantLock源码分析

图2

要解决自旋锁的性能问题必须让竞争锁失败的线程不空转,而是在获取不到锁的时候能把cpu资源给让出来,yield()方法就能让出cpu资源,当线程竞争锁失败时,会调用yield方法让出cpu。

自旋+yield的方式并没有完全解决问题,当系统只有两个线程竞争锁时,yield是有效的。需要注意的是该方法只是当前让出cpu,有可能操作系统下次还是选择运行该线程,比如里面有2000个线程,想想会有什么问题?

sleep+自旋方式实现同步

JUC AQS ReentrantLock源码分析

图3

缺点:sleep的时间为什么是10?怎么控制呢?很多时候就算你是调用者本身其实你也不知道这个时间是多少

park+自旋方式实现同步

JUC AQS ReentrantLock源码分析

图4

这种方法就比较完美,当然我写的都伪代码,我看看大师是如何利用这种机制来实现同步的;JDK的JUC包下面ReentrantLock类的原理就是利用了这种机制;

ReentrantLock源码分析之上锁过程

AQS(AbstractQueuedSynchronizer)类的设计主要代码(具体参考源码)

JUC AQS ReentrantLock源码分析

图5

AQS当中的队列示意图

JUC AQS ReentrantLock源码分析

图6

Node类的设计

JUC AQS ReentrantLock源码分析

图7

上锁过程重点

锁对象:其实就是ReentrantLock的实例对象,下文应用代码第一行中的lock对象就是所谓的锁

自由状态:自由状态表示锁对象没有被别的线程持有,计数器为0

计数器:再lock对象中有一个字段state用来记录上锁次数,比如lock对象是自由状态则state为0,如果大于零则表示被线程持有了,当然也有重入那么state则>1

waitStatus:仅仅是一个状态而已;ws是一个过渡状态,在不同方法里面判断ws的状态做不同的处理,所以ws=0有其存在的必要性

tail:队列的队尾 head:队列的对首 ts:第二个给lock加锁的线程 tf:第一个给lock加锁的线程 tc:当前给线程加锁的线程

tl:最后一个加锁的线程 tn:随便某个线程

当然这些线程有可能重复,比如第一次加锁的时候tf=tc=tl=tn

节点:就是上面的Node类的对象,里面封装了线程,所以某种意义上node就等于一个线程

首先一个简单的应用

JUC AQS ReentrantLock源码分析

图8

公平锁lock方法的源码分析

JUC AQS ReentrantLock源码分析

图9

非公平锁的looc方法

JUC AQS ReentrantLock源码分析

图10

下面给出他们的代码执行逻辑的区别图

JUC AQS ReentrantLock源码分析

图11

公平锁的上锁是必须判断自己是不是需要排队;而非公平锁是直接进行CAS修改计数器看能不能加锁成功;如果加锁不成功则乖乖排队(调用acquire);所以不管公平还是不公平;只要进到了AQS队列当中那么他就会排队;一朝排队;永远排队记住这点

acquire方法方法源码分析

JUC AQS ReentrantLock源码分析

图12

acquire方法首先会调用tryAcquire方法,注意tryAcquire的结果做了取反

tryAcquire方法源码分析

JUC AQS ReentrantLock源码分析

图13

hasQueuedPredecessors判断是否需要排队的源码分析

这里需要记住一点,整个方法如果最后返回false,则去加锁,如果返回true则不加锁,因为这个方法被取反了

JUC AQS ReentrantLock源码分析

图14

JUC AQS ReentrantLock源码分析

图15

JUC AQS ReentrantLock源码分析

图16

到此我们已经解释完了!tryAcquire(arg)方法,为了方便我再次贴一下代码

JUC AQS ReentrantLock源码分析

图17

acquireQueued(addWaiter(Node.exclusive),arg))方法解析

如果代码能执行到这里说tc需要排队

需要排队有两种情况—换言之代码能够执行到这里有两种情况:

1、tf持有了锁,并没有释放,所以tc来加锁的时候需要排队,但这个时候—队列并没有初始化

2、tn(无所谓哪个线程,反正就是一个线程)持有了锁,那么由于加锁tn!=tf(tf是属于第一种情况,我们现在不考虑tf了),所以队列是一定被初始化了的,tc来加锁,那么队列当中有人在排队,故而他也去排队

加锁过程总结

如果是第一个线程tf,那么和队列无关,线程直接持有锁。并且也不会初始化队列,如果接下来的线程都是交替执行,那么永远和AQS队列无关,都是直接线程持有锁,如果发生了竞争,比如tf持有锁的过程中T2来lock,那么这个时候就会初始化AQS,初始化AQS的时候会在队列的头部虚拟一个Thread为NULL的Node,因为队列当中的head永远是持有锁的那个node(除了第一次会虚拟一个,其他时候都是持有锁的那个线程锁封装的node),现在第一次的时候持有锁的是tf而tf不在队列当中所以虚拟了一个node节点,队列当中的除了head之外的所有的node都在park,当tf释放锁之后unpark某个(基本是队列当中的第二个,为什么是第二个呢?前面说过head永远是持有锁的那个node,当有时候也不会是第二个,比如第二个被cancel之后,至于为什么会被cancel,不在我们讨论范围之内,cancel的条件很苛刻,基本不会发生)node之后,node被唤醒,假设node是t2,那么这个时候会首先把t2变成head(sethead),在sethead方法里面会把t2代表的node设置为head,并且把node的Thread设置为null,为什么需要设置null?其实原因很简单,现在t2已经拿到锁了,node就不要排队了,那么node对Thread的引用就没有意义了。所以队列的head里面的Thread永远为null


分享到:


相關文章: