锁9—自旋锁 VS 适应性自旋锁

************ 如有侵权请提示删除 ***************

文章目录

  • 锁9---自旋锁 VS 适应性自旋锁
    • 自旋锁
      • 1、概念:
      • 2、提出背景
      • 3、自旋锁的原理
      • 4、 自旋锁的优缺点
      • 5、自旋锁开启
    • 自适应自旋锁
    • 总结

自旋锁

1、概念:

当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)

2、提出背景

由于在多处理器环境中某些资源的有限性,有时需要互斥访问(mutual exclusion),这时候就需要引入锁的概念,只有获取了锁的线程才能够对资源进行访问,由于多线程的核心是CPU的时间分片,所以同一时刻只能有一个线程获取到锁。那么就面临一个问题,那么没有获取到锁的线程应该怎么办?

通常有两种处理方式:一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁叫做自旋锁,它不用将线程阻塞起来(NON-BLOCKING);还有一种处理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁。

3、自旋锁的原理

自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。

因为自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。由于这个原因,操作系统的内核经常使用自旋锁。但是,如果长时间上锁的话,自旋锁会非常耗费性能,它阻止了其他线程的运行和调度。线程持有锁的时间越长,则持有该锁的线程将被 OS(Operating System) 调度程序中断的风险越大。如果发生中断情况,那么其他线程将保持旋转状态(反复尝试获取锁),而持有该锁的线程并不打算释放锁,这样导致的是结果是无限期推迟,直到持有锁的线程可以完成并释放它为止。

解决上面这种情况一个很好的方式是给自旋锁设定一个自旋时间,等时间一到立即释放自旋锁。自旋锁的目的是占着CPU资源不进行释放,等到获取锁立即进行处理。但是如何去选择自旋时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!JDK在1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋时间不是固定的了,而是由前一次在同一个锁上的自旋时间以及锁拥有的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。

4、 自旋锁的优缺点

优点:自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

缺点:但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。

5、自旋锁开启

虽然在JDK1.4.2的时候就引入了自旋锁,但是需要使用“-XX:+UseSpinning”参数来开启。在到了JDK1.6以后,就已经是默认开启了。

举个栗子:

public class SpinLockTest {/*** 持有锁的线程,null表示锁未被线程持有*/private AtomicReference<Thread> ref = new AtomicReference<>();public void lock(){Thread currentThread = Thread.currentThread();while(!ref.compareAndSet(null, currentThread)){//当ref为null的时候compareAndSet返回true,反之为false//通过循环不断的自旋判断锁是否被其他线程持有}}public void unLock() {Thread cur = Thread.currentThread();if(ref.get() != cur){//exception ...}ref.set(null);}
}//自旋锁测试
public class SpinLockTestTest {static int count = 0;@Testpublic void spinLockTest() throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(100);CountDownLatch countDownLatch = new CountDownLatch(100);SpinLockTest2 spinLockTest2 = new SpinLockTest2();for (int i = 0; i < 100; i++) {executorService.execute(new Runnable() {@Overridepublic void run() {spinLockTest2.lock();++count;spinLockTest2.unLock();countDownLatch.countDown();}});}countDownLatch.await();System.out.println (count);}
}

通过上面的代码可以看出,自旋就是在循环判断条件是否满足,那么会有什么问题吗?如果锁被占用很长时间的话,自旋的线程等待的时间也会变长,白白浪费掉处理器资源。因此在JDK中,自旋操作默认10次,我们可以通过参数“-XX:PreBlockSpin”来设置,当超过来此参数的值,则会使用传统的线程挂起方式来等待锁释放。

自适应自旋锁

随着JDK的更新,在1.6的时候,又出现了一个叫做“自适应自旋锁”的玩意。它的出现使得自旋操作变得聪明起来,不再跟之前一样死板。所谓的“自适应”意味着对于同一个锁对象,线程的自旋时间是根据上一个持有该锁的线程的自旋时间以及状态来确定的。例如对于A锁对象来说,如果一个线程刚刚通过自旋获得到了锁,并且该线程也在运行中,那么JVM会认为此次自旋操作也是有很大的机会可以拿到锁,因此它会让自旋的时间相对延长。但是如果对于B锁对象自旋操作很少成功的话,JVM甚至可能直接忽略自旋操作。因此,自适应自旋锁是一个更加智能,对我们的业务性能更加友好的一个锁。

总结

自旋锁是为了提高资源的使用频率而出现的一种锁,自旋锁说的是线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。

自旋锁在等待期间不会睡眠或者释放自己的线程。自旋锁不适用于长时间持有CPU的情况,这会加剧系统的负担,为了解决这种情况,需要设定自旋周期,那么自旋周期的设定也是一门学问。

在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock

这里直接举栗子:

