zoukankan      html  css  js  c++  java
  • Java Synchronized 锁的实现原理详解及偏向锁-轻量锁-重量锁

    Synchronize是重量级锁吗?是互斥锁吗?

    它的实现原理?

    前言

      线程安全是并发编程中的重要关注点,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多个线程共同操作共享数据。因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。

      在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块。

       

    synchronized底层语义原理

    Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的

    要深入理解synchronized实现原理,就需要先来了解在JVM中Java对象的结构,如图所示:在堆中的对象分为三块区域:对象头、实例数据和对齐填充。

    • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

    • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

    • 对象头:如下表格
    虚拟机位数头对象结构说明
    32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
    32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

     

      

     

     

     

    synchronized实现的三种方式

    1、同步普通方法

    public class SyncMethod {
       public int i;
       public synchronized void syncTask(){
          i++;
       }
    }

    java -P  

    Classfile  src/main/java/com/zejian/concurrencys/SyncMethod.class
      Last modified 2017-6-2; size 308 bytes
      MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
      Compiled from "SyncMethod.java"
    public class com.zejian.concurrencys.SyncMethod
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool;
    
     
      //==================syncTask方法======================
      public synchronized void syncTask();
        descriptor: ()V
        //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: dup
             2: getfield      #2                  // Field i:I
             5: iconst_1
             6: iadd
             7: putfield      #2                  // Field i:I
            10: return
          LineNumberTable:
            line 12: 0
            line 13: 10
    }
    SourceFile: "SyncMethod.java"

      从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

      这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化。

    JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放

     

    2、同步方法块

    public void sync1() {
        synchronized(this) {
            // do somethings  锁的是当前实例对象
        }
    }
    
    public void sync2() {
        synchronized(MyTest.css) {
            // do somethings 锁的的是当前类class对象
        }
    }

    //反编译class文件后,可以看到在sysnchronized
    1:flags: ACC_PUBLIC
    .............
    ........

    3: monitorenter //进入同步方法 //
    4:..........省略其他
    15: monitorexit //退出同步方法
    16: goto
    24 //省略其他.......
    21: monitorexit //退出同步方法

     从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置.

    3、同步静态方法

    1 public static synchronized void sync2() {
    2     // do somethings  //锁的是当前类class
    3 }

    总结:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步, 但是两者的实现细节不一样.

    1. 代码块同步: 通过使用monitorenter和monitorexit指令实现的.
    2. 同步方法: ACC_SYNCHRONIZED修饰

    锁的竞争

    随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

    JVM 默认几秒后开启偏向锁

      如果你确定应用程序中所有的锁通常是在竞争状态,你可以通过JVM参数关闭偏向锁UseBiasedLocking = false,那么程序会默认进入轻量锁状态。

    1、偏向锁(A线程独占锁,不用上下文切换。对象头标识)

      在实际场景中,如果一个同步方法,没有多线程竞争,并且总是由同一个线程多次获取锁,如果每次还有阻塞线程,唤醒cpu从用户态转核心态,那么对于cpu是一种资源的浪费,为了解决这类问题,旧引入了偏向锁的概念。
    偏向锁的核心思想是,如果不存在竞争的线程一个线程获得了锁,那么锁就进入偏向模式。此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果。
      当出现多个线程竞争锁之后偏向锁失败后,会升级为轻量级锁。
     

    2、轻量锁(A线程拥有锁,B获取,竞争,自旋(jdk1.7以后智能自转))

      如果说偏向锁是为了解决同步代码在单线程下访问性能问题,那么轻量锁是为了解决减少无实际竞争情况下,使用重量级锁产生的性能消耗

      轻量锁,顾名思义,轻量是相对于重量的问题,使用轻量锁时,不需要申请互斥量(mutex),而是将mark word中的信息复制到当前线程的栈中,然后通过cas尝试修改mark word并替换成轻量锁,如果替换成功则执行同步代码。如果此时有线程2来竞争,并且他也尝试cas修改mark word但是失败了,那么线程2会进入自旋状态,如果在自旋状态也没有修改成功,那么轻量锁将膨胀成状态,mark word会被修改成重量锁标记(10) ,线程进入阻塞状态。当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁

    3、自旋锁 (A线程拥有锁,B线程自旋尝试获取)

    轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,会进行自旋锁的优化。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(默认10次),在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。最后没办法也就只能升级为重量级锁了。

    4、重量锁 (B线程自旋获取不到锁,膨胀重量锁,阻塞A线程。直到B执行完)

      在jvm规范中,synchronized是基于监视器锁(monitor)来实现的。如前文所提到的,它会在同步代码之前添加一个monitorenter指令,获取到该对象的monitor,同时它会在同步代码结束处和异常处添加一个monitorexit指令去释放该对象的monitor,需要注意的是每一个对象都有一个monitor与之配对,当一个monitor被获取之后 也就是被monitorenter,它会处于一个锁定状态,其他尝试获取该对象的monitor的线程会获取失败,只有当获取该对象的monitor的线程执行了monitorexit指令后,其他线程才有可能获取该对象的monitor成功。

      所以从上面描述可以得出,监视器锁就是monitor它是互斥的(mutex)。由于它是互斥的,那么它的操作成本就非常的高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了。

     

     

    ==========================================================================           如果您觉得这篇文章对你有帮助,可以【关注我】或者【点赞】,希望我们一起在架构的路上,并肩齐行
    ==========================================================================
  • 相关阅读:
    区别@ControllerAdvice 和@RestControllerAdvice
    Cannot determine embedded database driver class for database type NONE
    使用HttpClient 发送 GET、POST、PUT、Delete请求及文件上传
    Markdown语法笔记
    Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required
    Mysql 查看连接数,状态 最大并发数(赞)
    OncePerRequestFilter的作用
    java连接MySql数据库 zeroDateTimeBehavior
    Intellij IDEA 安装lombok及使用详解
    ps -ef |grep xxx 输出的具体含义
  • 原文地址:https://www.cnblogs.com/amberJava/p/12547485.html
Copyright © 2011-2022 走看看