zoukankan      html  css  js  c++  java
  • synchronized底层源码

    深入理解synchronized底层源码

    前言

    这篇文章从JVM源码分析synchronized的实现逻辑,这样才能更加对synchronized深度的认识。

    进程:操作系统资源分配的基本单位。线程:cpu调度的基本单位(真实执行)

    一、synchronized的使用场景

    synchronized一般使用在下面这几种场景:

    1. 修饰代码块,指定一个加锁的对象,给对象加锁

    public Demo1{
       Object lock=new Object();
       public void test1(){
           synchronized(lock){
           }
       }
    }

      2.修饰静态方法,对当前类的Class对象加锁

    public class Demo2 {
       //形式一
        public void test1(){
            synchronized(Synchronized.class){
            }
        }
      //形式二
        public void test2(){
            public synchronized static void test1(){
            }
        }
    }

      3.修饰普通方法,对当前实例对象this加锁

    public class Demo3 {
        public synchronized void test1(){
        }
    }

    二、JVM中,对象在内存中的布局
    synchronized实现的锁是存储在Java对象头。所以要对synchronized深入理解,首先了解一下对象在内存中的布局怎样的?

    在 JVM 中,对象在内存中分为这么三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

    1、对象头(Header)两个部分组成:markOop或称为Mark Word和类元信息

    Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。源码如下:

    class markOopDesc: public oopDesc {
     private:
      // Conversion
      uintptr_t value() const { return (uintptr_t) this; }
     public:
      // Constants
      enum { age_bits                 = 4,  //分代年龄
             lock_bits                = 2, //锁标识
             biased_lock_bits         = 1, //是否为偏向锁
             max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
             hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits, //对象的hashcode
             cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
             epoch_bits               = 2 //偏向锁的时间戳
      };

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

    2、实例数据:这部分主要是存放类的数据信息,父类的信息

    3、对齐填充

    三、synchronized底层从字节码聊起
    首先,来看使用synchronized修饰的方法及方法块的小Demo案例,然后进行分析。由于synchronized的实现是在JVM层面的,我们要深入理解,那就是从字节码聊起了。

    public class Demo03 {
        public static void main( String[] args ){
            System.out.println( "hello Java" );
        }
        //synchronized修饰普通方法
        public synchronized void test1() { }
    
        //修饰代码块
        public void test2() {
            synchronized (this) {}
        }
    }

    接下打开终端运行javac Demo03.java意思是将Demo03.java变成Demo03.class,然后再javap -v Demo03.class这样可以查看字节码了如下。

    观察上面字节码会发现

    synchronized同步方法时:

    如果修饰同步方法是通过的flag ACC_SYNCHRONIZED来完成的,也就是说一旦执行到这个方法,就会先判断是否有标志位,然后ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。
    

    synchronized修饰同步代码块时:

    首先如果被synchronized修饰在方法块的话,是通过 monitorenter 和 monitorexit 这两个字节码指令获取线程的执行权的。当方法执行完毕退出以后或者出现异常的情况下会自动释放锁。

    以上不管修饰哪一种:不管哪一种本质是对一个对象监视器(monitor)进行获取

    在Java虚拟机执行到monitorenter指令时,1⃣️首先它会尝试获取对象的锁,如果该对象没有锁,或者当前线程已经拥有了这个对象的锁时,它会把计数器+1;然后当执行到monitorexit 指令时就会将计数器-1;然后当计数器为0时,锁就释放了。2⃣️如果获取锁 失败,那么当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

    四、monitor到底是什么?
    monitor它就是个监视器,底层源码是C++编写的。在hotspot虚拟机中,它是采用ObjectMonitor类来实现monitor的源码如下:

    bool has_monitor() const {
        return ((value() & monitor_value) != 0);
      }
      ObjectMonitor* monitor() const {
        assert(has_monitor(), "check");
        // Use xor instead of &~ to provide one extra tag-bit check.
        return (ObjectMonitor*) (value() ^ monitor_value);
      }

    monitor实现在虚拟机的ObjectMonitor.hpp文件中的如下:

     class ObjectMonitor {
    ...
      ObjectMonitor() {
        _header       = NULL; //markOop对象头
        _count        = 0;    
        _waiters      = 0,   //等待线程数
        _recursions   = 0;   //线程重入次数
        _object       = NULL;  //存储Monitor对象
        _owner        = NULL;  //获得ObjectMonitor对象的线程
        _WaitSet      = NULL;  //wait状态的线程列表
        _WaitSetLock  = 0 ; 
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;    // 单向列表
        FreeNext      = NULL ;
        _EntryList    = NULL ; //处于等待锁BLOCKED状态的线程
        _SpinFreq     = 0 ;   
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ; 
        _previous_owner_tid = 0; //监视器前一个拥有线程的ID
      }
    ...

    五、深入synchronized底层源码
    从 monitorenter和 monitorexit这两个指令来开始阅读源码,JVM将字节码加载到内存以后,会对这两个指令进行解释执行, monitorenter, monitorexit的指令解析是通过 InterpreterRuntime.cpp中的两个方法实现。

    IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
    #ifdef ASSERT
      thread->last_frame().interpreter_frame_verify_monitor(elem);
    #endif
      if (PrintBiasedLockingStatistics) {
        Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
      }
      Handle h_obj(thread, elem->obj());
      assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
             "must be NULL or an object");
      if (UseBiasedLocking) {
        // Retry fast entry if bias is revoked to avoid unnecessary inflation
        ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
      } else {//绕过偏向锁,直接进入轻量级锁
        ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
      }
      assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
             "must be NULL or an object");
    #ifdef ASSERT
      thread->last_frame().interpreter_frame_verify_monitor(elem);
    #endif
    IRT_END
    
    
    //%note monitor_1
    IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
    #ifdef ASSERT
      thread->last_frame().interpreter_frame_verify_monitor(elem);
    #endif
      Handle h_obj(thread, elem->obj());
      assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
             "must be NULL or an object");
      if (elem == NULL || h_obj()->is_unlocked()) {
        THROW(vmSymbols::java_lang_IllegalMonitorStateException());
      }
      ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
      // Free entry. This must be done here, since a pending exception might be installed on
      // exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
      elem->set_obj(NULL);
    #ifdef ASSERT
      thread->last_frame().interpreter_frame_verify_monitor(elem);
    #endif
    IRT_END

    注:

    //JavaThread 当前获取锁的线程
    //BasicObjectLock 基础对象锁

    UseBiasedLocking是在JVM启动的时候,是否启动偏向锁的标识
    1、如果支持偏向锁,则执行 ObjectSynchronizer::fast_enter的逻辑

    ObjectSynchronizer::fast_enter实现在 synchronizer.cpp文件中,代码如下

    void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
     if (UseBiasedLocking) {//判断是否开启锁
        if (!SafepointSynchronize::is_at_safepoint()) {//如果不处于全局安全点
          //通过`revoke_and_rebias`这个函数尝试获取偏向锁
          BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
          if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {//如果是撤销与重偏向直接返回
            return;
          }
        } else {//如果在安全点,撤销偏向锁
          assert(!attempt_rebias, "can not rebias toward VM thread");
          BiasedLocking::revoke_at_safepoint(obj);
        }
        assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
     }
    
     slow_enter (obj, lock, THREAD) ;
    }

    小结:

      再次检查偏向锁是否开启

      当处于不安全点时,通过 revoke_and_rebias尝试获取偏向锁,如果成功则直接返回,如果失败则进入轻量级锁获取过程

      revoke_and_rebias这个偏向锁的获取逻辑在 biasedLocking.cpp中

      如果偏向锁未开启,则进入 slow_enter获取轻量级锁的流程

    2、如果不支持偏向锁,则执行 ObjectSynchronizer::slow_enter逻辑,绕过偏向锁,直接进入轻量级锁,该方法同样位于 synchronizer.cpp文件中

    void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
      markOop mark = obj->mark();
      assert(!mark->has_bias_pattern(), "should not see bias pattern here");
    
      if (mark->is_neutral()) {//如果当前是无锁状态, markword的
        //直接把mark保存到BasicLock对象的_displaced_header字段
        lock->set_displaced_header(mark);
        //通过CAS将mark word更新为指向BasicLock对象的指针,更新成功表示获得了轻量级锁
        if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
          TEVENT (slow_enter: release stacklock) ;
          return ;
        }
        // Fall through to inflate() ...
      } 
      //如果markword处于加锁状态、且markword中的ptr指针指向当前线程的栈帧,表示为重入操作,不需要争抢锁
      else
      if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
        assert(lock != mark->locker(), "must not re-lock the same lock");
        assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
        lock->set_displaced_header(NULL);
        return;
      }
    
    #if 0
      // The following optimization isn't particularly useful.
      if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
        lock->set_displaced_header (NULL) ;
        return ;
      }
    #endif
        //代码执行到这里,说明有多个线程竞争轻量级锁,轻量级锁通过`inflate`进行膨胀升级为重量级锁
      lock->set_displaced_header(markOopDesc::unused_mark());
      ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
    }

    六、锁的优化
    大家都知道JDK1.6之前,synchronized是一个重量级锁,效率比较低。所以官方在JDK1.6开始,为了减少获得锁和释放锁带来的性能消耗,对synchronized进行了优化,引入了 偏向锁和 轻量级锁的概念。

    无锁状态--->偏向锁状态--->轻量级锁状态--->重量级锁状态四种状态,锁的状态会随着锁竞争的情况逐步升级,且锁升级是不可逆的。

    1、偏向锁
    偏向锁是指如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入同步,不需要再次进行抢占锁和释放锁的操作。

    该过程是采用CAS客观锁操作的,意思就是一个线程获取偏向锁时,如果没有其他线程来竞争锁,那么持有偏向锁的线程再次进入,虚拟机就不进行任何同步操作了,对标志位加1即可;如果不同线程来竞争锁,CAS会失败,这样获取锁就失败了

    JDK 1.5偏向锁是关闭的,开启参数xx:-UseBiasedLocking=false,JDK1.6后默认开启。

    2、偏向锁的获取
    当一个线程访问同步块获取锁时,会在对象头(Mark Word)和栈帧中的锁记录里存储偏向锁的线程ID,表示哪个线程获得了偏向锁。

    获取过程:

    1)首先根据锁的标志判断是不是处于偏向锁的状态

    2)如果是偏向锁状态,就通过CAS操作将自己的线程ID写入到MarkWord,如果CAS操作成功,说明当前线程获取到偏向锁,然后就继续执行同步代码块。如果CAS失败,那就是意味着获取锁失败。

    3)如果当前不是偏向锁,那它会去检测MarkWord中存储的线程ID和当前访问的线程的线程ID是否相等,如果相等,就说明当前线程已经是获取偏向锁,然后直接执行同步代码;如果不相等,说明当前偏向锁被其他线程获取,需要撤销偏向锁。

    3、撤销偏向锁
    获取偏向锁的线程才会释放偏向锁,撤销偏向锁的过程需要等待一个全局安全点(也就是等待获取偏向锁的线程都停止字节码执行)。

    撤销偏向锁的过程:

    1)首先,判断获取偏向锁的线程否为存活状态

    2)如果线程已存亡,那就直接把Mark Word设置为无锁状态

    3)如果线程还存活,当达到全局安全点时,获取的偏向锁的线程会被挂起,然后接着偏向锁升级为轻量级锁,最后唤醒被阻塞在全局安全点的线程继续往下执行同步代码

    4、轻量级锁
    当多个线程竞争偏向锁时,会发生偏向锁的撤销,偏向锁撤销无非两种状态:

    1)没有获取偏向锁的无锁状态

    2)没有获取偏向锁的有锁状态

    5、轻量级锁加锁过程
    1)如果这个对象是无锁的,JVM就会在当前线程的栈帧创建用于存储锁记录的空间(LockRecord),用来将对象头中的Mark Word复制到锁记录中的

    2)然后JVM采用CAS将对象头中的Mark Word替换为指向锁记录的指针

    3)替换成功,说明当前线程获得轻量级锁;替换失败,说明存在其他线程竞争锁。那么当前线程会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义)时仍然无法获得锁,此时锁会膨胀升级为重量级锁

    自旋,防止线程被挂起,一旦可以获取资源,就直接尝试成功,如果超出阈值,还没有获取锁,那么升级为重量级锁。(自旋锁默认是10次,-XX:PreBlockSpin可以修改)

    6、重量级锁状态
    一旦锁升级为重量级锁,就不会再恢复到轻量级锁状态。当锁处于重量级锁状态,其他线程尝试获取锁时,都会被阻塞,也就是 BLOCKED状态。当持有锁的线程释放锁之后会唤醒这些现场,被唤醒之后的线程

    ref:https://blog.csdn.net/realize_dream/article/details/106968443

  • 相关阅读:
    进阶 | 手把手教你模拟键盘和鼠标操作ActionChains
    做web自动化时,定位元素常用方法有哪些?
    C# 自定义控件无法查看视图设计:文件中的类都不能进行设计,因此未能为该文件显示设计器
    Windows Server时间服务器配置方法
    MySQL 8.0主从(MasterSlave)配置
    VMware VSphere Client克隆虚拟机
    VMware vSphere Client给虚拟机增加硬盘
    nginx负载+mysql8.0双主实现
    Java
    关于awvs和nessus的api的说明
  • 原文地址:https://www.cnblogs.com/cy0628/p/15309367.html
Copyright © 2011-2022 走看看