  1. TicketLock 是一种同步机制或锁定算法,它是一种自旋锁,它使用ticket 来控制线程执行顺序。
/*** @Description TicketLock 是一种同步机制或锁定算法,它是一种自旋锁,它使用ticket 来控制线程执行顺序。** TicketLock 是基于先进先出(FIFO) 队列的机制。* 它增加了锁的公平性,其设计原则如下:TicketLock 中有两个 int 类型的数值,* 开始都是0,第一个值是队列ticket(队列票据), 第二个值是 出队(票据)。* 队列票据是线程在队列中的位置,而出队票据是现在持有锁的票证的队列位置。* 可能有点模糊不清,简单来说,就是队列票据是你取票号的位置,出队票据是你距离叫号的位置。*** @Author zhoumm* @Version V1.0.0* @Since 1.0* @Date 2020-05-29*/
//这个设计是有问题的,因为获得自己的号码之后,是可以对号码进行更改的,这就造成系统紊乱,锁不能及时释放。
//需要有一个能确保每个人按会着自己号码排队办业务的角色
public class TicketLock {// 队列票据(当前排队号码)private AtomicInteger queueNum = new AtomicInteger();// 出队票据(当前需等待号码)private AtomicInteger dueueNum = new AtomicInteger();// 获取锁:如果获取成功,返回当前线程的排队号public int lock(){int currentTicketNum = dueueNum.incrementAndGet();while (currentTicketNum != queueNum.get()){// doSomething...}return currentTicketNum;}// 释放锁:传入当前排队的号码public void unLock(int ticketNum){queueNum.compareAndSet(ticketNum,ticketNum + 1);}//改进后,这就不再需要返回值,办业务的时候,要将当前的这一个号码缓存起来,在办完业务后,需要释放缓存的这条票据。//缺点:虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量queueNum ,// 每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。//为了解决这个问题,MCSLock 和 CLHLock 应运而生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);}}
}
  1. TicketLock 是基于队列的,那么 CLHLock 就是基于链表设计的
/*** @Description* TicketLock 是基于队列的,那么 CLHLock 就是基于链表设计的** CLH 是一种基于链表的可扩展,高性能,公平的自旋锁,申请线程只能在本地变量上自旋,* 它会不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。*** @Author zhoumm* @Version V1.0.0* @Since 1.0* @Date 2020-05-29*/
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;}
}
  1. MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销
/*** @Description* MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,* 直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。** 总结:* 1.都是基于链表,不同的是CLHLock是基于隐式链表,没有真正的后续节点属性,MCSLock是显示链表,有一个指向后续节点的属性。* 2.将获取锁的线程状态借助节点(node)保存,每个线程都有一份独立的节点,这样就解决了TicketLock多处理器缓存同步的问题。* @Author zhoumm* @Version V1.0.0* @Since 1.0* @Date 2020-05-29*/
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;// 循环判断,直到当前节点的锁标志位为falsewhile (currentNode.isLocked) {}}}public void unlock() {MCSNode currentNode = NODE.get();// next为null表示没有正在等待获取锁的线程if (currentNode.next == null) {// 更新状态并设置queue为nullif (UPDATE.compareAndSet(this, currentNode, null)) {// 如果成功了,表示queue==currentNode,即当前节点后面没有节点了return;} else {// 如果不成功,表示queue!=currentNode,即当前节点后面多了一个节点,表示有线程在等待// 如果当前节点的后续节点为null,则需要等待其不为null(参考加锁方法)while (currentNode.next == null) {}}} else {// 如果不为null,表示有线程在等待获取锁,此时将等待线程对应的节点锁状态更新为false,同时将当前线程的后继节点设为nullcurrentNode.next.isLocked = false;currentNode.next = null;}}}

参考:

  • 锁系列之自旋锁

  • java中的各种锁详细介绍

