ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

synchronized底层源码

2021-09-18 17:00:24  阅读:102  来源: 互联网

标签:obj monitor synchronized lock 源码 线程 偏向 底层


深入理解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

标签:obj,monitor,synchronized,lock,源码,线程,偏向,底层
来源: https://www.cnblogs.com/cy0628/p/15309367.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有