1.自旋锁-MCS/CLH队列
- 之前有写过一篇关于锁的笔记:【锁】公平锁/非公平锁/可重入锁/递归锁/自旋锁/独占锁/共享锁/读写锁
里面关于自旋锁,特别CLH队列并没有提到,故此有了这篇笔记。
2.自旋锁
- 指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上线文切换的消耗,缺点是循环会消耗CPU
2.1 CAS
JUC包里的AtomicInteger类底层用CAS机制的实现就是一种自旋锁,以下是其底层核心类UnSafe的一段源码:
具体可查看:【JUC】 Java中的CAS
2.2 我们来自己实现一个简单的自旋锁,Demo:
/**
* 手写一个自旋锁:循环比较知道成功为止
*
* @author wangjie
* @version V1.0
* @date 2019/12/18
*/
public class SpinLockDemo {
/**
* 原子引用线程
*/
AtomicReference<Thread> atomicReference = new AtomicReference();
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t come in");
while (!atomicReference.compareAndSet(null,thread)){
}
}
public void myUnLock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t in myUnLock()");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock();
},"AA").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
spinLockDemo.myLock();
spinLockDemo.myUnLock();
},"BB").start();
}
}
- 运行结果:
3.MCS 自旋锁
- MCS含义:
发明人的名字:John Mellor-Crummey和Michael Scot
- 介绍:
MCS 队列基于链表实现,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。
- Demo:
/**
* MCS自旋锁
*
* @author wangjie
* @version V1.0
* @date 2019/12/20
*/
public class MCSLock {
/**
* 链表节点
*/
public static class MCSNode {
volatile MCSNode next;
volatile boolean isWaiting = true; // 默认是在等待锁
MCSNode(){
}
}
/**
* 指向最后一个申请锁的MCSNode
*/
volatile MCSNode queue;
/**
* 一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。(注意这个字段不能是private的)
*
* 通过调用AtomicReferenceFieldUpdater的静态方法newUpdater就能创建它的实例,该方法要接收三个参数:
* 包含该字段的对象的类
* 将被更新的对象的类
* 将被更新的字段的名称
*/
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
.newUpdater(MCSLock.class, MCSNode.class, "queue");
public void lock(MCSNode currentThread) {
System.out.println(Thread.currentThread().getName()+"\t come in");
/**
* 原子更新该更新器管理的指定对象的字段的值为newValue,返回旧值
*/
MCSNode predecessor = UPDATER.getAndSet(this, currentThread);
//前个节点不为null
if (predecessor != null) {
//设置前一个节点指向当前节点
predecessor.next = currentThread;
while (currentThread.isWaiting) {
//自旋自己的isWaiting
System.out.println(Thread.currentThread().getName()+"\t Waiting");
}
} else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁
currentThread.isWaiting = false;
System.out.println(Thread.currentThread().getName()+"\t get lock");
}
}
public void unlock(MCSNode currentThread) {
System.out.println(Thread.currentThread().getName()+"\t in UnLock()");
if (currentThread.isWaiting) {
return;
}
// 检查是否有人排在自己后面
if (currentThread.next == null) {
if (UPDATER.compareAndSet(this, currentThread, null)) {
// compareAndSet返回true表示确实没有人排在自己后面
return;
} else {
// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
while (currentThread.next == null) {
}
}
}
currentThread.next.isWaiting = false;
// 辅助GC
currentThread.next = null;
}
public static void main(String[] args) {
MCSLock mCSLock = new MCSLock();
new Thread(()->{
MCSNode node = new MCSNode();
mCSLock.lock(node);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
mCSLock.unlock(node);
},"AA").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
MCSNode node = new MCSNode();
mCSLock.lock(node);
mCSLock.unlock(node);
},"BB").start();
}
}
- 运行结果:
4.CLH 自旋锁
- CLH含义:
与他们的发明人的名字相关:Craig,Landin and Hagersten
- 介绍:
CLH 锁与 MCS 锁的原理大致相同,都是各个线程轮询各自关注的变量,来避免多个线程对同一个变量的轮询,从而从 CPU 缓存一致性的角度上减少了系统的消耗。不同的是,MCS 轮询的是当前队列节点的变量,而 CLH 轮询的是当前节点的前驱节点的变量,来判断前一个线程是否释放了锁。
- Demo:
/**
* CLH队列自旋锁
*
* @author wangjie
* @version V1.0
* @date 2019/12/20
*/
public class CLHLock {
public static class CLHNode {
private volatile boolean isWaiting = true; // 默认是在等待锁
}
/**
* 尾节点
*/
private volatile CLHNode tail ;
/**
* 一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。(注意这个字段不能是private的)
*
* 通过调用AtomicReferenceFieldUpdater的静态方法newUpdater就能创建它的实例,该方法要接收三个参数:
* 包含该字段的对象的类
* 将被更新的对象的类
* 将被更新的字段的名称
*/
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
. newUpdater(CLHLock.class, CLHNode .class , "tail" );
public void lock(CLHNode currentThread) {
CLHNode preNode = UPDATER.getAndSet( this, currentThread);
//已有线程占用了锁,进入自旋
if(preNode != null) {
while(preNode.isWaiting ) {
System.out.println(Thread.currentThread().getName()+"\t Waiting");
}
}
}
public void unlock(CLHNode currentThread) {
// 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
if (!UPDATER .compareAndSet(this, currentThread, null)) {
// 还有后续线程
// 改变状态,让后续线程结束自旋
System.out.println(Thread.currentThread().getName()+"\t unlock");
currentThread.isWaiting = false ;
}
}
public static void main(String[] args) {
CLHLock cLHLock = new CLHLock();
new Thread(()->{
CLHNode node = new CLHNode();
cLHLock.lock(node);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
cLHLock.unlock(node);
},"AA").start();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
CLHNode node = new CLHNode();
cLHLock.lock(node);
cLHLock.unlock(node);
},"BB").start();
}
}
- 运行结果:
总结
1,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋。
2,CLH的队列是隐式的,通过轮询关注上一个节点的某个变量,隐式地形成了链式的关系,但CLHNode并不实际持有下一个节点,MCS的队列是物理存在的,而 CLH 的队列是逻辑上存在的
3,CLH 锁释放时只需要改变自己的属性,MCS 锁释放则需要改变后继节点的属性。
【完】
注:文章内所有测试用例源码:https://gitee.com/wjie2018/test-case.git