zoukankan      html  css  js  c++  java
  • Synchronized底层实现

    原链接:https://juejin.cn/post/6874196050446385165

    synchronized,是用来解决并发情况下数据同步问题的关键字,讲人话就是,多线程情况下,被synchronized修饰的代码,在任何时间内都只能有一个线程在执行。下面将要介绍synchronized如何使用,synchronized是如何通过锁定对象来限制单个线程执行,synchronized是如何通过对象头联系上Monitor对象。最后介绍一下JDK1.6之后,引入的偏向锁,轻量级锁,重量级锁的概念。

    synchronized用法

    3种用法

    • 同步实例方法,锁的是当前实例对象
    • 同步类静态方法,锁是当前类对象
    • 同步代码块,锁是括号里面的对象
    public class SynchronizedTest {
    
        /**
         * 同步实例方法,锁实例对象
         */
        public synchronized void test() {
        }
    
        /**
         * 同步类方法,锁类对象
         * 锁住的是SynchronizedTest.class类对象
         */
        public synchronized static void test1() {
        }
    
        /**
         * 同步代码块
         */
        public void test2() {
            String str = "ABC"
            // 锁实例对象
            synchronized (str) {
    
            }
        }
    }
    

      

     

    synchronized原理

    从上面的介绍的用法可以看出,synchronized上锁都是需要锁定类对象或者实例对象。所以先来介绍一下Java对象内存布局

    java对象内存布局

    在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对象填充(Padding)。

    实例数据:对象真正存储的有效信息,存放类的属性数据信息,包括父类的属性信息;

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

    重头戏是接下来的对象头,对象头中包含了与锁关联起来的答案。

    对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。

    对象头

    Class Pointer:是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

    Mark Word : 用于存储对象自身的运行时数据,就是它里面存储了与锁关联起来的关键信息。

    Mark Word

    下图就是Mark Word存储地信息。先把重点放在重量锁一行,指向互斥量(重量级锁)指针一行。其他的后续介绍。重点就是弄明白个互斥量指针是个什么东西?

    preview

    monitor对象

    好了,视线继续挪回到上面提到的synchronized用法。

    使用javap命令反编译之前生成的SynchronizedTest.class

    javap -c SynchronizedTest.class
    复制代码

    反编译后,得到以下字节码

    Compiled from "SynchronizedTest.java"
    public class SynchronizedTest {
      public SynchronizedTest();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public void doSth();
        Code:
           0: ldc           #2                  // class SynchronizedTest
           2: dup
           3: astore_1
           4: monitorenter                      // 重点
           5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
           8: ldc           #4                  // String test Synchronized
          10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          13: aload_1
          14: monitorexit						// 重点
          15: goto          23
          18: astore_2
          19: aload_1
          20: monitorexit					    // 重点
          21: aload_2
          22: athrow
          23: return
        Exception table:
           from    to  target type
               5    15    18   any
              18    21    18   any
    }
    

      

     

    这里的重点是这两行monitorentermonitorexit.

    monitorenter:尝试获取上文提到的Mark Word重量级锁对象。实际上就是Monitor对象。

    monitorexit: monitor的拥有者线程,执行同步代码完毕,计数器减1,如果计数器为0,表明该线程不再拥有monitor。其他线程就允许尝试去获得该monitor了。

    之所以会有两个monitorexit,是因为正常流程执行完后 15 直接goto 23了。 下面的是考虑了异常时的Monitor对象的释放。

    所以重点就是接下的监视器

    监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

    互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。


    结论

    synchronized关键字 通过 锁定 对象 ---> 对象头中的 Mark Work --> Mark Word中指向的Monitor对象 ---> 对应操作系统的一个Mutex Lock(互斥锁)。

    最终实现与操作系统互斥锁关联。现在我们继续了解Monitor对象的的数据结构

    monitor监视器源码是C++写的,在虚拟机的ObjectMonitor.hpp文件中。数据结构长这样

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; // 记录个数
        _waiters      = 0,
        _recursions   = 0; // 锁重入次数
        _object       = NULL;
        _owner        = NULL;  // 拥有持有ObjectMonitor的线程
        _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ;  // 处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }
    

      

     

    顺便解释一下 线程WaitingBlocked状态的区别。synchronized会导致线程进入Blocked状态,Object.wait()导致线程进入Waiting状态,Waiting线程被其他线程调用Object.notify()唤醒之后,重新获取对象上的锁的时候也会进入Blocked状态。也就是说Waiting状态的线程是自己放弃时间片,但是Blocking状态的线程想拥有时间片,但是被没有获取到锁。

    锁优化

    JDK1.6版本对锁进行了优化,引入了偏向锁轻量级锁的概念。下面先介绍一下这两种锁。

    偏向锁

    HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。所以才给起了个偏向锁的名称。 当出现线程竞争时锁才会升级成轻量级锁。

    偏向锁的获取过程

    1. 访问Mark Word(具体位置参考上图)中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
    2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
    3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果指向为空,竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
    4. 如果CAS获取偏向锁失败,则表示有竞争(CAS获取偏向锁失败说明至少有过其他线程曾经获得过偏向锁,因为线程不会主动去释放偏向锁)。
    5. 执行同步代码。

    竞争失败后

    偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断持有锁的线程是否处于活动状态。如果挂了,撤销偏向锁后恢复到无锁(标志位为“01”)然后重新偏向新的线程, 如果还活着,就升级为轻量级锁(标志位为“00”)的状态。

    大概流程图如下

     偏向锁流程图

    偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

    轻量级锁

    当线程竞争情况不严重时,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

    轻量级锁升级

    若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

    获取过程

    1. 判断同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),如果是进入步骤(2),否则执行步骤(5)
    2. 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
    3. 拷贝对象头中的Mark Word复制到锁记录中。
    4. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向锁记录的指针,并将锁记录里的owner指针指向object mark word。如果更新成功,表示竞争到锁,则执行同步代码,否则执行步骤(5)。
    5. 如果当前mark处于加锁状态,且mark中的ptr指针指向当前线程的栈帧,则执行同步代码,否则说明有多个线程竞争轻量级锁,若当前只有一个等待线程,则可通过自旋稍微等待一下,可能另一个线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁。

    流程图

     轻量级锁流程图

    区别

    这里可以看出轻量级锁与偏向锁的区别主要在于这个锁记录(Lock Record),还有轻量级锁允许有轻微的竞争。

    结语

    之前一直不明白,Synchronize为何要锁住一个对象就能够同步代码块,以及无锁 -->偏向锁 --> 轻量级锁 --> 重量级锁之前的转换过程. 后面有时间的话还有 参看以下 ReentrantLock是如何实现锁的。

  • 相关阅读:
    python读取xml文件报错ValueError: multi-byte encodings are not supported
    使用命令创建jenkins的job,解决jenkinsapi.custom_exceptions.JenkinsAPIException错误
    使用Python命令创建jenkins的job
    使用selenium grid分布式执行之一
    使用python 操作liunx的svn,方案二
    使用python 操作liunx的svn,方案一
    使用shell脚本实现在liunx上进行svn的上传下载更新功能
    java连接ssh执行shell脚本
    perl学习(二)正则表达式
    googletest进行单元测试(使用cmake编译)
  • 原文地址:https://www.cnblogs.com/yao5758/p/14460083.html
Copyright © 2011-2022 走看看