CLH锁与MCS锁
Ticket锁(传送门)的缺点在于扩展性,cpu不断增加扩展时,对于共享变量(当前叫号叫到了几号)的访问会成为系统的瓶颈,共享变量递增导致大量的线程本地缓存被置为无效,需要通过内存总线访问主存刷新本地缓存,会造成拥塞。而CLH锁与MCS锁正式为了解决此瓶颈而衍生的算法。下面我来看一下他们的实现以及对比。
CLH锁
定义
CLH锁是一个可扩展,高性能,公平的基于链表的自旋锁,应用线程仅自旋在本地变量,它一直读取前置节点的状态,如果它发现前置节点释放了锁则结束自旋
实现
import java.util.concurrent.atomic.AtomicReference;
import lombok.ToString;
/**
* @author 会灰翔的灰机
* @date 2019/9/21
*/
public class CLHLock implements MyLock {
AtomicReference<CLHNode> tail;
ThreadLocal<CLHNode> myPred;
ThreadLocal<CLHNode> myNode;
@ToString
private static class CLHNode {
private volatile boolean locked;
}
public CLHLock() {
tail = new AtomicReference<>(new CLHNode());
myNode = ThreadLocal.withInitial(CLHNode::new);
myPred = ThreadLocal.withInitial(() -> null);
}
@Override
public void lock() {
CLHNode clhNode = myNode.get();
clhNode.locked = true;
CLHNode pred = tail.getAndSet(clhNode);
myPred.set(pred);
while (pred.locked) {
System.out.println("spin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void unlock() {
CLHNode clhNode = myNode.get();
clhNode.locked = false;
myNode.set(myPred.get());
}
public static void main(String[] args) {
CLHLock clhLock = new CLHLock();
MyTask myTask = new MyTask(3000, clhLock);
myTask.start();
MyTask myTask2 = new MyTask(1000, clhLock);
myTask2.start();
MyTask myTask3 = new MyTask(1000, clhLock);
myTask3.start();
}
}
CLH锁优化版,参考AQS的实现,上一个版本对于每个线程都缓存一个当前节点与前置节点,一把锁的空间复杂度=2并发线程数的threadlocal引用+并发线程数1locked标识+1tail引用,下面这个版本一把锁的空间复杂度=并发线程数的next引用+并发线程数*1locked标识+1tail指针+1head指针。显然优化后的空间复杂度更低
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import lombok.ToString;
/**
* @author 会灰翔的灰机
* @date 2019/9/21
*/
public class CLHLock implements MyLock {
@ToString
private static class CLHNode {
/**
* 当前节点是否已释放,释放说明下一个节点可以获取锁,否则则进入自旋
*/
private volatile boolean released;
private volatile CLHNode next;
boolean isReleased() {
return released;
}
void release(){
released = true;
}
}
private volatile CLHNode tail;
private volatile CLHNode head;
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> HEAD_UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "head");
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> TAIL_UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail");
/**
* 记录锁持有者,只有持有者才能解锁
*/
private ThreadLocal<Thread> threadLocal = new ThreadLocal<>();
@Override
public void lock() {
CLHNode pre;
for(;;) {
CLHNode clhNode = new CLHNode();
// 1. 第一个节点初始化
if (tail == null) {
// 第一个节点不需要等待直接获取锁资源
clhNode.release();
if (HEAD_UPDATER.compareAndSet(this, null, clhNode)) {
tail = head;
}
} else {
// 2. 非第一个节点,需要等待锁持有者释放资源,添加等待节点
CLHNode lastTail = tail;
// 追加等待节点
if (TAIL_UPDATER.compareAndSet(this, tail, clhNode)) {
// 更新next指针,锁获取成功时,需要使用next指针将head节点更新为下一个等待节点
lastTail.next = clhNode;
// 前置节点缓存并遍历其状态判断是否可以终止自旋执行代码
pre = lastTail;
break;
}
}
}
// 3. 自旋在本地变量pre上,遍历前置节点的状态,判断是否锁资源已经释放,是则终止自旋执行代码
while(!pre.isReleased()) {
System.out.println("spin..." + pre);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 4. 自旋结束获取锁成功,head节点状态已经是释放状态,将head更新为下一个等待节点
while (!HEAD_UPDATER.compareAndSet(this, head, head.next)) {
System.out.println("head节点更新失败,稍候重试");
}
threadLocal.set(Thread.currentThread());
}
@Override
public void unlock(){
// 5. 更新头节点状态为释放
if (head != null && Thread.currentThread() == threadLocal.get()) {
head.release();
}
threadLocal.remove();
System.out.println("head : " + head);
}
public static void main(String[] args) {
CLHLock clhLock = new CLHLock();
MyTask myTask = new MyTask(3000, clhLock);
myTask.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
clhLock.unlock();
MyTask myTask2 = new MyTask(1000, clhLock);
myTask2.start();
MyTask myTask3 = new MyTask(1000, clhLock);
myTask3.start();
}
}
MCS锁
定义
与CLH锁类型,锁的实现结合了队列或链表。区别在于CLH自旋在前置节点的状态。MCS锁自旋在当前节点的状态。
实现
package com.gallant.dispatch.juc;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import lombok.ToString;
/**
* @author 会灰翔的灰机
* @date 2019/9/21
*/
public class MCSLock implements MyLock {
@ToString
private static class MyNode {
/**
* 当前节点是否已释放,释放说明下一个节点可以获取锁,否则则进入自旋
*/
private volatile boolean released;
private volatile MyNode next;
boolean isReleased() {
return released;
}
void release() {
released = true;
}
}
private volatile MyNode tail;
private static final AtomicReferenceFieldUpdater<MCSLock, MyNode> TAIL_UPDATER = AtomicReferenceFieldUpdater
.newUpdater(MCSLock.class, MyNode.class, "tail");
private ThreadLocal<Thread> owner = new ThreadLocal<>();
private ThreadLocal<MyNode> currentNodeCache = new ThreadLocal<>();
@Override
public void lock() {
MyNode currentNode;
for(;;) {
currentNode = new MyNode();
MyNode lastTail = tail;
if (TAIL_UPDATER.compareAndSet(this, tail, currentNode)) {
if (lastTail == null) {
currentNode.release();
} else {
lastTail.next = currentNode;
}
currentNodeCache.set(currentNode);
break;
}
}
while (!currentNode.isReleased()) {
System.out.println("mcslock spin..." + currentNode);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
owner.set(Thread.currentThread());
}
@Override
public void unlock() {
MyNode currentNode = currentNodeCache.get();
if (currentNode != null && currentNode.next != null && Thread.currentThread() == owner
.get()) {
currentNode.next.release();
currentNodeCache.remove();
owner.remove();
}
System.out.println("mcslock head:" + currentNodeCache);
}
public static void main(String[] args) {
MCSLock mcsLock = new MCSLock();
MyTask myTask = new MyTask(3000, mcsLock);
myTask.start();
MyTask myTask2 = new MyTask(1000, mcsLock);
myTask2.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
mcsLock.unlock();
MyTask myTask3 = new MyTask(1000, mcsLock);
myTask3.start();
}
}
对比
区别上面已经说过了,此处不再赘述。直接聊聊他们的优缺点与使用场景
CLHLock、MCSLock
共同的优点:无饥饿、公平性、可扩展性高、高性能(仅需要一次CAS操作,setHead操作由于是在锁内所以可以删除上文中多出来的一次CAS)
CLHLock缺点:缺点是在NUMA(cpu对称架构)系统结构下性能非常差。在这样的系统结构下,对于每个cpu其他资源(内存、io)都是共享的,每一个线程有自己的内存,假设前趋结点的内存位置比较远。自旋访问前趋结点的locked域(内存地址),性能将大打折扣,可是在SMP系统结构下该法还是非常有效的。MCSLock自旋在本地节点,每次访问的都是本地内存,所以不存在CLHLock的内存位置比较远的问题。
当锁节点状态发生变化时,CLHLock只需要更新本地状态,子节点需要同步刷新缓存,一次同步动作可能会存在子节点内存比较远的问题。MCSLock则需要更新子节点的状态,一次写动作可能会存在子节点内存比较远的问题。但是CLHLock每次自旋因为是轮询的前置节点状态会存在内存比较远的问题。MCSLock自旋在自身节点状态,自旋的场景不存在内存比较远的问题。所以CPU的架构在NUMA系统结构下性能优于CLHLock
CPU SMP、NUMA架构
SMP(Symmetric Multi-Processor)
对称多处理器架构,是一种cpu架构设计模式,服务器中多个CPU对称工作。SMP服务器的主要特征是共享,系统资源(CPU 、内存、 I/O 等 )都是共享的。由于这种特征,导致SMP服务器的扩展能力受到约束。每一个共享的环节都可能成为SMP服务器的扩展瓶颈,而最受限制的是内存。每个CPU必须通过内存总线访问相同的内存资源,随着CPU数量增加,内存访问冲突将随着增加,最终导致CPU性能大打折扣
NUMA(Non-Uniform Memory Access)
NUMA服务器的基本特征是具有多个CPU模块,每个CPU模块由多个CPU组成,具有独立的本地内存、I/O槽口等。其节点之间可以通过互联模块进行连接和信息交互,因此每个CPU可以访问整个系统的内存( NUMA系统与MPP系统的重要差别)。显然,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它模块的内存 )