简介
ReentranLock实现了Lock接口,接口很简单,成员如下:
ReentranLock内部主要是用AbstractQueuedSynchronizer来实现的,其中Sync就是实现AbstractQueuedSynchronizer抽象类,NofairSync和FairSync是 Sync的子类,分别对应非公平锁和公平锁的实现,实例化公平、非公平ReentranLock就是通过实例化NonfairSync和FairSync来实现的。
AQS的实现如下图所示:
内部用双向链表来保存等待锁线程,头节点为当前占用锁线程节点,后面的节点会使用for(;;)循环来获取锁,符合被中断条件阻塞线程在这个循环里,被唤醒后继续循环内自旋的获取锁。
从下面构造函数可以看到调用无参的构造函数会默认生成一个非公平锁,因为线程的阻塞和唤醒代价较大,所以非公平的方式总体上来说效率会更高。
AQS是current包中多个同步工具的基础,这里就包括了ReentranLock,它们就是内部使用AQS的定制化子类来实现同步。
从下图可以看到Sync实现或重写AQS中很有限个方法,主要是tryAcquire()方法和tryRelease()方法,这两个方法再AQS中只是简单抛出一个异常。
源码分析
lock()
可以看到lock()方法仅仅是调用sync的lock()方法。
注释中写明:
1、如果没有线程获取这把锁立即返回,并且把锁计数设置为1,即本线程获取锁成功。
2、如果当前线程已经获取这个锁了,立即返回,那么就把锁计数加1。这就是可重入。
3、如果锁由另一个线程持有,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到获取锁为止,此时锁持有计数设置为1。
由构造方法可以知道,sync根据是否是公平锁实例化不同的Sync,那么我们就分别从NonfairSync和FairSync分别分析他们的lock方法。
NonfairSync中lock的实现
/**
* 获取锁,尝试立即获取(即非公平)获取失败后尝试走正常获取锁的流程
*/
final void lock() {
//尝试使用cas,如果当前锁state字段为0,立即修改为1,并且设置当前线程为锁持有的线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
//失败,走正常的获取锁流程,这个方法是父类AQS实现的,在acquire方法中会调用tryAcquire()方
//法
else
acquire(1);
}
我们先来看一下父类AQS中acquire()方法里做了什么:
/**
* 1、调用tryAcquire()方法获取锁,获取成功返回,这个方法要AQS子类自己实现
* 2、第一步获取失败,调用addWaiter()方法将本线程作为一个节点添加到等待队列尾部
* 3、调用acquireQueued()方法,先自旋等待获取锁,如果符合阻塞条件阻塞该线程
* 4、如果在acquireQueued()方法中当前线程被阻塞会返回true,调用selfInterrupe(){Thread.currentThread.interrupe();}设置
* 当前线程中断位
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
对于NonfairSync类的tryAcquire()方法,它就是简单的调用非公平获取锁的方法nonfairTryAcquire(acquires),看一下其实现。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* 非公平的方式获取锁。
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//当前锁state值为0,则获取锁并返回true:设置state字段,设置当前锁持有线程为本线程
//这里就是非公平的具体做法,有一个新的线程要获取这把锁,不会管等待锁队列中的线程,直接尝试获取,如果当前锁状态刚好为0,那么这个新的线程直接获取锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//锁state字段不为0,但是本线程就是当前锁的持有者(重入),对state字段+当前要获取锁的参数,对于ReentrantLock就是+1,返回true
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
//判断state字段是否溢出,如果溢出抛异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//获取锁失败
return false;
}
由acquire()方法可知,如果尝试获取锁失败,那么需要调用addWorker()将当前线程包装为Node加入到等待锁的双向队列尾部,然后会调用acquireQueued()方法来操作此Node自旋的获取锁。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 等待当前节点的前驱节点为head节点,结束自旋的方式有两个,1、当前节点前驱节点就是head,2、调用LockSupport.park()中断循环,等待被unpark()唤醒再次进入循环
// 如果p的前驱节点是head节点,自旋的尝试获取锁,head节点是当前获取锁的节点,tryAcquire()
if (p == head && tryAcquire(arg)) {
//获取锁成功后将头节点设置为当前节点,setHead(node)方法中会把node.thread = null, node.prev = null
//所以头节点中并不记录是哪个线程持有锁,是由父接口AbstractOwnableSynchronizer.exclusiveOwnerThread(Thread类型)记录
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;//返回
}
if (shouldParkAfterFailedAcquire(p, node) && //判断当前线程是否应该被park()中断于此
parkAndCheckInterrupt()) //调用LockSupport.park(this)阻塞当前获取锁的线程,并且调用Thread.interrupted()返回线程是否被中断
//当前线程被LockSupport.unpark()后继续进行自旋,判断当前节点的前驱节点是否是head,是的话就尝试获取锁
interrupted = true;
}
} finally {
// tryAcquired()发生异常,failed不会被置false,这时就执行cancelAcquire方法
if (failed)
cancelAcquire(node);
}
}
我们再看一下shouldParkAfterFailedAcquire()方法是怎么判断当前请求锁线程是否应该被中断的。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点等待状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 前驱节点prev等待状态为SIGNAL,此时允许中断当前节点node中线程
*/
return true;
if (ws > 0) {
/*
* 前驱节点prev等待状态为CANCEL=1,从前驱节点prev往前遍历,依次从等待队列中移除连续的CANCEL状态节点
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 前驱节点prev等待状态为 0 or PROPAGATE,设置为SIGNAL,返回false,认为当前节点node中线程此次不应该被中断
* 可以看到如果node和prev在队列中的状态没有变化,对该节点再次调用这个方法就返回true了
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
FairSync中lock方法的实现
FairSync与NonfairSync之间的区别不大,就只有lock()方法和tryAcquire()方法不一样
lock()方法非常简单,对比非公平的lock()方法只是没有cas抢先获取锁的过程,直接使用acquire(1)尝试获取锁,acquire方法分析请看非公平锁中描述。实现如下:
final void lock() {
acquire(1);
}
tryAcquire()方法实现如下:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取锁state
int c = getState();
/**
* 锁的state为0,此时锁可以被占有
*/
if (c == 0) {
/**
* 只有这里和非公平锁不一样
* 1、判断等待队列中是否有比当前线程等待的更久的节点
* 2、没有比当前线程等待的更久的节点时占有锁(设置state字段,设置持有锁的线程)返回true
*/
if (!hasQueuedPredecessors() &&
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;
}
unlock()方法实现
unlock方法同样是调用Sync的方法来实现。
public void unlock() {
sync.release(1);
}
tryRelease()方法在Sync中实现,对于公平锁、非公平锁是一样的。我们先看一下AQS中release()方法的实现。
public final boolean release(int arg) {
if (tryRelease(arg)) { //释放锁成功,此时等待链表中节点可以来占有锁了
Node h = head;
/**
* 判断head节点状态是否非0,如果是的话,唤醒head节点的后继节点
* 个人理解为什么这么判断:
* 因为加入的Node中线程会有各自旋的过程,判断可以被阻塞后方可对Node中线程进行阻塞,这里当然也要判断headNode的后继Node中线程是否需要被唤醒
* addWaiter中如果第一次插入一个节点N1,会设置一个状态为0的head,首次插入节点后链表形式为“head<-->N1”。N1的等待状态为初值0,再次加入一个节点,这个节点会有个自旋的过程,
* 判断是否应该被阻塞的方法shouldParkAfterFailedAcquire中会判断它的前驱节点head状等待态是否为SINGLE(-1),如果是才会被阻塞,不是的时候会设置head等待状态为SINALE,
* 如果head等待状态为0,没有被后继Node线程设置过,那么后继节点肯定是未被阻塞的,那么也不需要调用LockSupport.unpark()唤醒
*/
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
//这时如果被阻塞的head节点的后继节点被唤醒,或者原本就在自旋,那么在acquireQueued()方法中,后继节点将获取锁的使用权,并且会把自己设置为head
return true;
}
return false;
}
tryRelease()方法实现分析如下:
protected final boolean tryRelease(int releases) {
//将锁state减去释放参数,这也是为什么加锁、解锁要成对出现的原因
int c = getState() - releases;
//检查释放锁的线程是不是锁持有者,不是抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//锁state被置0后说明锁持有者已经完全释放了锁的占用
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
tryLock()实现
非超时版本,有了上面的基础,看这个方法的实现就会发现非常简单
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// c=0时,当前锁未被占用,使用cas非公平的获取锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 当前锁就是被请求锁的线程占用了,加state
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 获取锁失败
return false;
}
这个方法可以说非常的简单了,需要注意的是它是非公平的方式获取的。
带有超时的版本:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// 这个方法是父抽象类AQS实现的
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted()) //判断线程是否被中断,如果被中断抛异常
throw new InterruptedException();
return tryAcquire(arg) || // 先尝试获取锁
doAcquireNanos(arg, nanosTimeout); //获取锁失败后尝试在有限时间内获取锁
}
它会先尝试获取锁,tryAcquire()在公平方式和非公平方式实现稍有区别,请参考前面的分析(基本逻辑就是:1、看当前锁是否未占用,2、如果被占用是否就是我自己占用的),如果失败了就会调用doAcquireNanos方法来使用超时时间获取锁,这个方法和前面分析的acquireQueued极其相似。
// 这个方法和acquireQueued()方法是相当的像,只是加入了阻塞时长,以及超时判断,就不展开说了,请参考前面acquireQueued的分析吧
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout; // 计算获取锁等待到的时间戳
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L) // 超时返回false,即获取锁失败
return false;
if (shouldParkAfterFailedAcquire(p, node) && // 达到阻塞条件,阻塞当前线程nanosTimeout时长
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
水平有限,能耐一般,如果有错误的地方,万望指出。