前言

Java语言中有许多原生线程安全的数据结构,比如ArrayBlockingQueue、CopyOnWriteArrayList、LinkedBlockingQueue,它们线程安全的实现方式并非通过synchronized关键字,而是通过java.util.concurrent.locks.ReentrantLock来实现。 刚好对这个很感兴趣, 因此写一篇博客详细分析此 “可重入锁实现原理”。

ReentrantLock的实现是基于其内部类FairSync(公平锁)和NonFairSync(非公平锁)实现的。 其可重入性是基于Thread.currentThread()实现的: 如果当前线程已经获得了执行序列中的锁, 那执行序列之后的所有方法都可以获得这个锁。

公平锁:

公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值。

锁的实现方式是基于如下几点:

表结点Node和状态state的volatile关键字。

sum.misc.Unsafe.compareAndSet的原子操作(见附录)。

非公平锁:

在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的。

ReentrantLock锁都不会使得线程中断,除非开发者自己设置了中断位。

ReentrantLock获取锁里面有看似自旋的代码,但是它不是自旋锁。

ReentrantLock公平与非公平锁都是属于排它锁。

ReentrantLock的可重入性分析

这里有一篇对锁介绍甚为详细的文章 朱小厮的博客-Java中的锁.

synchronized的可重入性

java线程是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的(java中线程获得对象锁的操作是以每线程为粒度的,per-invocation互斥体获得对象锁的操作是以每调用作为粒度的)

ReentrantLock的可重入性

前言里面提到,ReentrantLock重入性是基于Thread.currentThread()实现的: 如果当前线程已经获得了锁, 那该线程下的所有方法都可以获得这个锁。ReentrantLock的锁依赖只有 NonfairSync和FairSync两个实现类, 他们的锁获取方式大同小异。

可重入性的实现基于下面代码片段的 else if 语句

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
...
// 尝试获取锁成功
}
else if (current == getExclusiveOwnerThread()) {
// 是当前线程,直接获取到锁。实现可重入性。
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

此处有两个值需要关心:

/** * The current owner of exclusive mode synchronization. * 持有该锁的当前线程 */
private transient Thread exclusiveOwnerThread;

-----------------两个值不在同一个类----------------

/** * The synchronization state. * 0: 初始状态-无任何线程得到了锁 * > 0: 被线程持有, 具体值表示被当前线程持有的执行次数 * * 这个字段在解锁的时候也需要用到。 */
private volatile int state;

ReentrantLock锁的实现分析

公平锁和非公平锁

ReentrantLock 的公平锁和非公平锁都委托了 AbstractQueuedSynchronizer#acquire 去请求获取。

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

tryAcquire 是一个抽象方法,是公平与非公平的实现原理所在。

addWaiter 是将当前线程结点加入等待队列之中。公平锁会严格按照等到队列去取值,而非公平锁则是随机的。

acquireQueued 在多次循环中尝试获取到锁或者将当前线程阻塞。

selfInterrupt 如果线程在阻塞期间发生了中断,调用 Thread.currentThread().interrupt() 中断当前线程。

ReentrantLock 对线程的阻塞是基于 LockSupport.park(this); (见 AbstractQueuedSynchronizer#parkAndCheckInterrupt)。 先决条件是当前节点有限次尝试获取锁失败。

公平锁FairSync

公平锁的实现机理在于每次有线程来抢占锁的时候,都会检查一遍有没有等待队列,如果有, 当前线程会执行如下步骤:

if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}

其中hasQueuedPredecessors是用于检查是否有等待队列的。

public final boolean hasQueuedPredecessors() {
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());
}

非公平锁NonfairSync

非公平锁在实现的时候多次强调随机抢占:

if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}

与公平锁的区别在于新晋获取锁的进程会有多次机会去抢占锁。如果被加入了等待队列后则跟公平锁没有区别。

ReentrantLock锁的释放

ReentrantLock锁的释放是逐级释放的,也就是说在 可重入性 场景中,必须要等到场景内所有的加锁的方法都释放了锁, 当前线程持有的锁才会被释放!

释放的方式很简单, state字段减一即可:

protected final boolean tryRelease(int releases) {
// releases = 1
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;
}

ReentrantLock等待队列中元素的唤醒

当当前拥有锁的线程释放锁之后, 且非公平锁无线程抢占,就开始线程唤醒的流程。

通过tryRelease释放锁成功,调用LockSupport.unpark(s.thread); 终止线程阻塞。

见代码:

private void unparkSuccessor(Node node) {
// 强行回写将被唤醒线程的状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// s为h的下一个Node, 一般情况下都是非Null的
if (s == null || s.waitStatus > 0) {
s = null;
// 否则按照FIFO原则寻找最先入队列的并且没有被Cancel的Node
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 再唤醒它
if (s != null)
LockSupport.unpark(s.thread);
}

附录: CAS

上述文章以及ReentrantLock多次提到 sum.misc.Unsafe.compareAndSet。 这是一种CPU的原子操作技术。 需要CPU支持。 通过JNI调用本地的Native方法, 实现对内存元素的原子操作。

java.util.concurrent.atomic包里面的数字操作也是基于这项技术, 关于这项技术的介绍, 可以看JAVA CAS原理深度分析博客