博客的代码部分理解,建议通过观看免费课程:https://edu.csdn.net/course/play/25414/301464
在并发编程中一个很重要的概念就是锁,本文以java中对锁的实现为例子做一个分析。我们使用锁的场景一般是这样的:在程序中需要对某些关键代码块做并发控制,只有满足条件的线程允许访问。那么这里就有这样几个概念:锁的持有线程、等待线程、调度控制中心。其中调度控制中心起到的作用就是锁的分配,在java中这被称为同步器(Synchronizer),所有等待中的线程需要维护到一个数据结构中,队列便是一个很好的选择,这样java里锁机制实现的基类概念就出来了:AbstractQueuedSynchronizer(抽象队列同步器)。
接着讲讲并发,对同一个资源的同时多访问,就出现了并发;资源粒度小到最小,就是一个变量。如果对一个变量进行修改操作,是否都允许成功?我们定义一个有效性的概念,如果这次修改的结果是符合预期的,那就表示有效,否则就是无效(有副作用)的。那么如何判断是否符合预期呢?这个预期条件便是当前变量的初始值,只有与初始值一致才能进行修改;这就保证了我在操作的过程中该值没有变过(我没有覆盖其他线程的修改动作)。这样cas(compare and set)原语就出来了,事实上这就是java并发实现的最基础。AQS(AbstractQueuedSynchronizer)也是在这个基础上实现的锁机制。
对于一个代码块,访问线程进入前需要获取锁,出来后需要释放锁,显然这也是AQS的主要功能。到这里AQS的实现原理也就出来了:通过cas以及维护等待的线程队列,实现获取锁以及释放锁的动作。关键在于如何用cas在可能出现并发的点做好控制。下面对AQS的实现代码结构做具体分析。
AQS为申请锁的线程封装了一个Node对象,该对象中包含属性:关联线程thread,以及该节点的锁状态waitStatus。
AQS中有head、tail属性,分别表示Node队列的头与尾,还有一个state属性,这个属性与Node中的waitStatus不同,表示全局的锁状态。
对Node对象的waitStatus进行更新操作时均通过cas方式进行,waitStatus的取值如下:
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1; //取消竞争
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1; //需要通知
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;//条件等待
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;//
public final void acquire(int arg) {
//先尝试获取锁,不成功再新增节点到等待队列中,然后park(阻塞等待)
//其中tryAcquire方法在具体的aqs使用类中做实现,比如公平锁与非公平锁
//arg参数可以用来控制锁的释放条件,比如条件锁的实现,CountDownLatch,就可以通过acquire每次减少一个数值,直到减少到0
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
AQS 中对该队列的维护以及及时进行线程通知方面做得非常细致周密,在真正执行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;
}
//在真正park之前移除已经取消了的节点
// 如果真的有移除操作发生,则返回false,此时会在下一个循环中继续尝试获取锁
if (shouldParkAfterFailedAcquire(p, node)
&& parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)//如果前一个节点的状态为待通知,则表明当前的node没有机会获取锁
/*
* 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.
*/
//移除掉node前面所有状态为已取消的节点
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.
*/
//将pre节点的状态设置为待通知,此时还可以进行一次尝试,因为在这个过程中前面的节点还可能释放
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
public final boolean release(int arg) {
//如果释放锁成功,则去通知其他等待节点
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) //如果waitStatus=0,说明可能该节点线程自己还会去获取锁
unparkSuccessor(h);
return true;
}
return false;
}
在unparkSuccessor中,找到当前节点之后待通知的节点,对其进行unpack操作
/**
* Wakes up node's successor, if one exists.
*
* @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.
*/
//将当前节点的状态清除为0
int ws = node.waitStatus;
if (ws < 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.
*/
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);
}
在队列中等待并获得锁的过程中,如果出现异常,则需要取消当前节点对锁的等待。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
//移除前面已取消的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) { //设置新尾节点
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) { //waitStatus等于0时,表示后面那个节点的线程已经被唤醒去获取锁,这里将pred的waitStatus设置为SIGNAL,则其尝试机会只会有一次
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {//如果pred是头结点,或者pred的状态为已获得锁
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
通过对锁获取、释放中相关代码的分析,可以知道waitStatus在节点的维护以及是否通知、是否尝试获取锁等的判断中都起到了判断依据的作用,通常情况各节点的waitStatus取值如下:
在释放锁时waitStatus的变化如下:
通过分析包括后续的章节,读者可以发现:每当一个线程要获取锁时会将前一个节点的waitStatus从SIGNAL变为0,而从争抢锁状态进入到等待状态时会将前一个节点(也就是head节点)的waitStatus从0变为SIGNAL。所以判断某一个节点是否正在获得锁的方法就是判断该节点的前一个节点的waitStatus状态是否为0。
另一个关键点是state,在排它锁中,每次获取锁都要判断它的值,并将其加1,对它的修改也都通过cas原语进行。在共享锁的实现部分还会需要通过state判断当前锁的持有者是共享锁还是排它锁。