今天学习一下Java中lock的实现方式aqs
直接上图这是lock方法的实现类、分为公平锁和非公平锁两种。
先看非公平的实现方法、很暴力有木有,上来直接CAS(抢占锁的方法,是一个原子操作,没有学过的同学自行百度哦),如果抢占失败进入else方法,
这是else方法,进行了双&&短路运算符操作,咱们先从方法名去猜一猜,tryAcquire()这个方法应该是尝试去获取,如果返回false也就是获取不到才会去执行后面的acquireQueued(),看到这个queue我就联想到了aqs的数据结构双向等待队列。这些都是猜想,咱们继续看代码。
咱们接着看源码,这就是尝试获取锁调用的代码(非公平锁,公平锁等会看),if(c==0)表示锁未被占领然后就开始获取锁,获取成功返回true。else if表示当前获取锁的线程和持有锁的线程相等了,没错,这就是可重入锁了(可重入锁的好处就是不会发生死锁)。
综上所述,非公平锁是这么干的:首先上来直接获取锁(cas),如果获取不到发现当前锁没有被占领(if(c == 0))再次获取锁。
接着看公平锁是怎么干的。
并没有像非公平锁那样上来直接cas,温柔了很多。
公平锁同样分为两种情况,当c == 0表示当前锁没有被占领,公平锁比非公平锁多了hasQueuedPredecessors(),我猜这个方法是判断等待队列中是否存在节点,如果有节点就要遵循公平原则,没有节点才能cas去抢占,接下来看代码。
若 h == t则队列为空,直接返回false就会进入到抢占锁的代码中,咱们的猜测是正确的。
也就是说公平锁并没有像非公平锁一样一上来就抢占锁,也没有像非公平锁那样发现锁没有被占有就直接去抢占,而是先判断一下等待队列中还有没有其他线程。
看到这儿已经明白了公平锁和非公平锁是如何获取锁的,大家可以稍作休息,接下来接着看获取失败后是如何进入等待队列的,进入队列后又是如何再次获取锁的。
咱们先看addWaiter()方法,然后看acquireQueued(),大神写的代码太紧凑了。。。
首先创建了一个node节点包含当前线程,然后获取了尾部节点,如果尾部节点不为空,就尝试把该节点放到尾部。如果添加到尾部失败了就会进入enq()方法。我们接着看。
哈哈,一上来就是一个自旋,也就是说上一步的加入尾部顺利的话是不需要进入自旋的。
我们接着看自旋里干了什么,首先获取尾部节点,如果为空的话就初始化了头节点,并且将尾部节点指向头节点。
思考:也就说这个等待队列是在获取锁的时候才初始化的,对吧!并且初始化的队列是尾指向头的一个空节点(不包含线程信息new Node()构造函数没有传递任何内容)。
好,我们接着看代码,如果 t != null,就把当前节点设置为尾巴节点。
思考:为什么要自旋呢,一是为了初始化这个等待队列,二就涉及到了线程安全问题,这个队列是一个多条线程共享的资源,怎么保证线程安全问题呢,没错这儿的初始化队列方法和入队方法都是调用的保障线程安全的方法,即使发生了竞争导致失败了,依旧自旋继续尝试!总之最终要保证这个节点加入到等待队列中!
思考:如果是公平锁在加入等待队列的途中,发生了多条线程的竞争,会不会出现不公平的情况呢?
我们接着来看看acquireQueued(),继上一步的自旋成功入队以后,哈哈又开始自旋了,我们接着看自旋里做了什么。
node.predecessor()获取了当前节点的前一个节点,如果前一个节点是头节点就去抢占锁,(等待队列是一个双向fifo队列,并且头节点为空,这里的为空指的是node中的thread为null,setHead方法中体现)
思考:也就说抢占到锁的node不在等待队列中,排队最靠前的node在第二个节点,头节点永远是空的,没问题吧!
我们接着看shouldParkAfterFailedAcquire()和 parkAndCheckInterrupt()干了什么,需要注意的地方时这个if和上面的if是同级的,也就是说每次自旋都会反复执行。