AQS源码解读(番外篇)——四种自旋锁原理详解(Java代码实现SpinLock、TicketSpinLock(1)

最后

总而言之,面试官问来问去,问的那些Redis知识点也就这么多吧,复习的不够到位,知识点掌握不够熟练,所以面试才会卡壳。将这些Redis面试知识解析以及我整理的一些学习笔记分享出来给大家参考学习

还有更多学习笔记面试资料也分享如下:

都是“Redis惹的祸”,害我差点挂在美团三面,真是“虚惊一场”

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取


实现原理

SpinLock原理很简单,多个线程循环CAS修改一个共享变量,修改成功则停止自旋获取锁。

代码实现

实现接口参考了JavaLock,核心方法在tryAcquiretryRelease。获取锁的方式实现了自旋,可中断自旋,自旋超时中断,不自旋。共享变量state不仅作为锁的状态标志(state=0锁空闲,state>0有线程持有锁),同时可作为自旋锁重入的次数。exclusiveOwnerThread记录当前持有锁的线程。

public class SpinLock implements Lock {

protected volatile int state = 0;

private volatile Thread exclusiveOwnerThread;

@Override

public void lock() {

for(;😉 {

//直到获取锁成功,才结束循环

if (tryAcquire(1)) {

return;

}

}

}

@Override

public void lockInterruptibly() throws InterruptedException {

for(;😉 {

if (Thread.interrupted()) {

//有被中断 抛异常

throw new InterruptedException();

}

if (tryAcquire(1)) {

return;

}

}

}

/**

  • 返回获取锁的结果,不会自旋

  • @return

*/

@Override

public boolean tryLock() {

return tryAcquire(1);

}

/**

  • 返回获取自旋锁的结果,会自旋一段时间,超时后停止自旋

  • @param time

  • @param unit

  • @return

  • @throws InterruptedException

*/

@Override

public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {

long nanosTimeout = unit.toNanos(time);

if (nanosTimeout <= 0L) {

return false;

}

final long deadline = System.nanoTime() + nanosTimeout;

for(;😉 {

if (Thread.interrupted()) {

//有被中断 抛异常

throw new InterruptedException();

}

if (tryAcquire(1)) {

return true;

}

nanosTimeout = deadline - System.nanoTime();

if (nanosTimeout <= 0L) {

//超时自旋,直接返回false

return false;

}

}

}

@Override

public void unlock() {

tryRelease(1);

}

@Override

public Condition newCondition() {

throw new UnsupportedOperationException();

}

public int getState() {

return state;

}

/**

  • 获取持有锁的当前线程

  • @return

*/

public Thread getExclusiveOwnerThread() {

return exclusiveOwnerThread;

}

/**

  • 获取当前线程重入次数

  • @return

*/

public int getHoldCount() {

return isHeldExclusively() ? getState() : 0;

}

/**

  • 释放锁

  • @param releases

  • @return

*/

protected boolean tryRelease(int releases) {

int c = getState() - releases;

Thread current = Thread.currentThread();

if (current != getExclusiveOwnerThread())

//不是当前线程,不能unLock 抛异常

throw new IllegalMonitorStateException();

boolean free = false;

if (c <= 0) {

//每次减一,c = 0,证明没有线程持有锁了,可以释放了

free = true;

c = 0;

setExclusiveOwnerThread(null);

System.out.println(String.format(“spin un lock ok, thread=%s;”, current.getName()));

}

//排它锁,只有当前线程才会走到这,是线程安全的 修改state

setState©;

return free;

}

/**

  • 获取锁

  • @param acquires

  • @return

*/

protected boolean tryAcquire(int acquires) {

final Thread current = Thread.currentThread();

int c = getState();

if (c == 0) {

//若此时锁空着,则再次尝试抢锁

if (compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

System.out.println(String.format(“spin lock ok, thread=%s;”, current.getName()));

return true;

}

}

//若当前持锁线程是当前线程(重入性)

else if (current == getExclusiveOwnerThread()) {

int nextc = c + acquires;

if (nextc < 0) // overflow

throw new Error(“Maximum lock count exceeded”);

//重入

setState(nextc);

System.out.println(String.format(“spin re lock ok, thread=%s;state=%d;”, current.getName(), getState()));

return true;

}

return false;

}

/**

  • 判断当前线程是否持有锁

  • @return

*/

protected final boolean isHeldExclusively() {

return getExclusiveOwnerThread() == Thread.currentThread();

}

protected void setState(int state) {

this.state = state;

}

protected void setExclusiveOwnerThread(Thread exclusiveOwnerThread) {

this.exclusiveOwnerThread = exclusiveOwnerThread;

}

protected final boolean compareAndSetState(int expect, int update) {

return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

}

protected static final Unsafe getUnsafe() {

try {

//不可以直接使用Unsafe,需要通过反射,当然也可以直接使用atomic类

Field theUnsafe = Unsafe.class.getDeclaredField(“theUnsafe”);

theUnsafe.setAccessible(true);

Unsafe unsafe = (Unsafe) theUnsafe.get(null);

if (unsafe != null) {

return unsafe;

}

} catch (NoSuchFieldException | IllegalAccessException e) {

e.printStackTrace();

}

throw new RuntimeException(“get unsafe is null”);

}

private static final Unsafe unsafe = getUnsafe();

private static final long stateOffset;

static {

try {

stateOffset = unsafe.objectFieldOffset

(SpinLock.class.getDeclaredField(“state”));

} catch (Exception ex) { throw new Error(ex); }

}

SpinLock的特点

  • 优势:SpinLock实现原理简单,线程间没有频繁的上下文切换,执行速度快,性能高。

  • 缺陷:SpinLock是不公平的,无法满足等待时间最长的线程优先获取锁,造成 “线程饥饿”。

  • 缺陷:由于每个申请自旋锁的处理器均在一个全局变量上自旋检测,系统总线将因为处理器间的缓存同步而导致繁重的流量,从而降低了系统整体的性能。

由于传统自旋锁无序竞争的本质特点,内核执行线程无法保证何时可以取到锁,某些执行线程可能需要等待很长时间,导致“不公平”问题的产生。有两个方面的原因:

  1. 随着处理器个数的不断增加,自旋锁的竞争也在加剧,自然导致更长的等待时间。

  2. 释放自旋锁时的重置操作将使所有其它正在自旋等待的处理器的缓存无效化,那么在处理器拓扑结构中临近自旋锁拥有者的处理器可能会更快地刷新缓存,因而增大获得自旋锁的机率。

TicketSpinLock排队自旋锁的优化与不足


由于SpinLock传统自旋锁是不公平的,且在锁竞争激烈的服务器,”不公平“问题尤为严重。因此公平的排队自旋锁就应运而生了。( Linux 内核开发者 Nick Piggin 在 Linux 内核 2.6.25 版本中引入了排队自旋锁,并不是他发明的排队自旋锁,排队自旋锁只是一种思想,Windows中排队自旋锁采取了不一样的实现逻辑。)

实现原理

排队自旋锁通过保存执行线程申请锁的顺序信息来解决“不公平”问题。TicketSpinLock仍然使用原有SpinLock的数据结构,为了保存顺序信息,加入了两个新变量,分别是锁需要服务的序号(serviceNum)和未来锁申请者的票据序号(ticketNum)。当serviceNum=ticketNum时,代表锁空闲,线程可以获取锁。

代码实现

基本共享变量serviceNumticketNum,辅助变量threadOwnerTicketNumstateexclusiveOwnerThread

线程获取锁,获取排队序号ticketNum,并自增排队序号,自旋比较获取的排队序号和当前服务序号是否相等(serviceNum != myTicketNum),相等则停止自旋,获取锁。

threadOwnerTicketNum变量不是必须的,但是如果要实现重入锁,是必不可少的,用于记录每个线程持有的排队序号。当检测线程持有的排队序号为空时,可获取排队序号,如果不为空,则此时有其他线程持有锁。判断持有锁的线程是否为当前线程,是则重入。

stateexclusiveOwnerThread用于重入锁的实现,但是并不能代表锁的持有状态(可能有瞬时性)。

线程释放锁,因为是重入锁,需要state自减为0时,serviceNum才自增加1。

因为serviceNumstateexclusiveOwnerThread的操作环境是天生线性安全的,所以不需要CAS

public class TicketSpinLock {

//服务序号,不需要cas,因为释放锁的只有一个线程,serviceNum++的环境是天生安全的

private volatile int serviceNum = 0;

//排队序号,cas

private AtomicInteger ticketNum = new AtomicInteger(0);

//记录当前线程的排队号,主要的作用是为了实现可重入,防止多次取号

private ThreadLocal threadOwnerTicketNum = new ThreadLocal();

//state不作为锁状态标志,只代表锁重入的次数

protected volatile int state = 0;

private volatile Thread exclusiveOwnerThread;

public void lock() {

final Thread current = Thread.currentThread();

Integer myTicketNum = threadOwnerTicketNum.get();

if (myTicketNum == null) {

myTicketNum = ticketNum.getAndIncrement();

threadOwnerTicketNum.set(myTicketNum);

while (serviceNum != myTicketNum) {}

//若拿的排队号刚好等于服务序号,说明可以获取锁,即获取锁成功

setExclusiveOwnerThread(current);

state ++ ;

System.out.println(String.format(“ticket lock ok, thread=%s;state=%d;serviceNum=%d;next-ticketNum=%d;”, current.getName(), getState(), serviceNum, ticketNum.get()));

return;

}

//若已经取号,判断当前持锁线程是当前线程(重入性)

if (current == getExclusiveOwnerThread()) {

//重入

state++;

System.out.println(String.format(“ticket re lock ok, thread=%s;state=%d;serviceNum=%d;next-ticketNum=%d;”, current.getName(), getState(), serviceNum, ticketNum.get()));

return;

}

}

public void unlock() {

if (Thread.currentThread() != getExclusiveOwnerThread())

//不是当前线程,不能unLock 抛异常

throw new IllegalMonitorStateException();

state–;

if (state == 0) {

//完全释放锁,owner+1

//服务序号是线性安全的,无需cas

threadOwnerTicketNum.remove();

setExclusiveOwnerThread(null);

serviceNum ++;

System.out.println(String.format(“ticket un lock ok, thread=%s;next-serviceNum=%d;ticketNum=%d;”, Thread.currentThread().getName(), serviceNum, ticketNum.get()));

}

}

public int getState() {

return state;

}

public Thread getExclusiveOwnerThread() {

return exclusiveOwnerThread;

}

public void setExclusiveOwnerThread(Thread exclusiveOwnerThread) {

this.exclusiveOwnerThread = exclusiveOwnerThread;

}

}

TicketSpinLock的特点

TicketSpinLock是公平锁,基本解决了传统自旋锁“不公平”问题,但是并没有解决处理器缓存同步问题。

在大规模多处理器系统和 NUMA系统中,排队自旋锁(包括传统自旋锁)同样存在一个比较严重的性能问题:由于执行线程均在同一个共享变量上自旋,将导致所有参与排队自旋锁操作的处理器的缓存变得无效。如果排队自旋锁竞争比较激烈的话,频繁的缓存同步操作会导致系统总线和处理器内存的流量繁重,从而大大降低了系统整体的性能。

CLH队列自旋锁的优化与不足


CLH(Craig, Landin, and Hagersten)锁是基于链表实现的FIFO队列公平锁。CLH是其三个发明者的人名缩写(Craig, Landin, and Hagersten)。

实现原理

获取锁的线程先入队列,入到队列尾部后不断自旋检查前驱节点的状态,前驱为空 or检测到前驱释放锁则该节点获取锁。入队列尾部是CAS操作,保证了有序出入队列。

节点获取锁的条件:前驱为空or检测到前驱释放锁

代码实现

CLH锁只是一种思想,实现的方式很多,网上有基于隐式链表实现的,即节点与节点之间不是真实连接,只是当前线程记录了前驱节点和自己的节点。

如下代码实现的链表是真实连接的,即线程当前节点有前驱指针的变量(prev)。节点除了有前驱指针外还有一个locked变量记录节点锁持有状态,locked=true代表线程节点正在持有锁,或者需要锁,初始线程节点locked为true,释放锁后将locked改为false,以让其后继自旋感知前驱释放锁了,并停止自旋获取锁

public class CLHSpinLock {

class Node {

volatile Node prev;

/**

  • true表示正在持有锁,或者需要锁

  • false表示释放锁

*/

volatile boolean locked = true;

volatile Thread thread;

Node(Thread thread) {

this.thread = thread;

}

boolean isPrevLocked() {

return prev != null && prev.locked;

}

String getPrevName() {

return prev == null ? “null” : prev.thread.getName();

}

}

private volatile AtomicReference tail = new AtomicReference();

//线程和node key-value

private ThreadLocal threadNode = new ThreadLocal();

//记录持有锁的当前线程

private volatile Thread exclusiveOwnerThread;

//记录重入

protected volatile int state = 0;

//因为exclusiveOwnerThread和state只是作为记录,线程获取锁后才会设置这两个值,具有有瞬时性,所以不能作为锁是否空闲的判断标志

public Thread getExclusiveOwnerThread() {

return exclusiveOwnerThread;

}

public void setExclusiveOwnerThread(Thread exclusiveOwnerThread) {

this.exclusiveOwnerThread = exclusiveOwnerThread;

}

/**

  • cas自旋入队列->尾部

  • @return

*/

Node enq() {

Node node = new Node(Thread.currentThread());

threadNode.set(node);

for (;😉 {

Node prev = tail.get();

//cas设置tail指针指向node

if (tail.compareAndSet(prev, node)) {

node.prev = prev;

return node;

}

}

}

public void lock() {

Node node = threadNode.get();

if (node != null && getExclusiveOwnerThread() != null && node.thread == getExclusiveOwnerThread()) {

/**

  • 一般情况node != null,说明有同一个线程已经调用了lock()

  • 判断持有锁的线程是node.thread,重入

*/

state++;

System.out.println(String.format(“re lock thread=%s;state=%d;”, node.thread.getName(), state));

return;

}

node = enq();

while (node.isPrevLocked()) {

}

//前驱未持有锁,说明可以获取锁,即获取锁成功, prev设置为null,断开与链表的连接,相当于前驱出队列

System.out.println(String.format(“clh get lock ok, thread=%s;prev=%s;”, node.thread.getName(), node.getPrevName()));

setExclusiveOwnerThread(node.thread);

state++;

node.prev = null;

}

public void unlock() {

Node node = threadNode.get();

if (node.thread != getExclusiveOwnerThread()) {

throw new IllegalMonitorStateException();

}

//在node.setLocked(false) 之前设置 state

–state;

//完全释放锁,locked改为false,让其后继感知前驱锁释放并停止自旋

if (state == 0) {

System.out.println(String.format(“clh un lock ok, thread=%s;”, node.thread.getName()));

setExclusiveOwnerThread(null);

node.locked = false;

threadNode.remove();

node = null; //help gc

}

}

CLH锁的特点

CLH锁是公平的,且空间复杂度是常数级。在一定程度上减轻了排队自旋锁和传统自旋锁在同一个共享变量上自旋的问题,但是并不彻底。

CLH在SMP系统结构(每个cpu缓存一致)下是非常有效的。但在NUMA系统结构(每个cpu有各自的缓存)下,每个线程有自己的内存,如果前驱节点的内存位置比较远,自旋判断前驱节点的locked状态,性能将大打折扣。

MCS锁对CLH锁的优化


因为CLH前驱节点的内存位置可能较远,在NUMA系统结构下导致自旋判断前驱节点的locked状态的性能很低,所以一种解决NUMA系统结构的思路就是MCS队列锁。MCS也是其发明者人名缩写( John M. Mellor-Crummey 和 Michael L. Scott)。

实现原理

MCS和CLH区别在于,MCS是对自己节点的锁状态不断自旋当前驱为空即队列中只有自己一个节点或者检测到自己节点锁状态可以获取锁,则线程获取锁。

代码实现

MCS入队列方式,代码中实现了两种,一种是节点有前驱指针(enq()),这样MCS中的链表队列就是双向队列,另一种是入队列后返回前驱节点(enq1()),这样节点的前驱指针就是隐式的。

内部类Node中prev属性可有可无,next必须,节点释放锁时需要主动通知后继。locked代表锁持有状态,locked=false 代表线程未持有锁,locked=true代表线程可持有锁,初始节点locked=false

获取锁,线程先入队列尾部,并检查前驱是否为空,为空则停止自旋获取锁,不为空判断当前节点的locked是否为true,为true停止自旋获取锁。

释放锁,当前节点将后继节点的locked改为true,以让后继感知到自己的锁状态是可以获取锁了。如果当前节点后继为空,则自旋清空队列。

public class MCSSpinLock {

class Node {

//prev 可有可无

volatile Node prev;

volatile Node next;

//false代表未持有锁,true代表可持有锁

volatile boolean locked = false;

volatile Thread thread;

Node(Thread thread) {

this.thread = thread;

}

public boolean shouldLocked() {

return prev == null || locked;

}

public String getNextName() {

return next == null ? “null” : next.thread.getName();

}

}

private volatile AtomicReference tail = new AtomicReference();

//线程和node key-value

private ThreadLocal threadNode = new ThreadLocal();

//记录持有锁的当前线程

private volatile Thread exclusiveOwnerThread;

//记录重入

protected volatile int state = 0;

//因为exclusiveOwnerThread和state只是作为记录,线程获取锁后才会设置这两个值,具有有瞬时性,所以不能作为锁是否空闲的判断标志

public Thread getExclusiveOwnerThread() {

return exclusiveOwnerThread;

}

public void setExclusiveOwnerThread(Thread exclusiveOwnerThread) {

this.exclusiveOwnerThread = exclusiveOwnerThread;

}

/**

  • cas自旋入队列->尾部

  • @return

*/

Node enq() {

Node node = new Node(Thread.currentThread());

threadNode.set(node);

for (;😉 {

最后

无论是哪家公司,都很重视基础,大厂更加重视技术的深度和广度,面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。

针对以上面试技术点,我在这里也做一些分享,希望能更好的帮助到大家。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

private volatile Thread exclusiveOwnerThread;

//记录重入

protected volatile int state = 0;

//因为exclusiveOwnerThread和state只是作为记录,线程获取锁后才会设置这两个值,具有有瞬时性,所以不能作为锁是否空闲的判断标志

public Thread getExclusiveOwnerThread() {

return exclusiveOwnerThread;

}

public void setExclusiveOwnerThread(Thread exclusiveOwnerThread) {

this.exclusiveOwnerThread = exclusiveOwnerThread;

}

/**

  • cas自旋入队列->尾部

  • @return

*/

Node enq() {

Node node = new Node(Thread.currentThread());

threadNode.set(node);

for (;😉 {

最后

无论是哪家公司,都很重视基础,大厂更加重视技术的深度和广度,面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。

针对以上面试技术点,我在这里也做一些分享,希望能更好的帮助到大家。

[外链图片转存中…(img-K08R16td-1715811370372)]

[外链图片转存中…(img-j7bCcnD4-1715811370372)]

[外链图片转存中…(img-mW12qHkZ-1715811370373)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值