这篇从源码入手,深入了解ReentrantLock的非公平锁和公平锁的实现过程。之前已经写过ReentrantLock-基础,ReentrantLock的实现基于AQS,所以还需要先了解同步器AbstractQueueSynchronizer。
非公平锁
ReentrantLock lock = new ReentrantLock(false);
try {
lock.lock(); //加锁
TimeUnit.SECONDS.sleep(1);//模拟业务处理用时
} catch (InterruptedException e) {
...
} finally {
lock.unlock(); //释放锁
}
非公平锁获取锁的流程如下
1.NonfairSync.lock()
尝试以CAS方式将state从0更新为1。在ReentrantLock语境下,state=0表示当前锁未被任何线程持有,如果获取成功,将当前线程标记为锁的持有线程,加锁过程结束。如果失败,执行acquire(1)方法。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
setExclusiveOwnerThread方法定义在抽象类AbstractOwnableSynchronizer中,和ReentrantLock类在同一包下
private transient Thread exclusiveOwnerThread; //当前持有锁的线程
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
调用AbstractQueueSynchronizer#tryAcquire方法。tryAcquire(),addWaiter()和acquireQueued()这三个方法封装了加锁流程中的处理逻辑。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2.NonfairSync.tryAcquire(1)
在AQS中,tryAcquire是定义好的钩子函数,直接调用此函数会抛出异常,需要在子类继承AQS后重写该方法。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
具体实现由在ReentrantLock中,如果当前锁的状态为0,尝试通过CAS操作获取锁,将当前线程标记为锁的持有线程,加锁过程结束;否则判断是否当前线程和持有锁的线程相同,相同说明该锁被重入了,将state的值+1,获取成功;否则获取锁失败。
这里加入了判断锁状态是否为0的判断,如果在执行代码的过程中,之前占有锁的线程将锁释放,这里就可以直接获取锁,虽然降低了代码的可读性,但是提升了性能。后面类似代码,也是相同原理。
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;
}
如果这里返回true,那么if语句中判断就不会继续执行后续判断,如果返回false,则会继续执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
3.AbstractQueueSynchronizer.addWaiter(1)
此方法是将获取锁失败的线程加入安全队列
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //创建新节点,放入当前线程,mode为null
// 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); //插入Node至同步队列
return node;
}
如果队列为空,使用CAS方式构造新的空头结点,如果交换成功,则通过非同步的方式将尾节点指向头节点;否则尝试将尾节点指向当前节点,原尾节点的next指向当前节点。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//更新头节点时,原值必须为null
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
最外层的空条件for循环保证了所有获取锁失败的线程都能加入同步队列中,当队列为空时要先进行处理。需要注意的是,Node直接不能直接插入空队列,阻塞的线程由先驱节点进行唤醒。所以要插入一个空的Node作为Head,当锁释放时由Head来唤醒后续被阻塞的线程。所以Head可以表示当前获取锁的线程,但不一定真实持有该线程实例。
4.AbstractQueueSynchronizer.acquireQueued(node, 1)
线程加入同步队列后,会一直进行循环,尝试获取锁。在循环中,有两个if,第一个判断当前线程所在节点的前驱节点是否为头节点,如果是则继续尝试获取锁,获取成功,则设置当前节点为head,退出当前循环。第二个if判断是否需要阻塞当前线程。
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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)) { //如果前驱节点为head,则继续尝试获取锁
setHead(node); //获取成功,将当前节点指向头结点
p.next = null; // help GC
failed = false;
return interrupted;
}
//判断是否需要阻塞该线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire方法用来判断是否需要阻塞,如果pred节点为SIGNAL,则返回true;如果为CANCELLED,则回溯到不为CANCELLED的节点,并将此节点作为当前节点的prev节点;如果为初始化状态(0),则通过CAS操作将前一个节点状态改为SIGNAL。
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;
}
parkAndCheckInterrupt用于上面方法返回true时,进行阻塞线程,底层调用的LockSupport.park。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
非公平锁总结
以上就是非公平锁获取锁的全部流程,非公平锁之所以不公平,是因为两个原因:
- 线程并非在加入队列后才有机会获取锁,在加入队列之前会尝试获取锁
- 线程释放锁时,先修改state状态值,然后唤醒后续节点线程
如果在释放锁之后,其他线程尝试获取锁并获取成功,那么唤醒的后续节点会继续在队列中等待。甚至后续节点需要等待唤醒,在唤醒之后还要检查pred节点是否为head节点,相比之下未进入等待队列的节点获取锁的优势更大。这也就是为什么,在高并发的情况下,就会出现饥饿问题。
公平锁
公平锁相对非公平锁,会有更严格的条件限制。在同步队列中有线程已经等待的情况下,所有线程必须先加入同步等待队列。按照队列顺序依次获得锁。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
虽然公平锁保证了获取锁的顺序,但也增加了获取锁的成本。
公平锁与非公平锁的对比
非公平锁对锁的竞争是抢占式的(已经进入队列的除外),线程在进入等待队列前可以进行两次尝试,这大大增加了获取锁的机会。好处体现在两个方面:
- 线程不必加入等待队列,免去加入队列的操作,还节省了线程阻塞唤醒,上下文切换等开销。
- 减少CAS竞争,虽然CAS操作不会导致失败线程挂起,但不断尝试交换对CPU的浪费也不能忽视。