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

最后

即使是面试跳槽,那也是一个学习的过程。只有全面的复习,才能让我们更好的充实自己,武装自己,为自己的面试之路不再坎坷!今天就给大家分享一个Github上全面的Java面试题大全,就是这份面试大全助我拿下大厂Offer,月薪提至30K!

我也是第一时间分享出来给大家,希望可以帮助大家都能去往自己心仪的大厂!为金三银四做准备!
一共有20个知识点专题,分别是:

Dubbo面试专题

JVM面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Java并发面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Kafka面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

MongDB面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

MyBatis面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

MySQL面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Netty面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

RabbitMQ面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Redis面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Spring Cloud面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

SpringBoot面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

zookeeper面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

常见面试算法题汇总专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

计算机网络基础专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

设计模式专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

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

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

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 (;😉 {

Node t = tail.get();

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

if (t != null) {

t.next = node;

node.prev = t;

}

return node;

}

}

}

/**

  • @return 返回前驱

*/

Node enq1() {

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

threadNode.set(node);

Node prev = tail.getAndSet(node);

if (prev != null) {

prev.next = node;

}

return prev;

}

public void lock() {

Node node = threadNode.get();

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

/**

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

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

*/

state++;

System.out.println(“re lock thread=” + node.thread.getId() + “state=” + state);

return;

}

node = enq();

while (!node.shouldLocked()) {}

//判断node是否应该获取锁,若prev == null or node.locked=true,代表应该获取锁。则结束自旋

if (!node.locked) {

//当前驱为空时的情况,不过不改也问题不大

node.locked = true;

}

state++;

setExclusiveOwnerThread(node.thread);

System.out.println(String.format(“mcs get lock ok, thread=%s;locked=%b;node==tail=%b;next=%s;”, node.thread.getName(), node.locked, node == tail.get(), node.getNextName()));

}

public void lock1() {

Node node = threadNode.get();

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

/**

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

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

*/

state++;

System.out.println(“re lock thread=” + node.thread.getId() + “state=” + state);

return;

}

Node prev = enq1();

node = threadNode.get();

while (prev != null && !node.locked) {}

//判断node是否应该获取锁,若prev == null or node.locked=true,代表应该获取锁。则结束自旋

if (!node.locked) {

//当前驱为空时的情况,不过不改也问题不大

node.locked = true;

}

state++;

setExclusiveOwnerThread(node.thread);

System.out.println(String.format(“mcs get lock ok, thread=%s;locked=%b;node==tail=%b;next=%s;”, node.thread.getName(), node.locked, node == tail.get(), node.getNextName()));

}

public void unlock() {

Node node = threadNode.get();

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

throw new IllegalMonitorStateException();

}

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

state–;

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

if (state != 0) {

return;

}

//后继为空,则清空队列,将tail cas为null,

//如果此时刚好有节点入队列则退出循环,继续主动通知后继

while (node.next == null) {

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

//设置 tail为 null,threadNode remove

threadNode.remove();

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

return;

}

}

//threadNode 后继不为空 设置后继的locked=true,主动通知后继获取锁

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

//在node.next.locked前,设置setExclusiveOwnerThread 为null

setExclusiveOwnerThread(null);

node.next.locked = true;

threadNode.remove();

node = null; //help gc

}

}

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

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

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

String.format(“mcs un lock ok, thread=%s;clear queue”, node.thread.getName()));

return;

}

}

//threadNode 后继不为空 设置后继的locked=true,主动通知后继获取锁

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

//在node.next.locked前,设置setExclusiveOwnerThread 为null

setExclusiveOwnerThread(null);

node.next.locked = true;

threadNode.remove();

node = null; //help gc

}

}

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值