最近学习Jdk的源代码时,读到了ConcurrentHashMap的源码实现时,发现每一个分段Segment都是ReentrantLock类型,于是顺带对ReentrantLock的源代码进行了学习。。在这里做一个笔记总结。因为只有在工作之余才能有空看看,所以思路有点零散,仅供参考。。。。
1、如何确定哪个线程可重复进入该锁
在获取锁的时候,首先会检查当前同步对象的阻塞状态,如果已经是被某个线程持有,会检查持有的线程是否就是当前线程。同步对象有一个exclusiveOwnerThread属性用来表征占有此同步对象的线程。
如果当前线程就是持有该同步对象的线程,那么就不用阻塞。
具体逻辑通过下面代码可以表明(FairSync类),可以与nonfairTryAcquire来对比去理解公平锁和非公平锁的含义
protected final boolean tryAcquire(int acquires)
{
final Thread
current = Thread.currentThread();
int c
= getState();
if (c
== 0) {
if (isFirst(current)
&&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true ;
}
}
else if (current
== getExclusiveOwnerThread()) {
int nextc
= c + acquires;
if (nextc
< 0)
throw new Error("Maximum
lock count exceeded" );
setState( nextc);
return true ;
}
return false ;
}
}
2、同一线程多次进入临界区
通过检查拥有同步对象是否为当前线程来确认是否可以运行当前线程进入临界区。同步对象有一个state字段来表明,拥有此同步对象的线程,进入临界区的次数。
3、什么时候临界区是没有加锁状态
同步对象的state字段为0,则表示当前没有任何线程拥有此同步对象。
4、实现的原理和逻辑
内部逻辑通过Sync对象来实现加锁和解锁,重点关注AbstractQueuedSynchronizer抽象类的实现。
使用一个非循环的双向链表(FIFO)来维护等待线程队列,一个线程在进行状态获取的时候,如果获取不能马上获取成功,就会加入到这个队列的队尾中。这个队列中的线程能够运行的条件如“队列访问控制管理”的描述
AbstractQueuedSynchronizer的等待(sync)队列访问控制管理:只有一个线程能够在同一时刻运行,其他的进入等待状态。每个线程都是一个独立的个体,它们自省地观察,当自己的前驱节点是头节点并且已经原子性地获取了状态,这个线程才能运行。
线程进入sync队列之后,接下来就是要进行锁的获取,或者说是访问控制了,只有一个线程能够在同一时刻继续的运行,而其他的进入等待状态。而每个线程都是一 个独立的个体,它们自省的观察,当条件满足的时候(自己的前驱是头结点并且原子性的获取了状态),那么这个线程能够继续运行。
AbstractQueuedSynchronizer维护的队列中线程状态比较有意思:SIGNAL是表明当前节点的下一个节点需要unparking(当成解锁来理解),另外两个状态(CANCELLED和CONDITION则都是表示自身的状态)
AbstractQueuedSynchronizer的解析:
state,当前的同步状态,0表示未加锁,非0表示已加锁,同时对于ReentrantLock来说,这个值表示同一个进程加锁的次数
AQS同步器的核心主要是acquireQueued和shouldParkAfterFailedAcquire
acquireQueued用来处理AbstractQueuedSynchronizer的等待队列,同时检查某一个节点是否能够进入临界区。代码如下
final boolean acquireQueued(final Node
node, int arg) {
try {
boolean interrupted
= false;
for (;;)
{
final Node
p = node.predecessor();
if (p
== head && tryAcquire(arg)) {
setHead(node);
p. next = null; //
help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire (p,
node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (RuntimeException
ex) {
cancelAcquire(node);
throw ex;
}
}
final Node p = node.predecessor();和if (p
== head && tryAcquire(arg)) 用来检查要检查的节点是否满足进入临界区的条件:当前节点是等待队列中的第一个节点(前驱节点是头节点p==head),同时能够成功加锁(tryAcquire返回true)。如果满足状态,就移动头指针。
如果要检查的节点不满足加锁条件,那就执行到shouldParkAfterFailedAcquire 方法来讲线程进行阻塞
private static boolean shouldParkAfterFailedAcquire(Node
pred, Node node) {
int ws
= pred.waitStatus ;
if (ws
== Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park
*/
return true ;
if (ws
> 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node. prev =
pred = pred. prev;
} while (pred.waitStatus >
0);
pred. next =
node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don‘t park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false ;
}
shouldParkAfterFailedAcquire方法用来检查线程是否满足阻塞的条件,同时会清理掉队列中一些已经过期的节点(已取消,节点的waitStatus大于0),检查原则:
- 规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞
- 规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞
- 规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同
注意对shouldParkAfterFailedAcquire的调用是在acquireQueued的一个无限循环中调用的,但这个循环最终肯定是会有出口的,就在于shouldParkAfterFailedAcquire会修改前驱节点的状态,最后会使得方法调用到parkAndCheckInterrupt中,完成线程的阻塞。即便最终队列中就只剩下头节点(只是作为头节点标记)和当前节点,无限循环也是有出口,因为初始构造的头节点的waitStatus是为0的。所以最后,会把头节点的waitStatus设置为Node.SIGNAL,这样就会导致对当前节点调用parkAndCheckInterrupt方法。
完成线程阻塞:
对线程完成阻塞是在parkAndCheckInterrupt方法中调用的,通过LockSupport类来实现的。
设置当前线程的阻塞对象(每个线程有一个parkBlocker属性),然后通过系统调用实现线程的阻塞。线程恢复运行后,再把线程的阻塞对象设置为null。
下面是基本的加锁流程图