前言
Java 5.0 增加了一种新的加锁机制:ReentrantLock。ReentrantLock 并不是一种替代内置锁加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。
正题
在开始编写文章前,有几个问题需要思考一下:
- 锁的类型
- 公平锁工作原理
- 非公平锁工作原理
在 ReentrantLock 的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。(在 Semaphore 中同样可以选择采用公平的或非公平的获取顺序。)非公平的 ReentrantLock 并不提倡“插队”行为,但无法防止某个线程在合适的时候进行“插队”。在公平的锁中,如果有另一个线程持有这个锁或者有其他线程在队列等待这个锁,那么新发出请求的线程将被放入队列中。在非公平的锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
1. 锁的类型
2. 公平锁工作原理
在 ReentrantLock 使用了一个内部类来实现公平锁的加锁和解锁的功能。如下所示:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
......
}
当请求 lock 时,会传参数 1,是对该锁加 1。
2.1 AbstractQueuedSynchronizer##acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先调用公平锁的覆盖方法 tryAcquire。该方法主要做两件事:
- 判断该锁有没有被其他线程占用。
- 判断是不是已经持有该锁的线程再次请求锁。
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;
}
如果 A 线程在请求一个已经被其他线程持有的锁时,系统会创建一个锁节点 Node,把该节点放到锁请求链表上。在获取锁的步骤中,在 ReentrantLock 对象的内部使用了 state 来记录当前锁状态。
在 Node 节点里有几个状态标记:
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
从线程获取锁到释放锁的这个过程中,用 waitStatus 来跟踪节点的状态变化。初始化节点 waitStatus 状态为 0。其节点 Node 包括以下状态之一:
- SIGNAL:该节点的后继节点被阻塞,所以当前节点在释放或取消时必须唤醒等待该锁的节点线程。 为了避免竞争,获取方法必须首先表明他们需要一个信号,重试原子获取,在失败时阻塞。
- CANCELLED:取消状态,当节点 Node 所在的线程被中断或者执行时间片用完被挂起了。当节点所属的线程处于该状态时不会再次阻塞。
- CONDITION:该节点当前处于条件队列中。在它状态改变前被用作同步队列节点,此时的状态将被设置为0。
- PROPAGGATE:共享锁允许多个线程同时获得该锁。 这在 doReleaseShared 中设置(仅用于头节点)以确保传播继续
2.2 AbstractQueuedSynchronizer##addWaite
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;
}
在每一个 Lock 对象里,都有一个保存锁请求的链表:
private transient volatile Node head;
private transient volatile Node tail;
在 addWaiter 方法上会把锁请求节点 Node 添加到链表的尾部。但是有一个疑问:头部节点在哪里创建?接下里就查阅头结点创建过程:
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;
}
}
}
}
通过对链表的复合操作来保证原子性:
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
通过反射来获取 ReentrantLock 的 head 字段。
当某个线程释放锁时,会唤醒下一个获得锁的线程,在 acquireQueued 方法通过循环方式更新锁和头结点的状态。这是至关重要的一步:
- 如果当前请求线程获得了锁,则更新头结点;
- 设置当前节点的前置节点 pred 的 waitStatus 状态为 SIGNAL;
- 如果当前线程请求的锁已被其他线程占用,则阻塞线程;
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);
}
}
在公平锁类型下,获取锁的顺序是 FCFS,获得锁的顺序和请求的到来的顺序相同。
在 acquireQueued 方法里面做的几件事里,先看看设置前置节点状态过程:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这一步是设置 pred 节点的状态为 SIGNAL。每一个节点的 pred 都会在这一步设置为 SIGNAL 状态。
如果当前线程没有获得锁请求的锁,则阻塞:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
主要调用 LockSupport.part() 方法来实现阻塞:
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
最终调用 native 方式实现阻塞:
public native void park(boolean var1, long var2);
2.3 ReentrantLock##unLock
在 ReentrantLock 使用 unLock 方法实现解锁。如下所示:
public void unlock() {
sync.release(1);
}
最终还是调用内部类 Sync 的 release 方法来释放锁。注意传参 1:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
接下来看下是如何操作锁变量的。
2.4 Sync##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 操作里主要的还是修改锁状态 state 的值。如果在同一个线程多次请求相同的锁,只是设置 state 的值。而且从上面可知:会为当前获取锁的节点设置锁的 waitStatus 为 -1。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
在同一线程内释放了所有对同一个锁的所有请求,则会唤醒下一个等待该锁的线程:
public native void unpark(Object var1);
3. 非公平锁工作原理
在 ReentrantLock 使用了一个内部类来实现非公平锁的加锁和解锁的功能。如下所示:
static final class NonfairSync extends Sync {
......
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
当发出请求锁的同时该锁变为可用,线程跳过队列中的等待线程并获得锁。当锁被某个线程持有时,新发出请求的线程被放入等待队列中。