juc包之ReentrantLock
jdk1.5之后新增了ReentrantLock类,可以在代码里更灵活的控制同步代码块,那么这一块是如何实现的呢?
示例代码
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
try {
//something to do
} finally {
reentrantLock.unlock();
}
第一行初始化类对象

可以看到默认构造函数初始化的是一个非公平锁

还有一个重载的构造函数,通过传入布尔值,来控制初始化公平锁还是非公平锁,接下来先看非公平锁的实现
第二行上锁

此处接着执行非公平锁的lock方法

首先执行compareAndSetState方法将state变量从0变成1,一开始state变量值默认为0,所以第一个执行lock方法的线程将会执行成功,然后执行setExclusiveOwnerThread方法,将当前线程对象赋值到exclusiveOwnerThread变量上,代表当前独占的线程。
假如在之前已经有线程执行过lock方法了,已经修改过state变量值了,此时compareAndSetState方法就会执行失败,走下方acquire方法逻辑,acquire方法为从非公平锁父类AbstractQueuedSynchronizer继承而来的方法

- 首先执行tryAcquire方法,此处最终调用到了Sync类的nonfairTryAcquire方法

先获取当前执行的线程,再获取当前的state值。如果state值为0,代表在这期间占有锁的线程已经释放锁了,则跟lock方法一样,首先调用compareAndSetState方法修改state变量的值,修改成功则调用setExclusiveOwnerThread修改当前独占锁的线程对象,并返回true代表获取锁成功。
如果state值不为0,但当前请求锁的线程就是当前锁的独占线程,即拥有锁的线程又一次请求了锁,则给state值加上请求的资源数,并判读state值是否溢出了,如果溢出则代表当前线程请求锁资源过多,则直接抛出错误(此处溢出得这个线程请求锁超过21亿次,挺难的)。没有溢出则去修改state值,并返回true。
如果以上两种方式都不符合,则返回false,代表获取锁失败。(在这里的第一种判断是假设在执行后续逻辑前其他线程又释放了锁,则可以执行上锁并返回,第二种判断是为了实现锁的可重入,如果还是当前持有锁的线程在获取锁,则可以继续让它上锁)
- 再执行acquireQueued方法,在执行前先执行了addWaiter方法,并说明是独占节点。

先看addWaiter方法

先构造Node节点,传入当前线程对象,以及锁类型,此处为Exclusive独占
然后将当前链表最后一个节点赋值给pred变量,如果pred变量不为null,则将node的前继节点设置为pred,并且将node设置成链表的最后一个节点,如果设置成功将之前链表的最后一个节点的后驱指向node,并返回node。(即是将node追加到链表的最后一个,并且将node赋值给代表链表最后一个节点的变量)
如果pred变量为null,或者之前设置tail节点失败的话,则调用enq方法传入node节点,并返回node

enq方法内部是个死循环,首先判断tail变量是否有值,如果无值则代表之前都还没有链表,则新建了一个node对象设置到head变量,代表是头节点,同时当下只有一个节点,则head也是tail。
然后第二次循环时,tail变量将不再是null,此时执行上面的逻辑,将当前node设置成tail,并链接到上一个tail变量后形成链表。
再看acquireQueued方法

在死循环里,首先获取节点的前继,head节点由于在上面链表初始化的时候是单独new没有其他作用,所以此处判断前继是head节点时,即代表当前节点是第一个排队等待锁的,则调用tryAcquire方法去获取锁。
如果获取成功的话,则将当前节点设置成head了,并返回false,代表不中断。
如果前一个节点不是head节点或者获取锁失败的话,则调用shouldParkAfterFialedAcquire方法

