CLH锁和MCS锁。
CLH锁适用于SMP(symmetric Multi-processing)对称多处理器结构。
MCS锁使用于NUMA(non-Uniform Memory Access)非一致存储访问。
SMP:一台服务器,多个CPU,但多个CPU共享一块内存。每个CPU读取内存的时间一致。
NUMA:一台服务器,多个CPU,但每个CPU有独立的内存,多个CPU通过互联模块相互访问。分为本地内存,和远地内存。
CLH:
CLHLock 是一个自旋锁,能确保无饥饿性,能保证FIFO的顺序来获取资源。
排队取钱:
A过来排队,发现前面没人,就直接到窗口办理了。
这时候B来了,排在了A的后面,发现A还没弄完,那就自己玩玩手机,时不时看看A搞完了。
这时候A搞完了,B看到A搞完了,就来到窗口前办理。
CLHLock的对象中只有一个tail,然后两个ThreadLocal 表示当前的node和前节点,myNode和myPred
方法也只有lock和unlock两个方法。
其中ThreadLocal 保存的对象是QNode,QNode就更简单了,只有一个locked,是volatile。
类图如下:
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
head <-- A <-- B <-- tail.
CLHLock的操作就跟这个类方法数量一样,一个初始化,一个lock,一个unlock。
1. 初始化:
1.1 新建tail,指向一个new的QNode
1.2 myPred指向null
1.3 myNode,指向一个new的QNode
2. 当有一个A线程来调用lock的时候。
2.1 获取当前ThreadLocal变量myNode,如果为空则指向一个new的QNode
2.2 获取tail指向的node,设置为myPred
2.3 tail指向当前myNode
2.4 如果前驱节点的locked为true,则自旋
3. 当A线程unlock的时候
3.1 获取当前TheadLocal变量myNode
3.2 myNode.locked = false
3.3 myNode指向myPred
具体图如下:
实现代码如下:
public class CLHLock implements Lock {
AtomicReference<QNode> tail = new AtomicReference<QNode>(new QNode());
ThreadLocal<QNode> myPred;
ThreadLocal<QNode> myNode;
public CLHLock() {
tail = new AtomicReference<QNode>(new QNode());
myNode = new ThreadLocal<QNode>() {
protected QNode initialValue() {
return new QNode();
}
};
myPred = new ThreadLocal<QNode>() {
protected QNode initialValue() {
return null;
}
};
}
@Override
public void lock() {
QNode qnode = myNode.get();
qnode.locked = true;
QNode pred = tail.getAndSet(qnode);
myPred.set(pred);
while (pred.locked) {
}
}
@Override
public void unlock() {
QNode qnode = myNode.get();
qnode.locked = false;
myNode.set(myPred.get());
}
}
MCS:
CLH在SMP效果比较好,但是如果是NUMA中不同的CPU内存不同,有可能会因为前驱节点所在的内存获取耗时较长,会导致性能较差。
所以NUMA中更好的实现是SMP。
依然还是排队:
1. A来排队,没有人,直接取钱
2. B来排队了,发现有人,就跟A说,你后面是我哈,等会搞完了叫我
3. A取完了,喊了一下B,B继续取
MCS 跟CLH很类似,区别在于CLH是根据前驱节点的locked自旋,而MCS是根据本地节点的locked自旋。所以,不需要频繁访问远处内存。
MCSLock只有两个方法,lock和unlock。
1. A来lock了
1.1 获取A线程的myNode,没有就创建
1.2 获取tail的节点作为前驱节点
1.3 如果前驱节点不为空,则当前节点locked改为true,前驱节点的next指向当前节点。根据当前node的locked自旋
1.4 如果前驱节点为空,不需要自旋
2. B也来lock了。
2.1 获取B线程的myNode,没有就创建
2.2 获取tail的节点作为前驱节点
2.3 改A的next指向B的node,当前节点locked改为true,自旋等待
3. A准备release了
3.1 获取A的myNode
3.2 如果A的后继节点为空,则把tail从myNode改为null
3.3 如果tail不是myNode,或者其他情况改不成功,有可能是有线程来排队了,已经加到了tail,还没有append到next来。则自旋等待后继节点append
3.4 如果A的后继节点不为空,则把后继节点的lock改成false,把当前next设置为null代码如下:
public class MCSLock implements Lock {
AtomicReference<QNode> tail;
ThreadLocal<QNode> myNode;
@Override
public void lock() {
QNode qnode = myNode.get();
QNode pred = tail.getAndSet(qnode);
if (pred != null) {
qnode.locked = true;
pred.next = qnode;
// wait until predecessor gives up the lock
while (qnode.locked) {
}
}
}
@Override
public void unlock() {
QNode qnode = myNode.get();
if (qnode.next == null) {
if (tail.compareAndSet(qnode, null))
return;
// wait until predecessor fills in its next field
while (qnode.next == null) {
}
}
qnode.next.locked = false;
qnode.next = null;
}
class QNode {
boolean locked = false;
QNode next = null;
}
}