最近学习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)队列访问控制管理:只有一个线程能够在同一时刻运行,其他的进入等待状态。每个线程都是一个独立的个体,它们自省地观察,当自己的前驱节点是头节点并且已经原子性地获取了状态,这个线程才能运行。
注意对锁的理解:1、锁自身的状态(加锁的次数、当前拥有锁的线程);2、等待在该锁上的等待线程队列。
线程进入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。
下面是基本的加锁流程图
5、锁的释放
在加锁的时候会涉及到线程的阻塞,因此在当前释放锁对象的时候,会涉及到等待队列上线程的唤醒。
锁的释放,其实涉及两步:1、设置锁对象本身的状态(state),也就是当前锁的加锁次数;2、在这个锁对象上等待队列中线程的唤醒(这个只有在锁对象完全被释放以后才会进行)。
设置锁对象本身的状态,由各个子对象自己完成。当然在释放锁的时候,会检查这个线程是否就是当前拥有此锁对象的线程。
下面是ReentrantLock内部锁对象的实现,同时tryRelease的返回会指示是否需要进行等待队列的唤醒。
protected final boolean tryRelease ( int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false ;
if (c == 0) {
free = true ;
setExclusiveOwnerThread( null );
}
setState(c);
return free;
}
如果tryRelease返回true,那就表明当前线程已经完全释放锁了,这个时候就需要从等待队列中唤醒线程。
public final boolean release ( int arg) {
if (tryRelease(arg)) {
Node h = head ;
if (h != null && h. waitStatus != 0)
unparkSuccessor(h);
return true ;
}
return false ;
}
如果等待队列存在,那就唤醒第一个满足条件的线程,unparkSuccessor中传入的是头节点。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node. waitStatus ;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node. next ;
if (s == null || s. waitStatus > 0) {
s = null ;
for (Node t = tail ; t != null && t != node; t = t. prev )
if (t. waitStatus <= 0)
s = t;
}
if (s != null )
LockSupport. unpark(s. thread );
}
注意这里的Node s=node.next;这里唤醒的是等待队列中的第一个节点(阻塞状态),也就是前驱节点是头节点的那个节点,这样的唤醒顺序,也正是与进入等待队列的顺序一致。