普通自旋锁
利用AtomicReference.compareAndSet 确定对象的原子性,并通过while不断循环阻塞其他线程。
当上个线程unLock后,阻塞线程跳出while。
public class SpinningLock {
/**
* 持有锁的线程 为空标识没有线程持有
*/
private AtomicReference<Thread> ref = new AtomicReference<>();
/**
* 锁
*/
public void Lock(){
// 获取当前线程
Thread currentThread = Thread.currentThread();
// ref.compareAndSet
// 1. 当ref = null时 compareAndSet 把当前线程赋值到ref 并返回true
// 2. 当ref != null时 compareAndSet 返回false
while (!ref.compareAndSet(null, currentThread)){
// 通过循环不断的自旋判断锁是否被其他线程持有 hlod资源
}
}
/**
* 解锁
*/
public void unLock(){
Thread currentThread = Thread.currentThread();
ref.get();
ref.compareAndSet(currentThread, null);
}
}
优点:
- 无需上下文切换,速率快
缺点:
- conpareAndSet是其核心,底层通过各系统cpu指令实现(依赖硬件)。
- 无法保证等待线程按FIFO顺序获得锁(非公平)
自旋锁变种(TicketLock-解决普通自旋锁 公平性问题)
类似排号流程:
lock()
用户A和B去医院排号,A到了1号,B取到了2号 => myNum.set(ticketNum.getAndIncrement());。
医生按照票号顺序叫号对A一顿服务,B就老老实实坐板凳等着 => while (serviceNum.get() != myNum.get())unlock()
当医生服务完后,看了眼A的票号,下个应该是2号治疗了 => serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
然后将A请出去 => myNum.remove(); 让还在while等着的B进来。
public class TicketLock implements Lock {
// 服务号 线程完成作业 +1
private AtomicInteger serviceNum = new AtomicInteger(0);
// 取票号 线程进入时取号
private AtomicInteger ticketNum = new AtomicInteger(0);
// 当前线程持有号
private final ThreadLocal<Integer> myNum = new ThreadLocal<>();
@Override
public void lock() {
// 当前线程取号
myNum.set(ticketNum.getAndIncrement());
// 当服务号 != 线程所取到的号 死循环阻塞 监听到serviceNum = myNum时退出循环
while (serviceNum.get() != myNum.get()) {
}
}
@Override
public void unlock() {
serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
myNum.remove();
}
}
该变种虽然解决了公平性问题,但是在多处理系统上需要对serviceNum进行读写同步,增大了内存和总线的流量,降低了系统整体性能。
自旋锁变种(CLHLock)
CLHLock发明人是:Craig,Landin and Hagersten 所以才以CLH开头。这是种基于链表,可扩展和高性能的自旋锁。
该设计的思想主要是将线程有序的抽象成一个个Node对象,利用对象的线程共享locked属性,判断是否存在上个节点持有锁,以此阻塞或通过。每次获取锁时将当前node放入尾部链表,将上个node放入前区链表;解锁时获取当前node,置为false,让后续线程通过,再将currNode置为preNode,因为初始化时是个初始对象,相当于平移,这样就将当前node移出节点
public class CLHLock implements Lock {
// 指向尾部节点
private final AtomicReference<QNode> tail;
// 指向前驱节点
private final ThreadLocal<QNode> preNode;
// 当前节点
private final ThreadLocal<QNode> myNode;
public CLHLock() {
tail = new AtomicReference<>(new QNode());
myNode = ThreadLocal.withInitial(QNode::new);
preNode = new ThreadLocal<>();
}
@Override
public void lock() {
// 获取一个QNode
QNode qnode = myNode.get();
// 设置自己的状态为locked=true表示需要获取锁
qnode.locked = true;
// 链表的尾部设置为本线程的qNode,并将之前的尾部设置为当前线程的preNode
QNode pre = tail.getAndSet(qnode);
// 把旧的节点放入前驱节点。
preNode.set(pre);
// 当前线程在前驱节点的locked字段上旋转,直到前驱节点释放锁资源
while (pre.locked) {
}
}
@Override
public void unlock() {
// 获取当前节点
QNode qnode = myNode.get();
// 释放锁操作时将自己的locked设置为false,可以使得自己的后继节点可以结束自旋
qnode.locked = false;
// 回收自己这个节点,从虚拟队列中删除
// 将当前节点引用置为自己的preNode,那么下一个节点的preNode就变为了当前节点的preNode,这样就将当前节点移出了队列
myNode.set(preNode.get());
}
private class QNode {
// true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁,且不需要锁 volatile 修饰其它线程可见
private volatile boolean locked = false;
}
优点:
空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail),CLH的一种变体被应用在了JAVA并发框架中
缺点:
在NUMA系统结构下性能很差(在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣)
自旋锁变种(MCSLock)
MCS 来自于其发明人名字的首字母: John Mellor-Crummey和Michael Scott。是一种基于链表的可扩展、高性能、公平的自旋锁。
MCSLock 与 CLHNode的差异
- 从代码实现来看,CLH比MCS要简单得多。
- 从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋。
- MCSLock:
while (!qnode.locked)
- CLHNode:
while (pre.locked)
- 从链表队列来看,CLHNode不直接持有前驱节点,CLH锁释放时只需要改变自己的属性;MCSNode直接持有后继节点,MCS锁释放需要改变后继节点的属性。
- CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性。
public class MCSLock implements Lock {
// 尾结点
private AtomicReference<QNode> tail;
// 当前节点
private ThreadLocal<QNode> myNode;
public MCSLock() {
tail = new AtomicReference<>(null);
myNode = ThreadLocal.withInitial(QNode::new);
}
@Override
public void lock() {
// 获取当前节点
QNode qnode = myNode.get();
// 赋值当前节点 并返回旧值(即上个节点)
QNode preNode = tail.getAndSet(qnode);
// 上个节点不为空 说明有线程持有资源
if (preNode != null){
// 设置当前节点设置为false
qnode.locked = false;
// 将上个节点的指针 指向当前节点
preNode.next = qnode;
// 等待上个节点执行完毕
while (!qnode.locked) {
}
}
// 设置当前节点为持有资源进程
qnode.locked = true;
}
@Override
public void unlock() {
// 获取当前节点
QNode qnode = myNode.get();
if (qnode.next == null) {
//后面没有等待线程的情况
if (tail.compareAndSet(qnode, null)) {
//真的没有等待线程,则直接返回,不需要通知
return;
}
// if (tail.compareAndSet(qnode, null)) return false 说明已经进入了另外一个线程
while (qnode.next == null) {
}
}
//后面有等待线程,则通知后面的线程
qnode.next.locked = true;
qnode.next = null;
}
private class QNode {
/**
* 是否被qNode所属线程锁定
*/
private volatile boolean locked = false;
/**
* 与CLHLock相比,多了这个真正的next
*/
private volatile QNode next = null;
}
}
优点:
申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销,解决NUMA系统结构的思路是MCS队列锁。
网友评论