不懂什么是锁?看看这篇你就明白了 (5)

但是上面这个设计是有问题的,因为获得自己的号码之后,是可以对号码进行更改的,这就造成系统紊乱,锁不能及时释放。这时候就需要有一个能确保每个人按会着自己号码排队办业务的角色,在得知这一点之后,我们重新设计一下这个逻辑

public class TicketLock2 { // 队列票据(当前排队号码) private AtomicInteger queueNum = new AtomicInteger(); // 出队票据(当前需等待号码) private AtomicInteger dueueNum = new AtomicInteger(); private ThreadLocal<Integer> ticketLocal = new ThreadLocal<>(); public void lock(){ int currentTicketNum = dueueNum.incrementAndGet(); // 获取锁的时候,将当前线程的排队号保存起来 ticketLocal.set(currentTicketNum); while (currentTicketNum != queueNum.get()){ // doSomething... } } // 释放锁:从排队缓冲池中取 public void unLock(){ Integer currentTicket = ticketLocal.get(); queueNum.compareAndSet(currentTicket,currentTicket + 1); } }

这次就不再需要返回值,办业务的时候,要将当前的这一个号码缓存起来,在办完业务后,需要释放缓存的这条票据。

缺点

TicketLock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量queueNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

为了解决这个问题,MCSLock 和 CLHLock 应运而生。

CLHLock

上面说到TicketLock 是基于队列的,那么 CLHLock 就是基于链表设计的,CLH的发明人是:Craig,Landin and Hagersten,用它们各自的字母开头命名。CLH 是一种基于链表的可扩展,高性能,公平的自旋锁,申请线程只能在本地变量上自旋,它会不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

public class CLHLock { public static class CLHNode{ private volatile boolean isLocked = true; } // 尾部节点 private volatile CLHNode tail; private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<>(); private static final AtomicReferenceFieldUpdater<CLHLock,CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,CLHNode.class,"tail"); public void lock(){ // 新建节点并将节点与当前线程保存起来 CLHNode node = new CLHNode(); LOCAL.set(node); // 将新建的节点设置为尾部节点,并返回旧的节点(原子操作),这里旧的节点实际上就是当前节点的前驱节点 CLHNode preNode = UPDATER.getAndSet(this,node); if(preNode != null){ // 前驱节点不为null表示当锁被其他线程占用,通过不断轮询判断前驱节点的锁标志位等待前驱节点释放锁 while (preNode.isLocked){ } preNode = null; LOCAL.set(node); } // 如果不存在前驱节点,表示该锁没有被其他线程占用,则当前线程获得锁 } public void unlock() { // 获取当前线程对应的节点 CLHNode node = LOCAL.get(); // 如果tail节点等于node,则将tail节点更新为null,同时将node的lock状态职位false,表示当前线程释放了锁 if (!UPDATER.compareAndSet(this, node, null)) { node.isLocked = false; } node = null; } } MCSLock

MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。MCS 来自于其发明人名字的首字母: John Mellor-Crummey 和 Michael Scott。

public class MCSLock { public static class MCSNode { volatile MCSNode next; volatile boolean isLocked = true; } private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<>(); // 队列 @SuppressWarnings("unused") private volatile MCSNode queue; private static final AtomicReferenceFieldUpdater<MCSLock,MCSNode> UPDATE = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,MCSNode.class,"queue"); public void lock(){ // 创建节点并保存到ThreadLocal中 MCSNode currentNode = new MCSNode(); NODE.set(currentNode); // 将queue设置为当前节点,并且返回之前的节点 MCSNode preNode = UPDATE.getAndSet(this, currentNode); if (preNode != null) { // 如果之前节点不为null,表示锁已经被其他线程持有 preNode.next = currentNode; // 循环判断,直到当前节点的锁标志位为false while (currentNode.isLocked) { } } } public void unlock() { MCSNode currentNode = NODE.get(); // next为null表示没有正在等待获取锁的线程 if (currentNode.next == null) { // 更新状态并设置queue为null if (UPDATE.compareAndSet(this, currentNode, null)) { // 如果成功了,表示queue==currentNode,即当前节点后面没有节点了 return; } else { // 如果不成功,表示queue!=currentNode,即当前节点后面多了一个节点,表示有线程在等待 // 如果当前节点的后续节点为null,则需要等待其不为null(参考加锁方法) while (currentNode.next == null) { } } } else { // 如果不为null,表示有线程在等待获取锁,此时将等待线程对应的节点锁状态更新为false,同时将当前线程的后继节点设为null currentNode.next.isLocked = false; currentNode.next = null; } } } CLHLock 和 MCSLock

都是基于链表,不同的是CLHLock是基于隐式链表,没有真正的后续节点属性,MCSLock是显示链表,有一个指向后续节点的属性。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzjwwx.html