天天看点

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

前言

上篇文章已经分析了Java对象头构成、源码及其对象头的调试,本篇将分析偏向锁、轻量级锁、重量级锁的实现及其演变过程。由于涉及到c++源码,估计不少同学没兴趣看,因此重点多以图+源码辅助分析。

通过本篇文章,你将了解到:

1、什么是重量级锁

2、轻量级锁/偏向锁的由来

3、偏向锁的加锁、撤销锁、释放锁

4、轻量级锁的加锁、释放锁

5、偏向锁、轻量级锁、重量级锁的异同点

1、什么是重量级锁

简单例子

private synchronized void testLock() {
        //doSomething();
    }
           

现有两个线程(t1、t2)同时访问testLock()方法,假若t1先拿到锁并执行同步块里的代码。此时t2也要访问testLock()方法,但是因为锁被t1持有,因此t2只能阻塞等待t1释放锁。

此时,锁的形态称为重量锁。

重量级锁为什么"重"

由上面的例子可以看出,t2因为没有获取到锁然后挂起自己,等待t1释放锁后唤醒自己。线程的挂起/唤醒需要CPU切换上下文,此过程代价比较大,因此称此种锁为重量级锁。

线程挂起/唤醒请移步:Java Unsafe/CAS/LockSupport 应用与原理

2、轻量级锁/偏向锁的由来

轻量级锁的由来

还是上面的例子,假设现在t1、t2是交替执行testLock()方法,此时t1、t2没必要阻塞,因为它们之间没有竞争,也就是不需要重量级锁。

线程之间交替执行临界区的情形下使用的锁称为轻量级锁。

轻量级锁相比重量级锁的优势:

1、每次加锁只需要一次CAS

2、不需要分配ObjectMonitor对象

3、线程无需挂起与唤醒

偏向锁的由来

依旧是上面的例子,假设testLock()始终只有一个线程t1在执行呢?这个时候若是使用轻量级锁,每次t1获取锁都需要进行一次CAS,有点浪费性能。

因此就出现了偏向锁:

当锁偏向某个线程时,该线程再次获取锁时无需CAS,只需要一个简单的比较就可以获取锁,这个过程效率很高。

偏向锁相比轻量级锁的优势:

同一个线程多次获取锁时,无需再次进行CAS,只需要简单比较。

3、偏向锁的加锁、撤销锁、释放锁

上面阐述了这三种锁的由来,这些锁是如何实现的呢?接下来从源码的角度进行分析。这三种锁的基础是对象头,关于对象头的详细分析请查看:Java 对象头分析与使用(Synchronized相关)

**锁的本质是共享变量,因此问题的关键是如何访问这个共享变量。**理解这个对于理解三种锁的演变事半功倍,接下来将重点突出这一信息。

既然涉及到了锁,那么自然而然有加锁/释放锁操作,偏向锁比较特殊还多了个撤销锁的操作。

加锁

先来复习对象头:

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

可以看到偏向锁里存储了偏向线程的id,epoch,偏向锁标记(biased_lock),锁标记(lock)等信息。这些信息统称为Mark Word。

在看源码之前,先来朴素(脑补)地猜测线程t1获取偏向锁的过程:

1、先判断Mark Word里的线程id是否有值。

1.1、如果没有,说明还没有线程占用锁,则直接将t1的线程id记录到Mark Word里。可能会存在多个线程同时修改Mark Word,因此需要进行CAS修改Mark Word。

1.2、如果已有id值,那么判断分两种情况:

1.2.1、该id是t1的id,则此次获取锁是个重入的过程,直接就获取了。

1.2.2、如果该id不是t1的id,说明已经有其它线程获取了锁,t1想要获取锁就需要走撤销流程。

来看看源码:bytecodeInterpreter.cpp