会判断前继节点的waitStatus状态,首先判断是否是signal状态(该值为-1,node的初始值是0),接着判断是否大于0,也不满足,则走分支将前继节点的waitStatus修改为signal,并返回false,然后再第二次进入则由于已经改为了signal,则会返回true,然后走parkAndCheckInterrupt方法,该方法将调用LockSupport的park方法挂起当前线程等待其他线程唤醒,如果其他线程唤醒了这个线程,则获取当前线程是否已经被中断了并返回
此处还有个逻辑就是前继节点的waitStatus大于0的情况,即前继节点被取消了.则会一直往前遍历,直到遍历到一个未取消的前继节点,然后将其后驱节点设置成当前节点(此处就可以明白为嘛一开始创建链表的head节点为什么要new一个空Node了,空Node的waitStatus要么为0,要么为-1,不可能大于0,这样后面的节点就算全部取消了,一直往前遍历前继节点也不会有null值的情况)
(此处看下来感觉也就是挂起有用,判断中断没啥用,也跳不出循环,除非后续真的中断抛出错误了)
这里面的finally模块,感觉只有异常才会执行到cancelAcquire方法(__暂时不看了__)
在acquireQueued方法里如果获取到锁成功返回的话,即lock方法执行完毕不阻塞了,则会继续往下执行同步代码快逻辑。
总结如下,调用lock方法首先去修改state变量的值,从0修改到1,修改成功即获取锁,并将当前线程对象设置给锁的独占线程变量上。然后如果修改失败了,则判断当前线程是不是独占锁的那个线程,如果是,则允许重入,即给当前线程放行,state变量继续往上加。如果以上都没成功的话,则将当前线程对象封装成一个node节点,放入锁的链表对象里,按照先入先出的规则,遍历链表的node节点去获取锁,直到获取成功或者线程被中断
第三行释放锁

调用了父类AbstractQueuedSynchronizer继承而来的方法

- 先看tryRelease方法

计算当前state的值减去释放锁的数量,赋值给c变量,然后先判断当前线程是否是独占线程,不是则抛错。是的话判断c值是否是0,0代表当前线程已经不需要锁了,则设置独占线程变量为null,并设置free变量为true。然后将c变量值赋值给state变量,并返回free的值。
- 再回头看release方法
如果tryRelease返回了false,则代表锁目前还有线程占有,则不做其他处理了
如果返回了true,代表锁已经没有线程独占了,则获取head变量,并判断waitStatus状态是否不为0了(在lock时第一次获取不到锁时,会修改node的waitStatus为singal,值为-1),如果是,则调用unarkSuccessor方法

判断节点的waitStatus是否小于0,小于0则改为-1
然后获取节点的下一个节点,之前lock方法挂起的就是下一个节点
如果下一个节点为null或者waitStatus大于0(大于0只有canceled一种可能),则从tail往前遍历,直到遍历到一个waitStatus的值小于0的
然后调用LockSupport的unpark方法去唤醒那个线程.
总结: 非公平锁就是先获取锁,并且获取到锁的线程可重复获取锁,然后获取不到锁的线程会形成链表并挂起,当拥有锁的线程将资源全部释放后,会先按照先进先出的原则遍历节点,但当如果正向遍历的节点线程被取消了,则就从尾部反向遍历,直到遍历到一个在等待运行的线程,就去唤醒它.由于每次都是先尝试上锁,上锁失败才会去队列排队,会导致可能新线程直接获取锁成功,队列里的还继续在排队,所以叫非公平锁
再看下公平锁
示例代码
ReentrantLock reentrantLock = new ReentrantLock(true);
reentrantLock.lock();
try {
//something to do
} finally {
reentrantLock.unlock();
}
在构造ReentrantLock类时传入true,使用公平锁
- 先看lock方法

没有像非公平锁一样先去尝试加锁,而是直接调用了父类AbstractQueuedSynchronizer的acquire方法,跟非公平锁一样,会先调用tryAcquire方法,这个方法公平锁有重载

当state值为0时,首先会调用hasQueuedPredecessors方法,只有当这个方法返回false时,才会往下走去尝试上锁

该方法想返回false,有以下几种情况
- head与tail相等
- head与tail不等,但是head的下一个节点不为null且下一个节点的线程是当前线程
第一种情况是当前没有等待队列了,第二种情况是当前队列在队首(因为head节点都是空Node,head的下一个才算是第一个等待的),所以当前线程都可以继续往下去获取锁.
除了获取锁的方式与非公平锁不一样,其他环节都是一样的
总结,公平锁可以看出来不管何时去获取锁,都得先判断当前线程是不是队列里第一个,是才能去获取锁,不是只能加入队列里等待.这样先去申请锁的永远能先运行,所以公平
juc包之ReentrantLock