- Hello,大家好,我是Lorin 洛林,前一期我们聊了基于乐观锁实现的一种高效、线程安全的原子操作 CAS ,但简单的 CAS 也有一些不足,比如无法实现公平锁,依赖一个互斥标记,当线程竞争比较大时会导致大量的 CPU 高速缓存同步,这一期我们来聊聊 CAS 的升级版:CLH、MCS 如何解决这些问题。
导读
- 普通自旋锁可能存在的一些问题:饥饿、如何实现公平、CPU 高速缓存频繁同步
- CLH 锁 和 MCS 锁是什么?以及使用场景
环境及版本
- 运行版本:JDK8
普通自旋锁存在的问题
-
自旋锁是 Java 并发编程中的常见解决方案,当互斥资源被其它线程占用时,通过自旋的方式尝试获取锁,避免阻塞和唤醒线程带来的上下文切换开销,但普通的自旋锁存在以下几方面问题:
1、非公平锁,可能导致饥饿
2、依赖一个互斥标记,线程较多时竞技激烈,且多个CPU高速缓存同步频繁
3、实现非公平锁需要额外的字段
CLH 锁 和 MCS 锁
- 解决上述问题,我们可以用 CLH 锁 MCS 锁通过队列实现。
CLH 锁
- CLH 是一种逻辑队列自旋锁,由 Craig、Landin 和 Hagersten 三位作者提出,具体内容在 《Building FIFO and Priority-Queuing Spin Locks from Atomic Swap》 论文中有详细介绍。
- Java 中 AQS 就是基于变种 CLH 实现。
流程
- 下面是 CLH 锁 加锁和解锁的大致流程:
加锁
- 维护队列的尾节点,通过 CAS 操作将线程入队,并将前置节点置为上一个尾节点(逻辑连接),lock 状态置为 true (lock 状态为 true 表示正在获取锁或已经成功获取锁,需要结合前置节点 lock 状态判断)。
- 入队后的节点,自旋轮询前一个尾节点(即当前节点的前置节点)lock 状态,当前置节点为空或 lock 为 false 时,当前节点成功获取锁。
解锁
- 解锁时将当前节点的 lock 状态置为 false。
示例代码
Java
interface Lock {
void lock();
void unlock() throws Exception;
}
class CLHLock implements Lock {
/**
* tailNode 尾节点原子操作保证线程安全
*/
private final AtomicReference<Node> tailNode = new AtomicReference<>();
private final ThreadLocal<Node> currentNodeLocal = new ThreadLocal<>();
private static class Node {
/**
* 前驱节点
*/
private Node preNode;
/**
* 当前节点状态
* volatile 保证对后置线程的可见性
*/
private volatile Boolean lockState;
public Node(Boolean lockState) {
this.lockState = lockState;
}
}
@Override
public void lock() {
Node currentNode = currentNodeLocal.get();
if (currentNode == null) {
currentNodeLocal.set(new Node(true));
currentNode = currentNodeLocal.get();
}
// 拿到当前节点的前置节点 形成逻辑连接 无实际连接
Node preNode = tailNode.getAndSet(currentNode);
// 检查前置节点 lock state
while (currentNode.preNode != null && preNode.lockState) {
System.out.println(Thread.currentThread().getName() + " 自旋等待获取锁");
}
System.out.println(Thread.currentThread().getName() + " 获取锁成功");
}
@Override
public void unlock() throws Exception {
Node currentNode = currentNodeLocal.get();
if (!currentNode.lockState || (currentNode.preNode != null && currentNode.preNode.lockState)) {
throw new Exception("current thread is not locked");
}
currentNode.lockState = false;
// 清除线程 ThreadLocal 本次锁信息 避免拿到已经释放的锁信息
currentNodeLocal.remove();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
CLH 的优点
- 性能优异,获取和释放锁开销小。CLH 的锁状态不再是单一的原子变量,而是分散在每个节点的状态中,降低了自旋锁在竞争激烈时频繁同步的开销。释放锁的开销也因为不需要使用 CAS 指令而降低。
- 公平锁。
- 实现简单,可用于拓展,如 AQS 就是基于变种 CLH实现。
CLH 的不足
- 对于锁长时间持有的场景会造成 CPU 自旋损耗。
- 过于简单,实现复杂功能需要进行拓展。
MCS 锁
- MCS 由 John M. Mellor-Crummey 和 Michael L. Scott 提出,具体内容可以在 《Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors》 论文中查看。
- MCS 锁和 CLH 锁十分相似,都是逻辑队列自旋锁,但 CLH 锁轮询的是前置节点的 lock 域,而 MCS 锁轮询的自己当前节点的 lock 域,前置节点释放锁时会更新队列后置节点 lock 状态,即可以根据当前节点的 lock 状态来判断是否可以获取锁,主要是为了解决 NUMA(Non-Uniform Memory Access) 架构下读取远端内存速度较慢的问题。
和 CLH 锁区别
- CLH 锁逻辑队列之间连接无物理连接,MCS 锁存在物理连接。
- 核心:CLH 锁通过自旋轮询前置节点 lcok 域状态判断是否获取锁,MCS 锁判断当前节点 lock 状态。
流程
- 下面是 MCS 锁加锁、解锁大致流程:
加锁
- 维护队列的尾节点,通过 CAS 操作将线程入队,若前置节点为空,直接获取锁,若前置节点不为空,将前置节点的 next 节点指向当前节点,轮询当前节点 lock 状态。(初始值为 false 未获取锁,true 为获取到锁)
解锁
- 当前节点释放锁,唤醒队列中的后置节点,即将后置节点的 lock 置为 true。
示例代码
Java
interface Lock {
void lock();
void unlock() throws Exception;
}
class MSCLock implements Lock {
/**
* tailNode 尾节点原子操作保证线程安全
*/
final AtomicReference<Node> tailNode = new AtomicReference<>();
private final ThreadLocal<Node> currentNodeLocal = new ThreadLocal<>();
private static class Node {
/**
* 后驱节点
* volatile 保证 nextNode 引用的可见性
*/
private volatile Node nextNode;
/**
* 当前节点状态
* volatile 保证对后置线程的可见性
*/
private volatile Boolean lockState;
public Node(Boolean lockState) {
this.lockState = lockState;
}
}
@Override
public void lock() {
Node currentNode = new Node(true);
currentNodeLocal.set(currentNode);
Node preNode = tailNode.getAndSet(currentNode);
// 首节点直接获取锁
if (preNode == null) {
currentNode.lockState = true;
} else {
preNode.nextNode = currentNode;
// 自旋检测当前节点状态
while (!currentNode.lockState) {
System.out.println(Thread.currentThread().getName() + " 自旋等待获取锁");
}
}
System.out.println(Thread.currentThread().getName() + " 获取锁成功");
}
@Override
public void unlock() {
Node currentNode = currentNodeLocal.get();
Node nextNode = currentNode.nextNode;
// 若无等待线程 尝试将tailNode置为 null
if (nextNode == null) {
if (tailNode.compareAndSet(currentNode, null)) {
System.out.println(Thread.currentThread().getName() + " 锁释放成功");
return;
} else {
nextNode = currentNode.nextNode;
}
}
// 清除线程 ThreadLocal 本次锁信息 避免拿到已经释放的锁信息
currentNodeLocal.remove();
// 唤醒下一个等待线程
nextNode.lockState = true;
}
}
下期预告
- 下一期我们将聊聊 Java 多线程编程中最重要的 抽象队列同步器 AQS ,这是 JUC 中除了 CAS 外另一个 底层工具,我们使用的大多数锁都是基于这两个底层工具实现。