Java中的锁系列3
上一章我们讲了synchronized对象锁的各种级别。但是,有的时候会被人问到ReentrantLock是怎么实现的么?AQS是啥?公平锁和非公平锁怎么实现的么?排他锁和共享锁怎么实现么?
就算我们弄懂了前两章的内容,还是只能不好意思的回答:不会。
相关的类
在文章的开头,我自己画了一个相关的类的类图,让你可以在开始的时候有个大概的了解。在读文章的过程中,你也可以时不时回来看一下这个图,应该会有很大的收获。
ReentrantLock
首先我们从ReentrantLock的构造函数入手,来看下ReentrantLock是怎么实现的。
ReentrantLock有两种实现,FairSync公平锁的实现和NonfairSync非公平锁的实现。
ReentrantLock的lock和unlock也是调用的对应sync里的lock和unlock。
FairSync和NonfairSync都继承了Sync类,而Sync则继承了AbstractQueuedSynchronizer, AbstractQueuedSynchronizer也就是传说中的AQS,AQS继承了AbstractOwnableSynchronizer,我们可以把AbstractOwnableSynchronizer叫AOS(这个名字是我自己取的)。
NonfairSync
NonfairSync的代码特别少,只有两个方法lock()和tryAcquire()。
我看到这个代码第一映像是:不是应该lock()跟unlock()方法么,怎么会是lock()和tryAcquire()呢?
实际上,我们看下ReentrantLock的lock()方法和unlock()方法:
也就是说lock调的是NonfairSync的lock()方法,unlock调用的应该是NonfairSync的release()方法。
relase()
不管NonfairSync还是FairSync,release()方法都是在他们的父类AQS里实现的。这意味着,公平锁和非公平锁,释放锁的步骤是一样的。
下面是AQS里的release的代码。
这个就解释了为什么NonfairSync和FairSync里只有lock了。
lock()
NonfairSync里的lock()我们知道是干什么的了,那么tryAcquire()又是个什么鬼呢?
NonfairSync的实现里tryAcquire()只有一行,return nonfairTryAcquire(acquires)。
我们再来看下nonfairTryAcquire方法。
通过读nonfairTryAcquire()这个方法,我们知道,实际上这个方法就是试着通过state和对应的thread值去获取锁,获取成功了返回true,失败了返回false。
如果当前线程已经获取到锁了,那么再次调用lock的时候,nextc实际上是+1,然后将nextc设置到state里去。这样也实现了ReentrantLock的可重入性。
但是,问题来了,tryAcquire这个方法在哪里调用呢?
我们再回过头来看一下lock方法
compareAndSetState()方法就是对state的一个CAS操作,期望值0,修改值1。成功了,就意味着拿到锁了。
acquire()方法,是在AQS里实现的。我们看下acquire的代码。
好了,找到tryAcquire()的调用地方了:实际上在lock里调用的。只不过tryAcquire在AQS里是一个虚方法。真正实现其实还是在子类也就是NonfairSync和FairSync。
FairSync
我们在来看一下FairSync类的实现。
同样很简单,跟NonfairSync的区别, 无非就是lock方法少了一次CAS操作,然后tryAcquire方法多了一个hasQueuedPredecessors()的判断。
这意味着hasQueuedPredecessors()的判断,很可能是用来保证锁的公平性的。
锁的公平实际上就是指先进等待队列的先拿到锁。我们从hasQueuedPredecessors的方法名字中其实就可以看出,这个方法就是为了保证当前获取锁的线程前面没有其他等待获取锁的线程了,如果有其他线程在前面,当前线程就拿不到锁。这样也确实是可以保证得到锁的顺序为先来先得,也就是保证锁的公平性。
到这里对NonfairSync和FairSync的分析就告一段落了。lock已经转移到了对AQS里acquire方法的分析,unlock已经转移到了对AQS里release方法的分析。
AQS
AbstractQueuedSynchronizer,传说中的AQS。
AQS其实是一种变种的CLH锁。关于CLH的资料可以看这个blog:
http://blog.csdn.net/aesop_wubo/article/details/7533186。
前面我们说到了lock()已经转移到了AQS里的acquire()方法。那我们就从acquire()作为入口开始了解AQS。
Acquire获取锁的流程如下:
我们在来看下acquire()的代码:
首先通过tryAcquire()尝试获取锁,如果获取不到,就创建一个代表当前线程的节点加入到等待队列的尾部,这个节点的模式为EXCLUSIVE也就是排他模式,对应的还有一个模式是SHARE,也就是共享模式。
tryAcquire()方法我们前面已经讲过了,下面主要看一下addWaiter()方法和acquireQueued()方法。
addWaiter
通过addWaiter()的方法名,我们就可以知道这个方法是往等待队列里加入一个Node。
这个方法应该分为两部分来读:
tail != null和tail == null。
tail != null表示当前队列里有元素,那么就设置当前节点为队尾节点。
tail== null表示当前队列里没有元素,那么就执行enq()方法,将这个节点入队。
其实我也疑惑,这里直接调enq方法不就行了吗?为什么还要进行一次判断呢?希望有大神能给解释下~
我们再看下enq()方法
enq()方法采用的就是变种CLH算法,先看尾结点是否为空,如果为空就创建一个傀儡结点,头尾指针都指向这个傀儡结点,这一步只会在队列初始化时会执行;
如果尾结点非空,就采用CAS操作将当前结点插入到尾结点后面,如果在插入的时候尾结点有变化,就将尾结点向后移动直到移动到最后一个结点为止,然后再把当前结点插入到尾结点后面,尾指针指向当前结点,入队成功。
acquireQueued
我们再来看一下acquireQueued()方法。
将新加入的结点放入队列之后,这个结点有两种状态,要么获取锁,要么就挂起。
如果这个节点的前趋节点是头节点,就会用tryAcquire()来尝试获取锁。如果成功,就将当前节点设置为头节点,并且将前趋节点设置为null,这样方便前趋节点被GC回收。如果失败就继续在for循环里获取,直到获取成功。
如果这个结点不是头结点,就看看这个结点是否应该挂起,如果应该挂起,就挂起当前结点,是否应该挂起是通过shouldParkAfterFailedAcquire()方法来判断的。
看到这里的Node.SIGNAL,可能很多人也疑惑了,这些状态都代表什么意思呢?
CLH锁的本质是一个FIFO的队列,既然是队列, 那肯定有一个个的节点。
CLH锁的节点里,只有true和false的状态。但是AQS的节点有很多。
具体的状态值如下:
static final int CANCELLED =1;
static final int SIGNAL= -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
下面解释下这些节点状态的具体含义:
CANCELLED:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;
SIGNAL:表示这个结点的继任结点被阻塞了,到时需要通知它;
CONDITION:表示这个结点在条件队列中,因为等待某个条件而被阻塞;
PROPAGATE:使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;
0:None of the above,新结点会处于这种状态。
了解了状态,我们再来看看这个方法。
该方法有两个参数,pred和node。
pred为当前节点的前趋节点。
node为当前节点。
该方法首先检查前趋结点的waitStatus位,如果为SIGNAL,表示前趋结点会通知它,那么它可以放心大胆地挂起了;
如果waitStatus>0也就是前趋结点是一个被取消的结点怎么办呢?那么就向前遍历跳过被取消的结点,直到找到一个没有被取消的结点为止,将找到的这个结点作为它的前趋结点,将找到的这个结点的waitStatus位设置为SIGNAL,返回false表示线程不应该被挂起。
如果不被挂起 , 那就会一直在acquireQueued方法的for循环里执行。
对于AQS,我自己也在读代码的阶段,如果后面发现有什么不对的地方,或者有理解更透彻的地方,我会更新本章节,也欢迎大家评论指正。
参考资料: