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底层源码

观察上面字节码会发现

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

上一篇:L20_8 求解组合数


下一篇:如何知道安装程序在进行安装时对你的电脑到底做了什么?