interface Lock {
void lock();
void unLock();
}
static class SpinLock implements Lock {
/**
* currentHoldLockThread == null,无线程占用锁
*/
private AtomicReference currentHoldLockThread =
new AtomicReference<>();
@Override
public void lock() {
Thread current = Thread.currentThread();
/**
* CAS 更新,当前值不是null 说明锁被占用,自旋
*/
while (currentHoldLockThread.compareAndSet(null,current))
{
}
}
@Override
public void unLock() {
Thread current = Thread.currentThread();
//只有持有锁的线程才能释放锁
if (currentHoldLockThread.get() == Thread.currentThread()) {
currentHoldLockThread.compareAndSet(current, null);
}
}
}
/**
* 保证公平性的spinLock,实现类似银行叫号,服务号=被呼叫号 获得锁
* != 则自旋
* 票号
* 排位号
* 服务号
*/
static class TicketLock implements Lock {
private AtomicInteger serviceNum = new AtomicInteger(1);
private AtomicInteger ticketNum = new AtomicInteger(0);
private final static ThreadLocal ownNum = new ThreadLocal<>();
@Override
public void lock() {
//获取自己的排队号
int ownCalledNum = ticketNum.incrementAndGet();
//保存在当前线程里
ownNum.set(ownCalledNum);
//排队号不等于服务号 自旋
while (ownCalledNum != serviceNum.get()) {
}
}
@Override
public void unLock() {
//服务号=自己的排队号才能释放锁
//保证只有获得锁的线程才能释放锁
serviceNum.compareAndSet(ownNum.get(),serviceNum.get()+1);
}
}
Ticket Lock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。
下面介绍的CLH锁和MCS锁都是为了解决这个问题的。
MCS 来自于其发明人名字的首字母: John Mellor-Crummey和Michael Scott。
CLH的发明人是:Craig,Landin and Hagersten。
线程检查自己的前驱结点是否被锁定,锁定则自旋,未锁定则执行
下面已三个线程在同一锁上竞争说明。
第一个线程locked,A前驱节点false,B尾节点true,unlock后B节点false
graph LR
A-->B
线程2,前驱节点B,true,尾节点true,现在在节点B上自旋,线程1释放锁后,B节点false,线程2开始执行
graph LR
B-->C
线程有两个本地变量,一个前驱一个尾节点。前驱节点判定线程是否自旋,tail节点关联下个线程的状态
graph LR
C-->D
1)公平,FIFO,先来后到的顺序进入锁,在前驱自旋,隐式指针(指向下个节点)
2)而且没有竞争同一个变量,因为每个线程只要等待自己的前继释放就好了
3)空间复杂度低,如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要O(n+L),n个线程有n个node,L个锁有L个tail
static class CLHLock implements Lock
{
static class CLHNode
{
private boolean isLocked = false;
}
private AtomicReference tail = new AtomicReference<>(new CLHNode());
/**
* 前一节点
*/
private ThreadLocal predNode;
/**
* 当前节点
*/
private ThreadLocal currentNode;
public CLHLock()
{
currentNode = new ThreadLocal()
{
@Override
protected CLHNode initialValue() {
return new CLHNode();
}
};
predNode = new ThreadLocal()
{
@Override
protected CLHNode initialValue() {
return null;
}
};
}
@Override
public void lock() {
//对于新线程 node = new CLHNode();
CLHNode node = currentNode.get();
//进行自旋
node.isLocked = true;
CLHNode pre = tail.getAndSet(node);
predNode.set(pre);
while (pre.isLocked)
{
}
}
@Override
public void unLock() {
CLHNode node = currentNode.get();
node.isLocked=false; //currentNode.set(predNode.get());
}
}
在自己loclNode上自旋,持有锁的线程释放锁时把next指向的node并 设置false,线程开始运行
static class MCSLock implements Lock
{
static class Node
{
volatile boolean isLocked=false;
Node next;
}
private ThreadLocal local;
private AtomicReference tail = new AtomicReference<>(null);
public MCSLock()
{
local = new ThreadLocal()
{
@Override
protected Node initialValue() {
return new Node();
}
};
}
@Override
public void lock() {
//获取当前线程的Node节点
Node node = local.get();
Node pre = tail.getAndSet(node);
if(pre != null)
{
node.isLocked = true;
pre.next = node;
//在自己node上自旋
while (node.isLocked)
{
}
}
}
@Override
public void unLock() {
Node node = local.get();
//node是最后一个节点
if(node.next == null)
{
if(tail.compareAndSet(node,null))
{
return;
}
while (node.next == null)
{
}
}
node.next.isLocked = false;
node.next = null;
}
}
从上面的实现可以看出,MSC与CLH最大的不同并不是链表是显示还是隐式,而是线程自旋的规则不同,CLH是在前趋结点的locked域上自旋等待,而MSC是在自己的结点的locked域上自旋等待。
lock方法:
若要获得锁,线程会把自己的结点放到队列的尾部,如果队列中开始有结点,就将前一个结点的next结点指向当前结点;
然后就在自己的locked域上自旋等待,直到它的前趋结点把自己的locked设置为false为止。
unlock方法:
若要释放锁,先检查自己的next域是否为null,如果为null,要么当前结点是尾结点,要么还有其他线程正在争用锁。不管是哪种情况都可以采用compareAndSet(q,null)来判断,其中q为当前结点,如果调用成功,则没有其他线程在争用锁,于是将tail设置为null返回;如果调用失败,说明另一个比较慢的线程正在试图获得锁,于是自旋等待它结束。在以上任一种情况,一旦出现有后继结点就将后续结点的locked域设置为false,然后返回。