ReentrantLock中的lock和unlock
我们在使用并发编程大师Doug Lea提供的显示锁lock的时候,短短的一个lock和unlock即可实现元老级锁synchronized的功能,并且比其更灵活,功能更丰富。我们当然会好奇的发问,他到底是怎么做到的?Doug Lea到底有多牛?
1.lock方法
ReentrantLock是通过AQS实现的,在说明lock方法之前有必要提供下面这个继承图。在ReentrantLock中有三个内部类Sync、FairSync、NonfairSync,他们为ReentrantLock提供锁机制。
lock方法就是调用了Sync中的抽象方法,具体实现要看new ReentrantLock(boolean)指定的是公平锁还是非公平锁,true为公平锁,false为非公平锁,默认是false。下面是源码:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public ReentrantLock() {
sync = new NonfairSync();
}
public void lock() {
sync.lock();
}
要使用lock必须得重写AQS的tryAcquire()方法,因为AQS并没有实现tryAcquire,如下:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
我们这里以非公平锁为例来说明,下面是非公平锁对tryAcquire的实现:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
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;
}
好,到这里准备工作做完了,下面就对非公平锁lock实现原理加以说明。首先要弄懂lock()到底会有什么效果,如果成功获取锁,执行临界区代码,并且有可能再临界区还需要获取同一把锁,这就是锁重入;如果失败,进入同步队列park,等待唤醒。在获取锁的时候会调用AQS的acquire方法,调用路径和acquire源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter()主要是将node加入队列(双向链表),并不会改变waitstatus。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
acquireQueued()主要用途:死循环。如果当前node是head后一个节点就尝试获取一次锁,失败,就设置waitstatus=-1,使node节点park阻塞。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
注意:双向链表的头节点代指拿到锁的节点,但其thread属性为null,永远为null。每个节点的waitstatus状态保存在前驱节点上。
2.unlock方法
调用路径
release方法源码:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release源码中的tryRelease()方法由nonfairSync实现,源码如下:
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()的作用:state减1,state如果等于0,setExclusiveOwnerThread(null)。返回值,如果state不为0,返回false,反之返回true。里面包含重入情况。
目前为止,我们粗略的理清lock和unlock到底都干了些什么,下面是对两者在实际使用中是如何配合的,有哪些细节进行阐述。
从两种情况说明:头次抢锁时,获取锁成功和获取锁失败
1.获取锁成功
指的是在众多线程中第一个抢到锁,这时它不需要进入同步队列。获取锁成功的条件:state=0或者exclusiveOwnerThread为node(表示当前线程)。如果state为0,compareAndSetState(0, 1)原子设置state为1,setExclusiveOwnerThread(current)设置为当前线程,返回true,执行临界区代码。如果state不为0,current == getExclusiveOwnerThread(),表示重入,那么state再加1,返回true,执行临界区代码。不管哪种情况,执行完代码后都要释放锁,释放锁很简单,state减1,并setState,如果state为0,setExclusiveOwnerThread(null)返回true,这时就要唤醒head下一个节点,设置waitstatus=0,让其在park的地方苏醒,继续执行acquireQueued方法死循环的部分(尝试获取锁,获取不到设置waitstatus=-1,park);如果不为0只是setState,返回false。
2.获取锁失败
获取锁失败,说明有线程已经占用该锁。此时state不为0,并且exclusiveOwnerThread不为node(当前线程)。先进入同步队列,要是当前线程的前驱节点是Head,尝试获取锁,成功的话,设置为Head,返回,执行临界区代码;如果获取锁失败,设置waitstatus=-1,park。什么时候被唤醒呢?在拿到锁的线程执行完临界区代码,调用unlock,执行unparkSuccessor的时候唤醒。