CLH锁是对自旋锁的一种改进。先看看什么是自旋锁,自旋锁是互斥锁的一种体现,Java实现如下:
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread currentThread = Thread.currentThread();
// 如果锁未被占用,则设置当前线程为锁的拥有者
while (!owner.compareAndSet(null, currentThread)) {
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
// 只有锁的拥有者才能释放锁
owner.compareAndSet(currentThread, null);
}
}
自旋锁在获取锁时,线程会对一个原子变量循环执行compareAndSet
方法,直到该方法返回成功即成功获取锁。compareAndSet
操作是通过CAS实现的,因此该操作是原子操作。原子性保证了根据最新消息计算出新值,如果与此同时值已由另一个线程更新,则写入失败。因此,这段代码可以实现互斥锁的功能。
优点:
缺点:
自旋锁适用于锁竞争不激烈、锁持有时间短的场景。
CLH锁是对自旋锁的一种改进,由Craig、Landin和Hagersten(CLH)发明,有效的解决了以上的两个缺点:
CLH锁的数据结构类似一个链表结构(实现时并不是真实的链表,而是一种隐式的链表队列),所有请求获取锁的线程会排列在链表队列中,每个节点会自旋访问其前一个节点的状态。当前一个节点释放锁时,只有它的后一个节点才可以得到锁。
详细来说:CLH锁本身有一个对位指针Tail,其是一个原子变量,指向队列最末端的CLH节点。每一个CLH节点有两个属性:所代表的线程和标识是否持有锁的状态变量。当一个线程(记为Th1)要获取锁时,它会对Tail进行一个getAndSet
的原子操作,该操作会返回Tail指向的节点(也就是队尾节点,记为C0),并将其作为Th1所对应的CLH节点(记为C1)的前驱节点,最后将Tail指向C1,使其称为新的队尾节点。入队成功后,C1就会自旋访问其上一个节点C0的状态变量,当上一个节点C0释放锁后,C1将得到这个锁。如下图所示:
public class CLHLock {
private final ThreadLocal<Node> node = ThreadLocal.withInitial(Node::new);
private final AtomicReference<Node> tail = new AtomicReference<>(new Node());
private static class Node {
private volatile boolean locked;
}
public void lock(){
Node node = this.node.get();
node.locked = true;
Node pre = this.tail.getAndSet(node);
while (pre.locked);
}
public void unlock(){
final Node node = this.node.get();
node.locked = false;
this.node.set(new Node());
}
}
加锁过程:
Node pre = this.tail.getAndSet(node);
先将尾节点取出来作为当前节点node的前一个节点Pre,然后把当前节点node作为尾节点。getAndSet
是一个原子操作。解锁过程:
疑问:
1、CLH是一个链表队列,为什么Node节点没有指向前驱或后继的指针?
CLH锁是一种隐式的链表队列,没有显式的维护前驱或后继指针。因为每个等待获取锁的线程只需要自旋等待前一个节点的状态就好了,而不需要遍历整个队列。因此只需要使用一个局部变量保存前驱节点,不需要显式维护前驱或后继指针。
2、在解锁过程的第三步中,为什么需要重新向ThreadLocal中重新赋值一个新的CLH节点?
因此需要在ThreadLocal中重新赋值一个新的CLH节点,避免Node节点复用带来的死锁问题。
优点:
缺点:
针对CLH的缺点,AQS对CLH队列锁进行了改造:
AQS中由waitStatus
变量保存节点状态。
volatile int waitStatus;
AQS提供了该状态变量的原子读写操作,AQS中的节点状态有以下五种:
状态名 | 描述 |
---|---|
CANCELLED | 值为1,在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待。 |
SIGNAL | 值为-1,后续节点处于等待状态,当前节点地线程释放了锁(同步状态)或者被取消,会通知后继节点。 |
CONDITION | 值为-2,该节点位于条件队列中,并不在同步队列中。当其他线程调用该condaition的signal方法后,才会加入到同步队列中。 |
PROPAGATE | 值为-3,表示下一次共享式同步状态获取将会无条件地传播下去。 |
INITIAL | 值为0,初始状态。 |
AQS中使用阻塞等待替换了自旋操作,线程会阻塞等待锁的释放,不能主动感知到前驱节点的状态变化。因此AQS中显式维护了前驱节点和后继节点,需要释放锁的节点会显式通知下一个节点解除阻塞。
JVM 的垃圾回收机制使开发者无需手动释放对象。但在 AQS 中需要在释放锁时显式的设置为 null,避免引用的残留,辅助垃圾回收。
参考:
[1]https://zhuanlan.zhihu.com/p/398582011
[2]https://blog.csdn.net/fengyuyeguirenenen/article/details/123856507