CASE(_monitorenter): {
        //获取对象头,用oop表示对象头 -------->(1)
        oop lockee = STACK_OBJECT(-1);
        CHECK_NULL(lockee);
        BasicObjectLock* limit = istate->monitor_base();
        BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
        BasicObjectLock* entry = NULL;
        while (most_recent != limit ) {
          //遍历线程栈,找到对应的空闲的BasicObjectLock (2)
          if (most_recent->obj() == NULL) entry = most_recent;
          else if (most_recent->obj() == lockee) break;
          most_recent++;
        }
        if (entry != NULL) {
          //BasicObjectLock _obj字段指向oop ------>(3)
          entry->set_obj(lockee);
          int success = false;
          uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;

          //取出对象头里的Mark Word
          markOop mark = lockee->mark();
          intptr_t hash = (intptr_t) markOopDesc::no_hash;
          // 支持偏向锁
          if (mark->has_bias_pattern()) {
            uintptr_t thread_ident;
            uintptr_t anticipated_bias_locking_value;
            //当前的线程id
            thread_ident = (uintptr_t)istate->thread();
            //异或运算结果-------->(4)
            anticipated_bias_locking_value =
              (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
              ~((uintptr_t) markOopDesc::age_mask_in_place);

            if  (anticipated_bias_locking_value == 0) {
              //完全相等,则认为是重入了该锁------>(5)
              if (PrintBiasedLockingStatistics) {
                (* BiasedLocking::biased_lock_entry_count_addr())++;
              }
              success = true;
            }
            else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
              //不支持偏向锁了-------->(6)
              //构造无锁的Mark Word
              markOop header = lockee->klass()->prototype_header();
              if (hash != markOopDesc::no_hash) {
                header = header->copy_set_hash(hash);
              }
              //CAS 修改Mark Word为无锁状态
              if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
                if (PrintBiasedLockingStatistics)
                  (*BiasedLocking::revoked_lock_entry_count_addr())++;
              }
            }
            else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
              //epoch 过期了------->(7)
              //使用当前线程id构造偏向锁
              markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
              if (hash != markOopDesc::no_hash) {
                new_header = new_header->copy_set_hash(hash);
              }
              //CAS修改
              if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
                if (PrintBiasedLockingStatistics)
                  //成功则获取了锁
                  (* BiasedLocking::rebiased_lock_entry_count_addr())++;
              }
              else {
                //否则进行下一步
                CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
              }
              success = true;
            }
            else {
              //构造匿名偏向锁---------(8)
              markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |
                                                              (uintptr_t)markOopDesc::age_mask_in_place |
                                                              epoch_mask_in_place));
              if (hash != markOopDesc::no_hash) {
                header = header->copy_set_hash(hash);
              }
              //构造指向当前线程的偏向锁
              markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
              // debugging hint
              DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
              //CAS 修改为偏向当前线程的锁
              if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
                if (PrintBiasedLockingStatistics)
                  (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
              }
              else {
                //不成功则进行下一步
                CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
              }
              success = true;
            }
          }

          if (!success) {
            //上面尝试使用偏向锁,可惜没有成功,则尝试升级为轻量级锁
            markOop displaced = lockee->mark()->set_unlocked();
            entry->lock()->set_displaced_header(displaced);
            bool call_vm = UseHeavyMonitors;
            if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
              // Is it simple recursive case?
              if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
                entry->lock()->set_displaced_header(NULL);
              } else {
                CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
              }
            }
          }
          UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
        } else {
          istate->set_msg(more_monitors);
          UPDATE_PC_AND_RETURN(0); // Re-execute
        }
      }
           

代码看起来很多,重点说明标注的(1)~(9)个点:

(1)

oop 表示对象头,里边包括含了Mark Word、Klass Word。

(2)

在basicLock.hpp里,BasicObjectLock 结构如下:

#basicLock.hpp
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  //BasicLock
  BasicLock _lock;                                  
  //对象头
  oop       _obj;                                   
  ...
};
           

继续看BasicLock:

#basicLock.hpp
class BasicLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  //存储Mark Word
  volatile markOop _displaced_header;
  ...
};
           

BasicObjectLock 即是熟知的Lock Record的实现,其包含了两个内容:

1、存储Mark Word的_displaced_header

2、指向对象头的指针:_obj

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

如图,线程的私有栈里存储着多个Lock Record。

(3)

将Lock Record里的_obj赋值为lockee,也就是_obj表示的是对象头。

(4)

从对象头(lockee)里取出Klass Word,该Word是指向Klass类型的指针,Klass类里有个名为_prototype_header字段, 也是表示Mark Word,里面存储着epoch、偏向锁标记等信息(后面为方便描述,使用Klass替代说明)。此处是取出这些信息并拼接上当前线程id,进而和对象头里的Mark Word进行异或运算,找出不相等的位,接下来就是判断具体Mark Word的哪个部分不相等,从而有不同的处理逻辑。

(5)

如果上面的异或相等,那说明Mark Word里存储有当前线程id,epoch、偏向锁标记都一致,也就是锁被当前线程持有了,此次是个重入的过程。因为已经拥有锁了,所以啥也不干了。

(6)

发现Mark Word里的偏向锁标志位和Klass 里的不同,而Mark Word之前已经判断是偏向锁了,因此可以推断Klass 已经不支持偏向锁了。既然不支持偏向锁了,就修改Mark Word为无锁状态,等待后面升级为轻量级锁/重量级锁。

(7)

发现Mark Word里的epoch与Klass里的不同,则认为发生了批量重偏向,因此可以直接修改Mark Word偏向当前线程。

(8)

如果上述条件都不满足,则认为当前是匿名偏向锁(是偏向锁,但是没有偏向任何线程)。尝试直接修改Mark Word偏向当前线程。

通过上述步骤的分析,结果比较明显了:

1、线程每次尝试获取锁都需要关联Lock Record,并将_obj指向对象头,此时Lock Record与对象头就建立了联系。

2、线程成功将线程id写入Mark Word后即表示该线程获取了该偏向锁

偏向锁状态时Lock Record与对象头关系:

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

此时_displaced_header字段并没有使用。

用图表示偏向锁获取流程:

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

好了,再来回顾一下线程t1、t2获取偏向锁的过程:

1、t1获取锁,一开始锁是匿名偏向锁,所以走的是上图步骤4,成功获取锁。

2、t1再次获取锁,因为之前已经获取到锁了,所以走的是上图步骤1,重入获取锁。

3、此时t2尝试获取锁,因为t1正在持有锁,因此走的是上图步骤5。

1、4、5 步骤情景已经涉及到了,剩下2、3步骤下面分析。

撤销锁

偏向锁获取不成功,那么在升级为轻量级锁之前先将锁变为无锁状态,此为偏向锁的撤销过程。

来看看源码入口:

#InterpreterRuntime.cpp
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  ...
  if (UseBiasedLocking) {
    //使用偏向锁则进入快速处理流程
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    //升级为轻量级锁
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  ...

#synchronizer.cpp
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
      //不在安全点执行
      //可能是撤销,也可能是重偏向
      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

里面代码比较多,就不一一贴出了。

用图表示如下:

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

上面的撤销是在不安全点执行的,因此都会有CAS操作。

上图进行了初步的撤销/重偏向,若是成功则后续会升级为轻量级锁;若是失败,则需要进一步地撤销。

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

批量重偏向/批量撤销逻辑最终也会调用直接撤销函数,继续来看看直接撤销的流程,实际上就是biasedLocking.cpp#revoke_bias 函数:

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

可以看出,上图修改Mark Word时并没有使用CAS,因为执行这段代码是在安全点执行的,也就是说只要执行了就能成功。

批量重偏向与批量撤销

经过上面的分析,我们知道:

1、当某个线程持有偏向锁,另一个线程想要获取锁时需要撤销锁。

2、撤销先尝试在不安全点使用CAS修改Mark Word为无锁状态,若还是无法撤销则考虑在安全点撤销,等安全点是比较低效的操作。

因此偏向锁引入了批量重偏向与批量撤销。

当某个类的对象锁撤销次数达到一定阈值,比如达到了20次,那么就会触发批量重偏向的逻辑,修改Klass里的epoch值,并修改当前正在使用该类型锁Mark Word里的epoch值。当线程想要获取偏向锁时,对比当前对象的epoch值与Klass里的epoch值,发现不相等,则认为过期。此时该线程被允许直接CAS修改Mark Word偏向当前线程,就不用再走撤销逻辑了。这部分对应最初分析偏向锁入口的标记(7)。

同样的当撤销次数达到40次时,认为该对象已经不适合应用偏向锁了,因此会修改Klass里的偏向锁标记,更改为不支持偏向锁。当线程想要获取偏向锁时,检查Klass里的偏向锁标记值,若是不允许偏向,说明之前发生了批量撤销,因此该线程被允许直接CAS修改Mark Word为无锁状态,就不用再走撤销逻辑了。这部分对应最初分析偏向锁入口的标记(6)。

默认的阈值:

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

批量重偏向与批量撤销是对偏向锁性能的优化。

释放锁

正常的想法是:当线程退出临界区,也就是释放了锁。

#bytecodeInterpreter.cpp
      CASE(_monitorexit): {
        //对象头
        oop lockee = STACK_OBJECT(-1);
        CHECK_NULL(lockee);
        BasicObjectLock* limit = istate->monitor_base();
        BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
        //遍历线程栈
        while (most_recent != limit ) {
          //找到对应的Lock Record
          if ((most_recent)->obj() == lockee) {
            BasicLock* lock = most_recent->lock();
            markOop header = lock->displaced_header();
            //设置Lock Record 里的_obj字段 为null
            most_recent->set_obj(NULL);
            //此处是轻量级锁的释放,先省略
            UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
          }
          most_recent++;
        }
        ...
      }
           

偏向锁对象头和Lock Record关系前面已经分析:每次尝试获取偏向锁时,先找到空闲的Lock Record,并将Lock Record里的_obj指向对象头,表示这俩建立起了联系。释放锁的时候将这联系切断,_obj=null。

你也许已经发现了Mark Word并没有发生改变,依然是偏向了之前的线程,那还是没释放锁的嘛。的确是,线程退出临界区时候,并没有释放偏向锁,这么做的目的是:

当再次需要获取锁的时候,只需要简单按位运算判断是否是重入,即可快速获取锁,而不用每次都CAS,这也是偏向锁在只有一个线程访问锁的情景下高效的核心所在。

小结

前边花了很大篇幅阐述偏向锁,看起来很复杂,实际上就是撤销部分比较复杂。

1、偏向锁的"锁"即是Mark Word,想要获取锁就需要对Mark Word进行修改,可能会有多线程竞争修改,因此需要借助CAS。

2、因为撤销操作可能需要在安全点执行,效率比较低,多次撤销更会影响效率,因此引入了批量重偏向与批量撤销。

3、偏向锁的重入计数依靠线程栈里Lock Record个数。

4、偏向锁撤销失败,最终会升级为轻量级锁。

5、偏向锁退出时并没有修改Mark Word,也就是没有释放锁。

4、轻量级锁的加锁、释放锁

加锁

偏向锁的撤销操作比较复杂,而轻量级锁的加锁、释放锁则简单得多。

#synchronizer.cpp
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  //取出Mark Word
  markOop mark = obj->mark();
  //走到此说明已经不是偏向锁了
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

  if (mark->is_neutral()) {
    //如果是无锁状态
    //将Mark Word拷贝到Lock Record的_displaced_header 字段里
    lock->set_displaced_header(mark);
    //CAS修改Mark Word使之指向Lock Record
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
  } else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    //mark->has_locker() -->表示已经是轻量级锁
    //THREAD->is_lock_owned((address)mark->locker()) 并且是当前线程获取了轻量级锁
    //这两点说明当前线程重入该锁
    //直接设置header==null
    lock->set_displaced_header(NULL);
    return;
  }
  //走到这说明不能使用轻量级锁,则需要升级为重量级锁
           

此时,我们发现Lock Record与轻量级锁的关系更加紧密。

Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程前言1、什么是重量级锁2、轻量级锁/偏向锁的由来3、偏向锁的加锁、撤销锁、释放锁4、轻量级锁的加锁、释放锁5、偏向锁、轻量级锁、重量级锁的异同点您若喜欢,请点赞、关注,您的鼓励是我前进的动力持续更新中,和我一起步步为营系统、深入学习Java/Android

偏向锁了没用的_displaced_header用上了,用以存储无锁状态的Mark Word,待释放锁时恢复(保留了hash值等)。

而Mark Word里的锁记录指针指向了Lock Record,表示该Lock Record所在的线程获取了轻量级锁。

释放锁

#bytecodeInterpreter.cpp
      CASE(_monitorexit): {
        oop lockee = STACK_OBJECT(-1);
        CHECK_NULL(lockee);
        BasicObjectLock* limit = istate->monitor_base();
        BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
        while (most_recent != limit ) {
          if ((most_recent)->obj() == lockee) {
            BasicLock* lock = most_recent->lock();
            markOop header = lock->displaced_header();
            most_recent->set_obj(NULL);
            //以上部分和偏向锁释放一致的

            if (!lockee->mark()->has_bias_pattern()) {
              //不是偏向模式
              bool call_vm = UseHeavyMonitors;
              //header 不为空,说明是线程第一次获取轻量级锁时占用的Lock Record
              if (header != NULL || call_vm) {
                //而header存储的是无锁状态的Mark Word
                //因此需要将Mark Word修改恢复为之前的无锁状态
                if (call_vm || Atomic::cmpxchg_ptr(header, lockee->mark_addr(), lock) != lock) {
                  //失败的话,再将obj设置上,为了重量级锁使用
                  most_recent->set_obj(lockee);
                  CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
                }
              }
            }
            UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
          }
          most_recent++;
        }
        ...
      }
           

与偏向锁不同的是,轻量级锁是真的释放了锁,因为修改Mark Word为无锁状态了。

你可能会有疑惑:只有拿到锁的线程才会有释放锁的操作,为什么此处还需要CAS呢?

考虑一种情况:线程A获取了轻量级锁,此时线程B也想要获取锁,由于锁被A占用,因此B将锁膨胀为重量级锁(修改了Mark Word)。而此时A还在执行临界区代码,它并不知道Mark Word已经被更改了。所以当A退出临界区释放锁的时候,它不能直接修改Mark Word,于是使用CAS尝试更新Mark Word。若是Mark Word已经改变了,也就是说之前Mark Word是指向线程A的Lock Reocrd指针,现在是指向ObjectMonitor了,当然A的CAS会失败,接着进行下一步判断,最终可能膨胀为重量级锁。若是没有改变,A释放轻量级锁就直接成功了。

小结

1、轻量级锁的"锁"即是Mark Word,想要获取锁就需要对Mark Word进行修改,可能会有多线程竞争修改,因此需要借助CAS。

2、如果初始锁为无锁状态,则每次进入都需要一次CAS尝试修改为轻量级锁,否则判断是否重入。

3、如果不满足2的条件,则膨胀为重量级锁。

4、轻量级锁退出时即释放锁,变为无锁状态。

5、可以看出轻量级锁比较敏感,一旦有线程竞争就会膨胀为重量级锁。

5、偏向锁、轻量级锁、重量级锁的异同点

由上面的分析可知,想要在不安全点获取锁,就得依靠CAS操作,因此理解CAS的原理是深入锁的基础。有关CAS原理与使用请移步:Java Unsafe/CAS/LockSupport 应用与原理

三者的共同点:

1、都需要和Lock Record关联;偏向锁和重量级锁只用到了_obj字段,而轻量级锁用到了_displaced_header。

2、释放锁时都需要修改Lock Record 里的_obj字段。

三者不同点:

1、偏向锁和轻量级锁的"锁"即是Mark Word,而重量级锁的"锁"是ObjectMonitor,此时Mark Word保留了指针指向ObjectMonitor。

2、偏向锁和轻量级锁依靠Lock Record个数来记录重入的次数,而重量级锁通过

ObjectMonitor里的_recursions 整形变量记录。

3、偏向锁和轻量级锁的重入只需要做简单的判断即可,而重量级锁需要通过CAS判断是否是重入。

三者适用场景

1、偏向锁适合在只有一个线程访问锁的场景,在此种场景下,线程只需要执行一次CAS获取偏向锁,后续该线程再次访问该锁时仅仅只需要简单的判断即可获取锁。

2、轻量级锁适合在有多个线程交替访问锁,并且不会发生竞争的场景。此种场景下,线程每次获取锁只需要执行一次CAS即可。

3、重量级锁适合在多线程竞争环境下访问锁,执行临界区的时间比较长,未获取锁的线程将会被挂起,等待拥有锁的线程释放锁而后唤醒它。此种场景下,线程每次都需要进行多次CAS操作,操作失败将会被放入队列里等待唤醒。

值得注意的是:

偏向锁、轻量级锁是在Java1.6(Java 6)提出的用以对重量级锁的改进。

从上面分析我们也知道为了实现偏向锁的撤销,引入了复杂的同步代码,包括在安全点执行等操作,且对 HotSpot 的其他组件产生了影响。这种复杂性已经成为理解代码的障碍,也阻碍了对同步系统进行重构。**因此,在Java 15废弃了偏向锁。**https://openjdk.java.net/jeps/374。

至此,偏向锁、轻量级锁的原理已经阐述完毕,由于篇幅所限,重量级锁的原理下篇分析。

虽然尽量避免贴过多的代码,但还是无法避免贴了一些,不关注源码的同学请直接看每段的小结。若是对更多的源码细节感兴趣,可查看下面的链接,本篇也参考了以下链接:

https://github.com/farmerjohngit/myblog/issues/13

https://github.com/HenryChenV/my-notes/issues/3

本文源码基于jdk1.8。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Java/Android