锁9---自旋锁 VS 适应性自旋锁相关推荐

  1. 1.6的锁优化(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁)

    高效并发是JDK 1.6的一个重要主题,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning).锁削除(Lock Elimin ...

  2. java锁结构之自旋锁与适应性自旋锁

    一.为什么需要自旋锁与适应性自旋锁 1.1.自旋锁的提出背景   由于在多处理器环境中某些资源的有限性,有时需要互斥访问,这时候就需要引入锁的概念,只有获取了锁的线程才能对资源进行访问,由于多线程的核 ...

  3. 自旋锁与适应性自旋锁

    自旋锁与适应性自旋锁 概念引入 自旋锁与非自旋锁流程图 自旋锁的缺陷 自旋锁的实现原理 自适应自旋锁 概念引入 在介绍自旋锁之前,我们需要介绍一些前提知识来帮助大家理解自旋锁的概念. 阻塞或唤醒一个J ...

  4. 偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化

    JDK1.6 对锁的优化: 偏向锁.轻量级锁.自旋锁.适应性自旋锁.锁消除.锁粗化 等技术. 锁主要存在四中状态,依次是: 无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态 锁可以升级不可降级,即 无 ...

  5. JUC并发编程系列详解篇十四(自旋锁 VS 适应性自旋锁)

    自旋锁 由于在多处理器环境中某些资源的有限性,有时需要互斥访问(mutual exclusion),这时候就需要引入锁的概念,只有获取了锁的线程才能够对资源进行访问,由于多线程的核心是CPU的时间分片 ...

  6. 非自旋锁VS自旋锁和适应性自旋锁

    1.什么是自旋 所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的.锁在原地循环的时候,是会消耗cp ...

  7. java适应性自旋锁_深夜!小胖问我,什么是自旋锁?怎么使用?适用场景是啥?...

    自旋锁 & 非自旋锁 什么是自旋?字面意思是 "自我旋转" .在 Java 中也就是循环的意思,比如 for 循环,while 循环等等.那自旋锁顾名思义就是「线程循环地去 ...

  8. 轻量级锁的加锁和解锁逻辑-自旋锁

    轻量级锁在加锁过程中,用到了自旋锁 所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的. 注意,锁在 ...

  9. 自旋锁和互斥锁实例_多线程编程之自旋锁

    一.什么是自旋锁 一直以为自旋锁也是用于多线程互斥的一种锁,原来不是! 自旋锁是专为防止多处理器并发(实现保护共享资源)而引入的一种锁机制.自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用 ...

最新文章

  1. Python 操作 MongoDB 数据库!
  2. php获取数组中,相同键名的键值之和
  3. 【C 语言】文件操作 ( 按照单个字符的方式读写文件 | fgetc 函数 | fputc 函数 )
  4. 2018年企业运维开发经典面试题
  5. USACO Training Section 1.1 坏掉的项链Broken Necklace
  6. STM32的CAN过滤器详解
  7. 样本量很少如何获得最佳的效果?最新小样本学习工具包来啦!
  8. gambas 编译_使用Gambas进行BASIC编程,适合初学者
  9. ReactNative入门 —— 动画篇(下)
  10. java 1.4 下载_j2sdk1.4.2
  11. ESP8266固件烧录软件flash_download_tools的安装过程
  12. 小何同学问了苹果CEO库克哪些问题?
  13. 再见PDF提取收费!我用100行Python代码搞定!
  14. 智能车|直流电机、编码器与驱动器
  15. 关于桌面程序被安全软件误判为HEUR:Trojan.Win32.Generic的解决方案
  16. 【JavaWeb】Servlet系列——HttpServletRequest接口详解
  17. 1.1 Python中turtle的使用(画奥运五环)
  18. 使用adb从手机拉取apk包
  19. 由于轮播图片超宽造成的影响
  20. Pytest如何重复执行N次脚本

热门文章

  1. 双代号网络图基础算法_从网络图探寻基因互作的蛛丝马迹(5)
  2. mac下搭建PHP开发环境
  3. 一本二本三本有什么区别
  4. vue html标签、Element-ui vue template字符串模板渲染
  5. 双非渣本Android四年磨一剑,赶紧收藏!
  6. 卸载aptitude,安装换回apt
  7. 从事互联网金融行业需具备哪些常识?
  8. 特征得分_Windows 10的Windows体验得分
  9. Por Una Cabeza(只差一步) 闻香识女人、真实的谎言、辛德勒名单中的探戈舞曲
  10. 从搜狗浏览器看“创新”