CLH锁
AQS是CLH锁的变形实现,首先我们先了解下CLH锁
CLH锁,是根据作者的名字简称命名,优点:无饥饿,先到先服务的公平性,下面的实现代码是最简单的一种实现方式。可以看到实现方式是一种虚拟队列(链式结构)并不是一个真正存在的队列在维护节点
/**
* @author 会灰翔的灰机
* @date 2019/8/6
*/
public class CLHLock {
public static class CLHNode {
// 默认是在等待锁
private volatile boolean isWaiting = true;
}
// 尾巴节点
private volatile CLHNode tail ;
// 尾巴节点的CAS更新者
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
. newUpdater(CLHLock.class, CLHNode .class , "tail" );
public void lock(CLHNode currentThreadCLHNode) {
CLHNode preNode = UPDATER.getAndSet( this, currentThreadCLHNode);
if(preNode != null) {
//已有线程占用了锁或者已有排队等待获取锁的节点,进入自旋,轮询前置节点的状态
while(preNode.isWaiting ) {
}
// 前置节点通知当前节点结束自旋获取锁成功
}
}
public void unlock(CLHNode currentThreadCLHNode) {
// 1. 操作成功说明当前节点是tail尾巴节点,释放节点即可
// 2. 操作失败所有当前节点不是tail尾巴节点,更改当前节点状态
if (!UPDATER .compareAndSet(this, currentThreadCLHNode, null)) {
// 当前节点不是tail尾巴节点,改变状态,让后续线程结束自旋
currentThreadCLHNode.isWaiting = false ;
}
}
}
AQS实现
Node结构
// CLH Node 节点
static final class Node {
/** 标识当前节点是一个共享节点,在实现读写锁的读节点时会使用该类型的节点,
代表当前节点是共享模式也就是读模式,如果下一个节点也是共享模式则可以继续唤醒下一个节点,
实现并发读,写入的时候使用的是排他节点 */
static final Node SHARED = new Node();
/** 排他节点 */
static final Node EXCLUSIVE = null;
/** 该waitStatus值表示线程已取消 */
static final int CANCELLED = 1;
/** 该waitStatus值表示执行成功的线程需要unpark唤醒子节点 */
static final int SIGNAL = -1;
/** 该waitStatus值表示线程正在等待条件满足 */
static final int CONDITION = -2;
/**
* 该waitStatus值表示下一个申请共享类型的节点应无条件的传递,例如:并发读锁
*/
static final int PROPAGATE = -3;
/**
* 状态属性,仅有以下值:
* SIGNAL: 节点的子节点是(或者即将变为)阻塞(由park阻塞),
* 那么当前节点必须unpark唤醒它的子节点当节点释放时或者取消时.
* 避免竞争, acquire(获取锁的) 方法必须首先表示他们需要一个signal信号,
* 然后重试获取锁这个原子操作, 要么失败,阻塞。
* CANCELLED: 节点由于超时或中断而取消.
* 节点永远不会离开此状态(即不会由此状态变为其他状态)。
* 特别是,具有已取消节点的线程再也不会阻塞。
* CONDITION: 当前节点在一个条件队列上.它将不会被作为同步队列节点使用直到被转换,
* 在某个时间状态将被设置为0.(这里使用这个值没有结合该field属性其他用法做什么,
* 而是简化了机制。)
* PROPAGATE: 当释放一个Shared节点(例如:读锁)时应当继续被传播信号给其他节点。
* 这个设置(仅为head头节点)在doReleaseShared方法中来确认继续传播信号,
* 即使其他操作已经介入.
* 0: None of the above
*
* 数值的排列是为了简化使用。
* 非负数意味着一个节点不需要信号。那么代码不需要去检查特定的值,只需要判断符号。
*
* 普通的同步节点这个属性被初始化为0,CONDITION状态为条件节点。它的更新使用CAS操作
* (或者在可能的情况下,无条件的volatile写)
*/
volatile int waitStatus;
/**
* 前置节点
*/
volatile Node prev;
/**
* 后置节点
*/
volatile Node next;
/**
* 节点上排队的线程. 在构造方法上初始化并在使用后置空
*/
volatile Thread thread;
/**
* 指向下一个等待条件触发的节点,或者特殊的共享节点。因为条件队列
* 仅当处于独占模式时才能被访问,我们仅需要一个简单的链式队列,当他们正
* 在等待条件时保存节点。然后他们被转移到等待队列中重新请求获取锁。
* 并且因为条件可能仅是独占的,我们使用一个特殊的值保存至一个
* 属性来表示共享模式。
* 该属性维护一个等待队列,等待节点的该指针指向ConditionObject的实例是
* Condition的子类,支持所有Condition条件的用法。
* 仅当锁持有者即处于独占模式时可以访问等待队列
*/
Node nextWaiter;
/**
* 是否共享锁节点
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前置节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
head头节点
等待队列的头节点并且是锁的持有者,懒惰初始化。除了初始化以外,它仅能由setHead方法更改。注意:如果head存在,它的waitStatus状态被保证不能取消
tail尾巴节点
等待队列的尾巴节点,懒惰初始化。仅能由enq方法添加一个新的等待节点来改变
state同步状态
ReentrantLock公平锁实现
lock锁
// 获取锁
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
// 1. 尝试获取锁
// 2. 尝试获取锁失败,添加排他等待节点,并提交至队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1. tryAcquire尝试获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 1.没有前置节点
// 2.cas获取锁成功,则设置当前线程为排他锁持有者,返回true。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果是当前线程再次申请锁,通过并state递增,ReentrantLock是可重入锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 是否存在前置节点。等同于getFirstQueuedThread() != Thread.currentThread() &&
// * hasQueuedThreads()}
// 注意:因为中断或超时取消可能随时发生,返回true不能保证其他线程将获取锁在当前线程之前(因为前置节点可能随时会取消)。
// 同样地,其他线程可能竞争获胜enqueue进入等待队列在这个方法已经返回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());
}
2. addWaiter添加等待节点
创建节点并添加至等待队列(虚拟队列,实际是一个双向链式结构)
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) {
// 1. 已经存在等待节点
// 2. 将当前尾巴节点设置为新节点的前置节点
node.prev = pred;
// 3. 将新节点设置为tail尾巴节点
if (compareAndSetTail(pred, node)) {
// 4. 将上一个尾巴节点(即新节点的前置节点)的next指针指向新节点
pred.next = node;
return node;
}
// 5. 更新tail尾巴节点失败
}
// 6. 可能tail更新失败,也可能tail尾巴节点为空
// 7. 向队列添加新等待节点
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 1. 如果尾巴节点为空
if (t == null) { // Must initialize
// 2. 如果head也为空则创建一个内建节点作为初始的head节点
if (compareAndSetHead(new Node()))
// 3. 尾巴节点指向head节点。继续自旋
tail = head;
} else {
// 4. 如果尾巴节点不为空,更新尾巴节点为新节点,并返回上一次的tail尾巴节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
3. acquireQueued获取队列
获取队列中已存在线程的独占不可中断模式。用于条件等待方法以及acquire获取。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 1. 获取前置节点
final Node p = node.predecessor();
// 2. 如果前置节点是head节点
// 3. 尝试获取锁,如果成功则将当前节点设置为head节点,前置节点的next指针置空(帮助GC)
// 4. 返回中断标志为false
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 5. 如果前置节点不是head节点,或者申请锁失败
// 6. 在获取锁失败后是否应该park阻塞
// 7. 如果应该park阻塞,则park阻塞挂起线程并检查线程是否已中断
if (shouldParkAfterFailedAcquire(p, node) &&
// park阻塞挂起线程并检查线程是否已可中断
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
4. 获取锁失败后是否应该挂起
// 在获取锁失败后是否应该park挂起阻塞线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 1. 前置节点状态为SIGNAL,则park阻塞当前节点线程
if (ws == Node.SIGNAL)
/*
* 这个节点已经设置了状态要求释放时发信号给当前节点,所以可以安全的park阻塞
*/
return true;
if (ws > 0) {
/*
* 前置节点已取消,当前节点的前置节点的指针向前一步移动直至不为取消的状态
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus 必须为0或者传播状态。表示我们需要一个信号,但是不要park阻塞。
* 调用者将需要重试来确定他在parking阻塞之前不能获取锁
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
5.阻塞线程并返回当前线程是否已可中断
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
// 测试当前线程是否可中断
return Thread.interrupted();
}
unlock释放锁
public void unlock() {
// 释放锁
sync.release(1);
}
public final boolean release(int arg) {
// 1. 尝试释放锁,state-arg之后为0,设置锁持有者线程为null,设置state状态为0
if (tryRelease(arg)) {
// 2. 释放锁成功
Node h = head;
// 3. 如果存在等待节点,并且节点状态不为初始化状态0(内嵌节点是0状态)
if (h != null && h.waitStatus != 0)
// 4. unpark发信号唤醒子节点park阻塞的线程
unparkSuccessor(h);
// 4. 返回释放成功
return true;
}
// 释放锁失败
return false;
}
1. unparkSuccessor发信号唤醒子节点挂起的线程
private void unparkSuccessor(Node node) {
/*
* 如果节点(head节点)状态是负数(也就是, 可能需要信号)尝试清除预期的状态(即尝试cas更新为0)
* 如果cas失败或者如果状态被等待的线程改变也没什么问题
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 线程去unpark唤醒被阻塞的子节点, 通常是下一个节点。但是如果取消或者出现了null,
* 从尾巴节点向后遍历,直到找到一个实际没有取消的子节点
* (此处的子节点就是最接近head并且没有取消的节点)。
*/
Node s = node.next;
// 1. 如果下一个节点为null 或者 已经取消
if (s == null || s.waitStatus > 0) {
// 2. 置空next的node节点
s = null;
// 从尾巴节点向后遍历,直到当前节点
for (Node t = tail; t != null && t != node; t = t.prev)
// 如果状态<=0说明节点是非取消状态的节点,记录等待unpark
if (t.waitStatus <= 0)
s = t;
}
// 如果存在非取消状态的子节点,unpark尝试唤醒子节点的线程
if (s != null)
LockSupport.unpark(s.thread);
}
ReentrantReadWriteLock公平锁实现
WriteLock写锁
操作与ReentrantLock相同,添加都节点都是排他节点
ReadLock读锁
lock锁
public void lock() {
// 获取共享节点
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
// 1. 尝试获取读取锁,如果失败,添加节点后等待
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// 返回1代表获取读锁成功,-1代表尝试获取读锁失败,需要添加节点后等待
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. 如果写锁被另外一个线程获取到,返回失败.
* 2. 否则, 这个线程是符合锁的相关状态,那么因为队列策略询问它是否应该阻塞。如果不,
* 尝试CAS操作状态并且更新count值。
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. 如果步骤2失败,要么因为线程不符合条件,要么cas操作失败,或者count值满了,
* chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
// 1. 存在排他节点(即获取读锁之前已经存在排他节点),并且持有者不是当前线程,返回失败-1
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
// 2. 如果读不需要阻塞
// a. 当前head等于tail即没有等待节点,即当前存在节点但不一定是当前节点正在添加(正在初始化等待队列期间),
// 但是最后只能有一个节点cas操作state状态成功即获取锁成功
// b. 或者head节点的next指针不为空,并且是当前线程(即当前线程是锁持有者)
// 相反则需要等待
if (!readerShouldBlock() &&
// 2.1 读锁小于最大值65535
r < MAX_COUNT &&
// 2.2 cas更新state状态值成功(每次增加65536),即获取读锁成功。
// 更新成功后,后来的写锁是无法获取锁的,因为state状态不为0,只能添加等待节点等待读锁或写锁释放后才能获取
compareAndSetState(c, c + SHARED_UNIT)) {
// 3. 第一个读锁获取者更新firstReader属性,并初始化count值
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 4. 如果是第一个读锁再次进入,递增count值即可
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 5. 如果不是第一个读锁的线程申请读锁,则通过ThreadLocal记录上次成功获取读锁的线程count值
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
// 上次线程成功获取读锁的数量readHolds初始值为0
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
// 返回1代表获取读锁成功
return 1;
}
// 6. 如果需要阻塞
// a. 等待队列的head不等于tail(即已经存在head锁持有者,且可能存在等待节点)
// b. 并且(head节点的next指针为空,即当前节点与锁持有者head节点不可能是同一个线程
// a. 或者持有head节点的next节点的线程不是当前线程)
// 6.1 或者读锁超过最大值限制,
// 6.2 或者cas更新state状态值失败,即获取读锁失败。
return fullTryAcquireShared(current);
}
Sync() {
// 初始化计数属性readHolds
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
// 存在排他节点(即获取读锁之前已经存在排他节点),并且持有者不是当前线程,返回失败-1
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) { // 含义同hasQueuedPredecessors方法,见下面该方法解释
// 确保我们没有获取可重入的读锁,可重入的读锁不计入读锁的count中
// 如果当前申请读锁的线程与第一个读锁(锁持有者或者与写锁是同一线程)是同一线程,
// 则不需要阻塞继续执行
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 如果应该阻塞并且与第一个读锁不是同一线程则返回-1即获取锁失败添加等待节点阻塞等待唤醒
if (rh == null) {
// 上次成功获取读锁的线程数量
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
// 如果上次线程成功获取读锁的数量等于0则需要阻塞,即添加共享类型的等待节点
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
// 超出最大值65535抛出异常
throw new Error("Maximum lock count exceeded");
// 走到此处的可能条件:
// 1. 排他锁数量为零,全都是读锁
// 2. 排它锁不为零并且当前读锁获取者是排它锁的持有者(同一个线程不需要线程同步)
// 3. 当前线程与第一个读锁持有者是同一个线程
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
// 递增一个线程获取的读锁数量
rh.count++;
cachedHoldCounter = rh; // cache for release
}
// 递增数量如果需要,返回获取读取锁成功
return 1;
}
}
}
// 读写锁判断是否需要阻塞等待
// 1. 如果head不等于tail节点(即已经存在head锁持有者,且可能存在等待节点)
// 并且(head节点的next指针为空,即当前节点与锁持有者head节点不可能是同一个线程
// 或者持有head头节点的next节点的线程不是当前线程)
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());
}
添加共享锁节点等待doAcquireShared
private void doAcquireShared(int arg) {
// 1. 添加共享锁节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
// 2. 自旋
for (;;) {
final Node p = node.predecessor();
// 3. 如果锁持有者是当前节点的前置节点,则尝试获取读锁
if (p == head) {
int r = tryAcquireShared(arg);
// 4. 获取读锁成功(写锁释放,或者读锁与写锁是同一线程)
if (r >= 0) {
// 5. 读锁获取成功,更新head头节点为当前读锁节点并传播信号。
// 如果信号为1则说明读锁获取成功,当前读锁节点的next指针即下一个节点如果依然是
// 共享节点,则释放共享锁,如果head节点的等待状态为SIGNAL
// 则unparkSuccessor发信号唤醒子节点挂起的线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 已可中断,中断当前线程不再自旋
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 获取读锁失败后挂起线程等待唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
// 阻塞线程并返回当前线程是否已可中断
parkAndCheckInterrupt())
// 是则标识当前线程已可中断
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
unlock释放锁
调用sync.releaseShared释放共享锁
- 尝试释放锁tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 1. 如果是第一个读锁持有者则递减数量
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
// 2. 不是第一个读锁持有者则递减对应的缓存count值
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
// 3. 递减共享锁的单元格,递减后如果是0则说明当前可以进入无锁状态,即尝试释放锁成功
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
doReleaseShared执行共享锁释放
private void doReleaseShared() {
/*
* 确保传播释放信号,tryReleaseShared成功说明当前state==0代表无锁状态,
* 可以传递给下一个等待节点信号,唤醒下一个等待节点的线程,如果waitStatus是默认值0,
* 则先更新为PROPAGATE,然后再进行传播信号
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
ReentrantReadWriteLock非公平锁实现
主要区别在于sync实现的方式改为了NonFairSync类型。可以看到里面最大的区别在于readerShouldBlock代码的重写,我们看下读锁可以不阻塞的条件
- head为null。即没有锁
- head不为null,但是head的next为null。如果此时有多个线程同事竞争不一定谁能拿到锁
- head不为null,head的next不为null,但是next节点是共享节点(即读锁)。此时的读就允许插队获得锁
- head不为null,head的next不为null,next节点不是共享节点,但是next节点取消了锁的获取。此时也允许插队
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
Q&A
为什么需要一个内嵌的节点作为初始的head、tail
根据CLH锁的定义需要记录一个pre指针来遍历前置节点的状态,第一个节点需要遍历的前置节点状态是一个虚拟不存在的,不然就进入死循环,永远没有第一个节点,总要为第一个节点找到前置节点,所以需要使用一个内嵌节点作为第一个头节点(头节点释放锁之后会被更新)的前置节点终止死循环
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;
}
}
}
}
为什么需要记录head节点
- 如果不记录head节点,实现FIFO的公平性就每次只能从尾巴处遍历至最前端获取最先进入的等待者进行唤醒,很明显是不合理的。head指向当前锁的持有者节点,在head节点释放锁成功时可以结合next指针直接定位锁的第一优先获取的等待节点进行锁资源交接并将其更新为head节点
为什么需要记录tail节点
如果不记录tail节点,每次新线程获取锁资源时发现需要等待时,每次都要从head节点一直遍历至尾巴节点进行追加(必须保证公平性),显然性能很差
为什么既有pre指针又有next指针
- 使用双向链表的好处就是等待节点可以不断的向尾巴节点追加,头部节点锁释放后可以快速定位下一个等待节点,并将其设置成为新的头节点即锁持有者
- pre指针是CLH锁的必须属性,因为要轮询前置节点的状态判断是否终止自旋即获取锁成功,如果不使用pre指针也可以实现,那就是使用ThreadLocal将每一个前置节点保存在缓存中
- next指针的一种使用场景:在实现读写锁时,如果写锁唤醒的下一个等待节点是读锁,读锁可能是并发连续的多个读锁,读锁的子节点如果也是读锁,是不需要阻塞的,可以立即被唤醒传递下去,直到下一个节点是写锁时终止。也就是并发读,排他写。
- next指针的另一个用处就是在释放锁时,需要将head节点切换为下一个等待节点(公平性),如果不保留next指针,那么head的切换需要从tail中向前一直遍历至最前方等待的节点,并且不是当前head节点的节点处,将其替换为head