深入理解java同步、锁机制我们主要讲解了关于synchronized的实现和各种锁的类型,本节将尝试从源码的角度去理解可重入锁ReentrantLock的实现。由于个人水平有限,文中出现错误的地方还请指出,避免误导更多人。
要理解ReentrantLock需要先理解所有锁的基础。AQS(AbstractQueuedSynchronizer)主要利用硬件原语指令(CAS compare-and-swap),来实现轻量级多线程同步机制,并且不会引起CPU上文切换和调度,同时提供内存可见性和原子化更新保证(线程安全的三要素:原子性、可见性、顺序性)。AQS的本质上是一个同步器/阻塞锁的基础框架,其作用主要是提供加锁、释放锁,并在内部维护一个FIFO等待队列,用于存储由于锁竞争而阻塞的线程。AQS提供了两种类型的锁,一种是共享锁一种是独占锁。
AQS内部基于一个等待队列(双向)实现:
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
等待队列是一个CLH队列,并且它的头节点时虚拟节点。AQS中有个非常重要的成员变量private volatile int state;用它来标示当前锁的状态,比如对于非可重入锁来说0代表该锁空闲,1代表该锁已经被锁定。
下面是ReentrantLock最简单的使用方法:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
//在需要同步的地方先加上锁
lock.lock(); // block until condition holds
try {
//需要被同步语句
// ... method body
} finally {
//和sync块不同的是需要手动释放锁。并且在finally中
lock.unlock()
}
}
}
虽然加锁和解锁只有非常简单的两句话,但是这背后的实现确实非常复杂的,下面来慢慢探究其中的实现细节。
首先从整体看下类的结构:
ReentrantLock类中有三个内部类,Sync是另外两个类的父类,ReentrantLock的公平锁和非公平锁的实现就是通过Sync的两个子类NonfairSync和FairSync来完成的。默认ReentrantLock实现的是非公平锁,非公平锁虽然失去了公平但是获得了更好地吞吐量。
首先来看下ReentrantLock默认的lock方法也就是非公平锁的lock方法:
final void lock() {
// 如果锁没有被任何线程锁定且加锁成功则设定当前线程为锁的拥有者
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 否则执行acquire方法传入参数为1
acquire(1);
}
关键的逻辑在acquire也就是当有线程发生竞争的时候:
//首先调用tryAcquire方法来再一次尝试获取锁,如果成功则返回,否则执行acquireQueued方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//非公平锁的获取方法
final boolean nonfairTryAcquire(int acquires) {
//首先获取当前线程
final Thread current = Thread.currentThread();
//获取锁的状态
int c = getState();
//如果为0则继续通过原子操作设置state,如果成功则设置获取锁的线程为当前线程并返回成功
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//否则锁已经被某个线程获取到,判断是否为当前线程
else if (current == getExclusiveOwnerThread()) {
//如果是当前线程则将state+1,可以看出锁的可重入性就体现在这里
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置状态,并返回成功
setState(nextc);
return true;
}
//否则该锁已经被其他线程占用,因此后面需要考虑阻塞该线程。
return false;
}
//新增等待线程节点
private Node addWaiter(Node mode) {
//使用当前线程构造出新的节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//第一次tail肯定为空则走enq(node)入队列
Node pred = tail;
if (pred != null) {
//非第一次竞争
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
//入队列操作
private Node enq(final Node node) {
//乐观等待
for (;;) {
//第一次tail仍然为空
Node t = tail;
if (t == null) { // Must initialize
//当第一次产生竞争的时候初始化虚拟头结点,节省空间
Node h = new Node(); // Dummy header
//头结点h的下一个节点指向前面新建的节点
h.next = node;
//双向链表
node.prev = h;
//原子操作设置头结点,如果成功则尾节点为前面新建的节点,否则循环直到成功,如果同一时刻有其他线程set成功,则可能走else分支
if (compareAndSetHead(h)) {
//返回头结点
tail = node;
return h;
}
}
else {
//否则
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
下面用图来表示下入队的操作,假设这是第一次发生竞争操作,且当前线程抢到了设置头节点的权限则如下图:
时刻要注意头结点始终是虚拟节点,而此时new node也就是包含当前线程的节点是tail节点。实际上当header被设置好后,后面的入队从队尾进行,只有队尾有多线程的竞争,类似头部竞争,整个操作在else中。如上图此时阻塞住一个线程,如果阻塞住两个线程,则整个队列类似下面:
注意在addWaiter方法中如果不是第一次竞争,直接设置tail,不进入enq方法,入队操作成功后接下来是acquireQueued方法。
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
// 如果当前线程是head的直接后继则尝试获取锁
// 这里不会和等待队列中其它线程发生竞争,但会和尝试获取锁且尚未进入等待队列的线程发生竞争。这是非公平锁和公平锁的一个重要区别
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//如果当前线程获取到锁,这将头结点设置为当前节点,当前节点下一个节点设置为null
setHead(node);
p.next = null; // help GC
//返回中断状态false
return interrupted;
}
//如果当前线程未获取到锁或者不是头结点则进行下一步处理
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
}
看下关键的shouldPark...方法
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
//如果获取锁失败,判断是否应该挂起当前线程,可以预见挂起线程是有条件的
//这里需要先说明一下waitStatus,它是AbstractQueuedSynchronizer的静态内部类Node的成员变量,
//用于记录Node对应的线程等待状态.等待状态在刚进入队列时都是0,如果等待被取消则被设为Node.CANCELLED,
//若线程释放锁时需要唤醒等待队列里的其它线程则被置为Node.SIGNAL,还有一种状态Node.CONDITION这里先不讨论。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//waitStatus初始值为0,假设node前驱为头结点,但是没有获取到锁,则进入else中
int ws = pred.waitStatus;
//如果状态为-1则表示当前线程可以被安全挂起
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park
*/
return true;
//如果大于0表示前一个节点处于被取消的状态,直接剔除该状态的节点
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.
*/
//修改pred的状态为SINGAL -1,且不挂起当前线程
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果判断可以park当前线程则调用parkAndCheckInterrupt()方法。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //阻塞,即挂起;在没有unpark之前,下面的代码将不会执行;
return Thread.interrupted();//Thread.interrupted()只是设置中断状态,当前线程被中断过则返回true否则返回false
}
上面是lock的操作,有lock必然有unlock操作,下面再来看一下unlock的逻辑。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
/*head的waitStatus不为零表示它的后继在等待唤醒,
还记得AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire中对waitStatus的操作么?
waitStatus!=0表明或者处于CANCEL状态,或者是置SIGNAL表示下一个线程在等待其唤醒,CANCEL状态这里先不分析,
可以认为这里!=0即表示SIGNAL状态*/
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//tryRelease方法获取状态并减去releases的值,如果为0表示锁完全被释放
int c = getState() - releases;
//只有持有锁的线程才能进行释放动作
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
//锁被释放,独占线程设置为null
setExclusiveOwnerThread(null);
}
//更改状态
setState(c);
return free;
}
/**
* Wakes up node's successor, if one exists.
*唤醒node节点的后继
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
//如果当前节点为SIGNAL则修改为0
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 若后续节点为空或已被cancel,则从尾部开始找到队列中第一个waitStatus<=0,即未被cancel的节点
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;
}
//如果找到这么一个节点它的状态<=0则唤醒该节点中的线程
if (s != null)
LockSupport.unpark(s.thread);
}
以上就是ReentrantLock的lock和unlock的一个简单分析,该类中还有很多其他的方法值得我们去探究,有兴趣的同学可以结合源码自行分析。