在AQS源码的注释中提到AQS是CLH锁的一个变种,所以了解一下CLH锁有助于我们学习AQS。CLH锁是基于链表的、公平的的自旋锁。
这里直接贴代码(代码来自《多处理器编程的艺术》机械工业出版社)
// 这里只实现了Lock接口的部分方法
public class CLHLock implements Lock {
AtomicReference tail = new AtomicReference(new QNode());
ThreadLocal myPred;
ThreadLocal myNode;
public CLHLock() {
// 队列有一个默认的头节点
tail = new AtomicReference(new QNode());
myNode = new ThreadLocal() {
@Override
protected QNode initialValue() {
return new QNode();
}
};
myPred = new ThreadLocal() {
@Override
protected QNode initialValue() {
return null;
}
};
}
@Override
public void Lock() {
QNode qnode = myNode.get();
qnode.locked = true;
// 原子的进入队列
QNode pred = tail.getAndSet(qnode);
myPred.set(pred);
// 在前驱节点的locked字段上自旋,第一个线程则会检测默认的头节点的locked的值
while (pred.locked) {
}
}
@Override
public void Unlock() {
QNode qnode = myNode.get();
// 放弃锁,后驱节点可以继续
qnode.locked = false;
// 这个有点绕,把当前线程的myNode引用设置成队列的头节点,前线程的myNode的原引用则变成队列的头结点。前线程的myNode则会从队列中脱离。
myNode.set(myPred.get());
}
// 在书中没有给出QNode的定义
private class QNode {
// 不知道作者的原意是在这里使用volatile来实现锁的内存语义还是通过其他的方式。
public volatile boolean locked = false;
}
}
CLH锁的核心在于线程在他对应节点的前驱节点上自旋,等待前驱节点放弃锁的通知,CLH锁在NUMA系统结构下表现差也是因为此,AQS的实现与CLH锁相似的地方也在于此。在AQS中线程获取失败会进入等待队列,阻塞并等待前驱节点对应线程的唤醒。