• Java中的乐观锁、悲观锁、自旋锁、偏向锁、轻量级锁、重量级锁


    一、乐观锁与悲观锁

    乐观锁

    乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作

    java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

    悲观锁

    悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

    基础知识之:java线程阻塞的代价

    java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态(内核态)之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

    • 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
    • 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

    synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

    基础知识之:markword

    详细了解前往:java对象结构(对象头、实例数据、对齐填充)
    markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:
    在这里插入图片描述

    二、自旋锁、偏向锁、轻量级锁、重量级锁

    自旋锁

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

    但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。

    如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    自旋锁的优缺点

    • 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换
    • 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;

    偏向锁

    通过对大量数据的分析可以发现,大多数情况下锁竞争是不会发生的,往往是一个线程多次获得同一个锁,于是引入了偏向锁,偏向锁不会被刻意的释放,如果没有竞争,线程再次请求锁时可以直接获得锁。

    偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。

    如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

    它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

    轻量级锁

    轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

    轻量级锁的获取步骤可以看出,竞争锁的线程如果竞争失败不会进入阻塞状态,所以不会发生线程在用户态与核心态的切换,资源消耗比重量级锁少,但是竞争失败的线程会进入自旋状态,这又白白浪费了CPU计算资源。

    重量级锁Synchronized

    如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。

    在JDK1.5之前都是使用synchronized关键字保证同步的,Synchronized的作用相信大家都已经非常熟悉了;

    重量级锁在JVM中有一个监视器(Monitor),保持了两个队列:锁竞争队列和信号阻塞队列,一个实现线程互斥,另一个实现线程同步。重量级锁在底层是靠操作系统的Mutex Lock实现的,线程在阻塞和唤醒状态间切换需要操作系统将线程在用户态与核心态之间转换,成本很高,所以最早的synchronized效率不高

    偏向锁、轻量级锁和重量级锁对比

    锁类型优点缺点适用场景
    偏向锁加锁、解锁不需要额外资源消耗,效率较高如果线程间存在锁竞争,会带来额外的解锁消耗适用只有一个线程访问同步块的情景
    轻量级锁竞争的线程不会阻塞,提高了程序响应速度如果获取锁失败,会进入自旋消耗cpu针对锁占用时间短,对响应时间比较敏感的情况
    重量级锁线程竞争不使用自旋,不消耗cpu线程会被阻塞,影响响应时间锁占用时间较长,对吞吐量要求较高

    参考博客:https://blog.csdn.net/yz18931904/article/details/117281134
    java 中的锁 – 偏向锁、轻量级锁、自旋锁、重量级锁

  • 相关阅读:
    CSS文本超限后使用省略号代替
    ArcGIS标注的各种用法和示例
    HCNP Routing&Switching之DHCP中继
    UI框架布局
    Qt 中 deleteLater 使用总结
    面试高频问题----2
    图论16(Leetcode863.二叉树中所有距离为K的结点)
    HarmonyOS应用开发-首选项与后台通知管理
    设计模式(二)-创建者模式(1)-单例模式
    Python数据分析入门与实践(学习路线,视频教程,源码资料)
  • 原文地址:https://blog.csdn.net/m0_46378271/article/details/125900855