面试必备之深入理解自旋锁
自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态
缺点: 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗 CPU。使用不当会造成 CPU 使用率极高。
1. 实现
非公平不可重入:
当第一个线程 A 获取锁的时候,能够成功获取到,不会进入 while 循环,如果此时线程 A 没有释放锁,另一个线程 B 又来获取锁,此时由于不满足 CAS,所以就会进入 while 循环,不断判断是否满足 CAS,直到 A 线程调用 unlock 方法释放了该锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class SpinLock { private AtomicReference<Thread> cas = new AtomicReference<Thread>(); public void lock() { Thread current = Thread.currentThread(); while (!cas.compareAndSet(null, current)) { } } public void unlock() { Thread current = Thread.currentThread(); cas.compareAndSet(current, null); } }
|
2. 可重入
为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class ReentrantSpinLock { private AtomicReference<Thread> cas = new AtomicReference<Thread>(); private int count; public void lock() { Thread current = Thread.currentThread(); if (current == cas.get()) { count++; return; } while (!cas.compareAndSet(null, current)) { } } public void unlock() { Thread cur = Thread.currentThread(); if (cur == cas.get()) { if (count > 0) { count--; } else { cas.compareAndSet(cur, null); } } } }
|
3. TicketLock
TicketLock 主要解决的是公平性的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class TicketLock {
private AtomicInteger serviceNum = new AtomicInteger();
private AtomicInteger ticketNum = new AtomicInteger();
public int lock() { int currentTicketNum = ticketNum.incrementAndGet(); while (currentTicketNum != serviceNum.get()) { } return currentTicketNum; }
public void unlock(int ticketnum) { serviceNum.compareAndSet(ticketnum, ticketnum + 1); } }
|
上面的实现方式是,线程获取锁之后,将它的排队号返回,等该线程释放锁的时候,需要将该排队号传入。但这样是有风险的,因为这个排队号是可以被修改的,一旦排队号被不小心修改了,那么锁将不能被正确释放。一种更好的实现方式如下:
缺点:
多处理器系统上,每个进程 / 线程占用的处理器都在读写同一个变量 serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class TicketLockV2 {
private AtomicInteger serviceNum = new AtomicInteger();
private AtomicInteger ticketNum = new AtomicInteger();
private ThreadLocal<Integer> ticketNumHolder = new ThreadLocal<Integer>(); public void lock() { int currentTicketNum = ticketNum.incrementAndGet();
ticketNumHolder.set(currentTicketNum); while (currentTicketNum != serviceNum.get()) { } } public void unlock() { Integer currentTickNum = ticketNumHolder.get(); serviceNum.compareAndSet(currentTickNum, currentTickNum + 1); } }
|
4. CLHLock
CLH 锁是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋,获得锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
public class CLHLock {
public static class CLHNode { private volatile boolean isLocked = true; }
private volatile CLHNode tail; private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<CLHNode>(); 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) { while (preNode.isLocked) { } preNode = null; LOCAL.set(node); } } public void unlock() { CLHNode node = LOCAL.get(); if (!UPDATER.compareAndSet(this, node, null)) { node.isLocked = false; } node = null; } }
|
5. MCSLock
MCSLock 则是对本地变量的节点进行循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
|
public class MCSLock {
public static class MCSNode { volatile MCSNode next; volatile boolean isLocked = true; } private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>(); @SuppressWarnings("unused") private volatile MCSNode queue; private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "queue"); public void lock() { MCSNode currentNode = new MCSNode(); NODE.set(currentNode); MCSNode preNode = UPDATER.getAndSet(this, currentNode); if (preNode != null) { preNode.next = currentNode; while (currentNode.isLocked) { } } } public void unlock() { MCSNode currentNode = NODE.get(); if (currentNode.next == null) { if (UPDATER.compareAndSet(this, currentNode, null)) { return; } else { while (currentNode.next == null) { } } } else { currentNode.next.isLocked = false; currentNode.next = null; } } }
|
6. CLHLock 和 MCSLock 比较
- 都是基于链表,不同的是 CLHLock 是基于隐式链表,没有真正的后续节点属性,MCSLock 是显示链表,有一个指向后续节点的属性
- 将获取锁的线程状态借助节点(node)保存,每个线程都有一份独立的节点,这样就解决了 TicketLock 多处理器缓存同步的问题