farmerjohngit/myblog

死磕Synchronized底层实现--偏向锁

farmerjohngit opened this issue · 78 comments

本文为synchronized系列第二篇。主要内容为分析偏向锁的实现。

偏向锁的诞生背景和基本原理在上文中已经讲过了,强烈建议在有看过上篇文章的基础下阅读本文

更多文章见个人博客:https://github.com/farmerjohngit/myblog

本系列文章将对HotSpot的synchronized锁实现进行全面分析,内容包括偏向锁、轻量级锁、重量级锁的加锁、解锁、锁升级流程的原理及源码分析,希望给在研究synchronized路上的同学一些帮助。主要包括以下几篇文章:

死磕Synchronized底层实现--概论

死磕Synchronized底层实现--偏向锁

死磕Synchronized底层实现--轻量级锁

死磕Synchronized底层实现--重量级锁

本文将分为几块内容:

1.偏向锁的入口

2.偏向锁的获取流程

3.偏向锁的撤销流程

4.偏向锁的释放流程

5.偏向锁的批量重偏向和批量撤销

本文分析的JVM版本是JVM8,具体版本号以及代码可以在这里看到。

偏向锁入口

目前网上的很多文章,关于偏向锁源码入口都找错地方了,导致我之前对于偏向锁的很多逻辑一直想不通,走了很多弯路。

synchronized分为synchronized代码块和synchronized方法,其底层获取锁的逻辑都是一样的,本文讲解的是synchronized代码块的实现。上篇文章也说过,synchronized代码块是由monitorentermonitorexit两个指令实现的。

关于HotSpot虚拟机中获取锁的入口,网上很多文章要么给出的方法入口为interpreterRuntime.cpp#monitorenter,要么给出的入口为bytecodeInterpreter.cpp#1816。包括占小狼的这篇文章关于锁入口的位置说法也是有问题的(当然文章还是很好的,在我刚开始研究synchronized的时候,小狼哥的这篇文章给了我很多帮助)。

要找锁的入口,肯定是要在源码中找到对monitorenter指令解析的地方。在HotSpot的中有两处地方对monitorenter指令进行解析:一个是在bytecodeInterpreter.cpp#1816 ,另一个是在templateTable_x86_64.cpp#3667

前者是JVM中的字节码解释器(bytecodeInterpreter),用C++实现了每条JVM指令(如monitorenterinvokevirtual等),其优点是实现相对简单且容易理解,缺点是执行慢。后者是模板解释器(templateInterpreter),其对每个指令都写了一段对应的汇编代码,启动时将每个指令与对应汇编代码入口绑定,可以说是效率做到了极致。模板解释器的实现可以看这篇文章在研究的过程中也请教过文章作者‘汪先生’一些问题,这里感谢一下。

在HotSpot中,只用到了模板解释器,字节码解释器根本就没用到,R大的读书笔记中说的很清楚了,大家可以看看,这里不再赘述。

所以montorenter的解析入口在模板解释器中,其代码位于templateTable_x86_64.cpp#3667。通过调用路径:templateTable_x86_64#monitorenter->interp_masm_x86_64#lock_object进入到偏向锁入口macroAssembler_x86#biased_locking_enter,在这里大家可以看到会生成对应的汇编代码。需要注意的是,不是说每次解析monitorenter指令都会调用biased_locking_enter,而是只会在JVM启动的时候调用该方法生成汇编代码,之后对指令的解析是通过直接执行汇编代码。

其实bytecodeInterpreter的逻辑和templateInterpreter的逻辑是大同小异的,因为templateInterpreter中都是汇编代码,比较晦涩,所以看bytecodeInterpreter的实现会便于理解一点。但这里有个坑,在jdk8u之前,bytecodeInterpreter并没有实现偏向锁的逻辑。我之前看的JDK8-87ee5ee27509这个版本就没有实现偏向锁的逻辑,导致我看了很久都没看懂。在这个commit中对bytecodeInterpreter加入了偏向锁的支持,我大致了看了下和templateInterpreter对比除了栈结构不同外,其他逻辑大致相同,所以下文就按bytecodeInterpreter中的代码对偏向锁逻辑进行讲解templateInterpreter的汇编代码讲解可以看这篇文章,其实汇编源码中都有英文注释,了解了汇编几个基本指令的作用再结合注释理解起来也不是很难。

偏向锁获取流程

下面开始偏向锁获取流程分析,代码在bytecodeInterpreter.cpp#1816注意本文代码都有所删减

CASE(_monitorenter): {
  // lockee 就是锁对象
  oop lockee = STACK_OBJECT(-1);
  // derefing's lockee ought to provoke implicit null check
  CHECK_NULL(lockee);
  // code 1:找到一个空闲的Lock Record
  BasicObjectLock* limit = istate->monitor_base();
  BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
  BasicObjectLock* entry = NULL;
  while (most_recent != limit ) {
    if (most_recent->obj() == NULL) entry = most_recent;
    else if (most_recent->obj() == lockee) break;
    most_recent++;
  }
  //entry不为null,代表还有空闲的Lock Record
  if (entry != NULL) {
    // code 2:将Lock Record的obj指针指向锁对象
    entry->set_obj(lockee);
    int success = false;
    uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
	// markoop即对象头的mark word
    markOop mark = lockee->mark();
    intptr_t hash = (intptr_t) markOopDesc::no_hash;
    // code 3:如果锁对象的mark word的状态是偏向模式
    if (mark->has_bias_pattern()) {
      uintptr_t thread_ident;
      uintptr_t anticipated_bias_locking_value;
      thread_ident = (uintptr_t)istate->thread();
     // code 4:这里有几步操作,下文分析
      anticipated_bias_locking_value =
        (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
        ~((uintptr_t) markOopDesc::age_mask_in_place);
	 // code 5:如果偏向的线程是自己且epoch等于class的epoch
      if  (anticipated_bias_locking_value == 0) {
        // already biased towards this thread, nothing to do
        if (PrintBiasedLockingStatistics) {
          (* BiasedLocking::biased_lock_entry_count_addr())++;
        }
        success = true;
      }
       // code 6:如果偏向模式关闭,则尝试撤销偏向锁
      else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
        markOop header = lockee->klass()->prototype_header();
        if (hash != markOopDesc::no_hash) {
          header = header->copy_set_hash(hash);
        }
        // 利用CAS操作将mark word替换为class中的mark word
        if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
          if (PrintBiasedLockingStatistics)
            (*BiasedLocking::revoked_lock_entry_count_addr())++;
        }
      }
         // code 7:如果epoch不等于class中的epoch,则尝试重偏向
      else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
        // 构造一个偏向当前线程的mark word
        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替换对象头的mark word  
        if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
          if (PrintBiasedLockingStatistics)
            (* BiasedLocking::rebiased_lock_entry_count_addr())++;
        }
        else {
          // 重偏向失败,代表存在多线程竞争,则调用monitorenter方法进行锁升级
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
        success = true;
      }
      else {
         // 走到这里说明当前要么偏向别的线程,要么是匿名偏向(即没有偏向任何线程)
       	// code 8:下面构建一个匿名偏向的mark word,尝试用CAS指令替换掉锁对象的mark word
        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);)
        if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
           // CAS修改成功
          if (PrintBiasedLockingStatistics)
            (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
        }
        else {
          // 如果修改失败说明存在多线程竞争,所以进入monitorenter方法
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
        success = true;
      }
    }

    // 如果偏向线程不是当前线程或没有开启偏向模式等原因都会导致success==false
    if (!success) {
      // 轻量级锁的逻辑
      //code 9: 构造一个无锁状态的Displaced Mark Word,并将Lock Record的lock指向它
      markOop displaced = lockee->mark()->set_unlocked();
      entry->lock()->set_displaced_header(displaced);
      //如果指定了-XX:+UseHeavyMonitors,则call_vm=true,代表禁用偏向锁和轻量级锁
      bool call_vm = UseHeavyMonitors;
      // 利用CAS将对象头的mark word替换为指向Lock Record的指针
      if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
        // 判断是不是锁重入
        if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {		//code 10: 如果是锁重入,则直接将Displaced Mark Word设置为null
          entry->lock()->set_displaced_header(NULL);
        } else {
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
      }
    }
    UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
  } else {
    // lock record不够,重新执行
    istate->set_msg(more_monitors);
    UPDATE_PC_AND_RETURN(0); // Re-execute
  }
}

再回顾下对象头中mark word的格式:image

JVM中的每个类也有一个类似mark word的prototype_header,用来标记该class的epoch和偏向开关等信息。上面的代码中lockee->klass()->prototype_header()即获取class的prototype_header。

code 1,从当前线程的栈中找到一个空闲的Lock Record即代码中的BasicObjectLock,下文都用Lock Record代指),判断Lock Record是否空闲的依据是其obj字段 是否为null。注意这里是按内存地址从低往高找到最后一个可用的Lock Record,换而言之,就是找到内存地址最高的可用Lock Record

code 2,获取到Lock Record后,首先要做的就是为其obj字段赋值。

code 3,判断锁对象的mark word是否是偏向模式,即低3位是否为101。

code 4,这里有几步位运算的操作 anticipated_bias_locking_value = (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) & ​ ~((uintptr_t) markOopDesc::age_mask_in_place); 这个位运算可以分为3个部分。

第一部分((uintptr_t)lockee->klass()->prototype_header() | thread_ident) 将当前线程id和类的prototype_header相或,这样得到的值为(当前线程id + prototype_header中的(epoch + 分代年龄 + 偏向锁标志 + 锁标志位)),注意prototype_header的分代年龄那4个字节为0

第二部分 ^ (uintptr_t)mark 将上面计算得到的结果与锁对象的markOop进行异或,相等的位全部被置为0,只剩下不相等的位。

第三部分 & ~((uintptr_t) markOopDesc::age_mask_in_place) markOopDesc::age_mask_in_place为...0001111000,取反后,变成了...1110000111,除了分代年龄那4位,其他位全为1;将取反后的结果再与上面的结果相与,将上面异或得到的结果中分代年龄给忽略掉。

code 5anticipated_bias_locking_value==0代表偏向的线程是当前线程且mark word的epoch等于class的epoch,这种情况下什么都不用做。

code 6(anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0代表class的prototype_header或对象的mark word中偏向模式是关闭的,又因为能走到这已经通过了mark->has_bias_pattern()判断,即对象的mark word中偏向模式是开启的,那也就是说class的prototype_header不是偏向模式。

然后利用CAS指令Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark撤销偏向锁,我们知道CAS会有几个参数,1是预期的原值,2是预期修改后的值 ,3是要修改的对象,与之对应,cmpxchg_ptr方法第一个参数是预期修改后的值,第2个参数是修改的对象,第3个参数是预期原值,方法返回实际原值,如果等于预期原值则说明修改成功。

code 7,如果epoch已过期,则需要重偏向,利用CAS指令将锁对象的mark word替换为一个偏向当前线程且epoch为类的epoch的新的mark word

code 8,CAS将偏向线程改为当前线程,如果当前是匿名偏向则能修改成功,否则进入锁升级的逻辑。

code 9,这一步已经是轻量级锁的逻辑了。从上图的mark word的格式可以看到,轻量级锁中mark word存的是指向Lock Record的指针。这里构造一个无锁状态的mark word,然后存储到Lock RecordLock Record的格式可以看第一篇文章)。设置mark word是无锁状态的原因是:轻量级锁解锁时是将对象头的mark word设置为Lock Record中的Displaced Mark Word,所以创建时设置为无锁状态,解锁时直接用CAS替换就好了。

code 10, 如果是锁重入,则将Lock RecordDisplaced Mark Word设置为null,起到一个锁重入计数的作用。

以上是偏向锁加锁的流程(包括部分轻量级锁的加锁流程),如果当前锁已偏向其他线程||epoch值过期||偏向模式关闭||获取偏向锁的过程中存在并发冲突,都会进入到InterpreterRuntime::monitorenter方法, 在该方法中会对偏向锁撤销和升级。

偏向锁的撤销

这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块时的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别

如果获取偏向锁失败会进入到InterpreterRuntime::monitorenter方法

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  ...
  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);
  }
  ...
IRT_END

可以看到如果开启了JVM偏向锁,那会进入到ObjectSynchronizer::fast_enter方法中。

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) ;
}

如果是正常的Java线程,会走上面的逻辑进入到BiasedLocking::revoke_and_rebias方法,如果是VM线程则会走到下面的BiasedLocking::revoke_at_safepoint。我们主要看BiasedLocking::revoke_and_rebias方法。这个方法的主要作用像它的方法名:撤销或者重偏向,第一个参数封装了锁对象和当前线程,第二个参数代表是否允许重偏向,这里是true。

BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
  assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");
    
  markOop mark = obj->mark();
  if (mark->is_biased_anonymously() && !attempt_rebias) {
     //如果是匿名偏向且attempt_rebias==false会走到这里,如锁对象的hashcode方法被调用会出现这种情况,需要撤销偏向锁。
    markOop biased_value       = mark;
    markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
    markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
    if (res_mark == biased_value) {
      return BIAS_REVOKED;
    }
  } else if (mark->has_bias_pattern()) {
    // 锁对象开启了偏向模式会走到这里
    Klass* k = obj->klass();
    markOop prototype_header = k->prototype_header();
    //code 1: 如果对应class关闭了偏向模式
    if (!prototype_header->has_bias_pattern()) {
      markOop biased_value       = mark;
      markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
      assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked");
      return BIAS_REVOKED;
    //code2: 如果epoch过期
    } else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
      if (attempt_rebias) {
        assert(THREAD->is_Java_thread(), "");
        markOop biased_value       = mark;
        markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
        markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED_AND_REBIASED;
        }
      } else {
        markOop biased_value       = mark;
        markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
        markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED;
        }
      }
    }
  }
  //code 3:批量重偏向与批量撤销的逻辑
  HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
  if (heuristics == HR_NOT_BIASED) {
    return NOT_BIASED;
  } else if (heuristics == HR_SINGLE_REVOKE) {
    //code 4:撤销单个线程
    Klass *k = obj->klass();
    markOop prototype_header = k->prototype_header();
    if (mark->biased_locker() == THREAD &&
        prototype_header->bias_epoch() == mark->bias_epoch()) {
      // 走到这里说明需要撤销的是偏向当前线程的锁,当调用Object#hashcode方法时会走到这一步
      // 因为只要遍历当前线程的栈就好了,所以不需要等到safepoint再撤销。
      ResourceMark rm;
      if (TraceBiasedLocking) {
        tty->print_cr("Revoking bias by walking my own stack:");
      }
      BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD);
      ((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
      assert(cond == BIAS_REVOKED, "why not?");
      return cond;
    } else {
      // 下面代码最终会在VM线程中的safepoint调用revoke_bias方法
      VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
      VMThread::execute(&revoke);
      return revoke.status_code();
    }
  }
	
  assert((heuristics == HR_BULK_REVOKE) ||
         (heuristics == HR_BULK_REBIAS), "?");
   //code5:批量撤销、批量重偏向的逻辑
  VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
                                (heuristics == HR_BULK_REBIAS),
                                attempt_rebias);
  VMThread::execute(&bulk_revoke);
  return bulk_revoke.status_code();
}

会走到该方法的逻辑有很多,我们只分析最常见的情况:假设锁已经偏向线程A,这时B线程尝试获得锁。

上面的code 1code 2B线程都不会走到,最终会走到code 4处,如果要撤销的锁偏向的是当前线程则直接调用revoke_bias撤销偏向锁,否则会将该操作push到VM Thread中等到safepoint的时候再执行。

关于VM Thread这里介绍下:在JVM中有个专门的VM Thread,该线程会源源不断的从VMOperationQueue中取出请求,比如GC请求。对于需要safepoint的操作(VM_Operationevaluate_at_safepoint返回true)必须要等到所有的Java线程进入到safepoint才开始执行。 关于safepoint可以参考下这篇文章

接下来我们着重分析下revoke_bias方法。第一个参数为锁对象,第2、3个参数为都为false

static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread) {
  markOop mark = obj->mark();
  // 如果没有开启偏向模式,则直接返回NOT_BIASED
  if (!mark->has_bias_pattern()) {
    ...
    return BiasedLocking::NOT_BIASED;
  }

  uint age = mark->age();
  // 构建两个mark word,一个是匿名偏向模式(101),一个是无锁模式(001)
  markOop   biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
  markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);

  ...

  JavaThread* biased_thread = mark->biased_locker();
  if (biased_thread == NULL) {
     // 匿名偏向。当调用锁对象的hashcode()方法可能会导致走到这个逻辑
     // 如果不允许重偏向,则将对象的mark word设置为无锁模式
    if (!allow_rebias) {
      obj->set_mark(unbiased_prototype);
    }
    ...
    return BiasedLocking::BIAS_REVOKED;
  }

  // code 1:判断偏向线程是否还存活
  bool thread_is_alive = false;
  // 如果当前线程就是偏向线程 
  if (requesting_thread == biased_thread) {
    thread_is_alive = true;
  } else {
     // 遍历当前jvm的所有线程,如果能找到,则说明偏向的线程还存活
    for (JavaThread* cur_thread = Threads::first(); cur_thread != NULL; cur_thread = cur_thread->next()) {
      if (cur_thread == biased_thread) {
        thread_is_alive = true;
        break;
      }
    }
  }
  // 如果偏向的线程已经不存活了
  if (!thread_is_alive) {
    // 允许重偏向则将对象mark word设置为匿名偏向状态,否则设置为无锁状态
    if (allow_rebias) {
      obj->set_mark(biased_prototype);
    } else {
      obj->set_mark(unbiased_prototype);
    }
    ...
    return BiasedLocking::BIAS_REVOKED;
  }

  // 线程还存活则遍历线程栈中所有的Lock Record
  GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(biased_thread);
  BasicLock* highest_lock = NULL;
  for (int i = 0; i < cached_monitor_info->length(); i++) {
    MonitorInfo* mon_info = cached_monitor_info->at(i);
    // 如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码
    if (mon_info->owner() == obj) {
      ...
      // 需要升级为轻量级锁,直接修改偏向线程栈中的Lock Record。为了处理锁重入的case,在这里将Lock Record的Displaced Mark Word设置为null,第一个Lock Record会在下面的代码中再处理
      markOop mark = markOopDesc::encode((BasicLock*) NULL);
      highest_lock = mon_info->lock();
      highest_lock->set_displaced_header(mark);
    } else {
      ...
    }
  }
  if (highest_lock != NULL) {
    // 修改第一个Lock Record为无锁状态,然后将obj的mark word设置为指向该Lock Record的指针
    highest_lock->set_displaced_header(unbiased_prototype);
    obj->release_set_mark(markOopDesc::encode(highest_lock));
    ...
  } else {
    // 走到这里说明偏向线程已经不在同步块中了
    ...
    if (allow_rebias) {
       //设置为匿名偏向状态
      obj->set_mark(biased_prototype);
    } else {
      // 将mark word设置为无锁状态
      obj->set_mark(unbiased_prototype);
    }
  }

  return BiasedLocking::BIAS_REVOKED;
}

需要注意下,当调用锁对象的Object#hashSystem.identityHashCode()方法会导致该对象的偏向锁或轻量级锁升级。这是因为在Java中一个对象的hashcode是在调用这两个方法时才生成的,如果是无锁状态则存放在mark word中,如果是重量级锁则存放在对应的monitor中,而偏向锁是没有地方能存放该信息的,所以必须升级。具体可以看这篇文章hashcode()方法对偏向锁的影响小节(注意:该文中对于偏向锁的加锁描述有些错误),另外我也向该文章作者请教过一些问题,他很热心的回答了我,在此感谢一下!

言归正传,revoke_bias方法逻辑:

  1. 查看偏向的线程是否存活,如果已经不存活了,则直接撤销偏向锁。JVM维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。
  2. 偏向的线程是否还在同步块中,如果不在了,则撤销偏向锁。我们回顾一下偏向锁的加锁流程:每次进入同步块(即执行monitorenter)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record,将其obj字段指向锁对象。每次解锁(即执行monitorexit)的时候都会将最低的一个相关Lock Record移除掉。所以可以通过遍历线程栈中的Lock Record来判断线程是否还在同步块中。
  3. 将偏向线程所有相关Lock RecordDisplaced Mark Word设置为null,然后将最高位的Lock RecordDisplaced Mark Word 设置为无锁状态,最高位的Lock Record也就是第一次获得锁时的Lock Record(这里的第一次是指重入获取锁时的第一次),然后将对象头指向最高位的Lock Record,这里不需要用CAS指令,因为是在safepoint。 执行完后,就升级成了轻量级锁。原偏向线程的所有Lock Record都已经变成轻量级锁的状态。这里如果看不明白,请回顾上篇文章的轻量级锁加锁过程。

偏向锁的释放

偏向锁的释放入口在bytecodeInterpreter.cpp#1923

CASE(_monitorexit): {
  oop lockee = STACK_OBJECT(-1);
  CHECK_NULL(lockee);
  // derefing's lockee ought to provoke implicit null check
  // find our monitor slot
  BasicObjectLock* limit = istate->monitor_base();
  BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
  // 从低往高遍历栈的Lock Record
  while (most_recent != limit ) {
    // 如果Lock Record关联的是该锁对象
    if ((most_recent)->obj() == lockee) {
      BasicLock* lock = most_recent->lock();
      markOop header = lock->displaced_header();
      // 释放Lock Record
      most_recent->set_obj(NULL);
      // 如果是偏向模式,仅仅释放Lock Record就好了。否则要走轻量级锁or重量级锁的释放流程
      if (!lockee->mark()->has_bias_pattern()) {
        bool call_vm = UseHeavyMonitors;
        // header!=NULL说明不是重入,则需要将Displaced Mark Word CAS到对象头的Mark Word
        if (header != NULL || call_vm) {
          if (call_vm || Atomic::cmpxchg_ptr(header, lockee->mark_addr(), lock) != lock) {
            // CAS失败或者是重量级锁则会走到这里,先将obj还原,然后调用monitorexit方法
            most_recent->set_obj(lockee);
            CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
          }
        }
      }
      //执行下一条命令
      UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
    }
    //处理下一条Lock Record
    most_recent++;
  }
  // Need to throw illegal monitor state exception
  CALL_VM(InterpreterRuntime::throw_illegal_monitor_state_exception(THREAD), handle_exception);
  ShouldNotReachHere();
}

上面的代码结合注释理解起来应该不难,偏向锁的释放很简单,只要将对应Lock Record释放就好了,而轻量级锁则需要将Displaced Mark Word替换到对象头的mark word中。如果CAS失败或者是重量级锁则进入到InterpreterRuntime::monitorexit方法中。该方法会在轻量级与重量级锁的文章中讲解。

批量重偏向和批量撤销

批量重偏向和批量撤销的背景可以看上篇文章,相关实现在BiasedLocking::revoke_and_rebias中:

BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
  ...
  //code 1:重偏向的逻辑
  HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
  // 非重偏向的逻辑
  ...
      
  assert((heuristics == HR_BULK_REVOKE) ||
         (heuristics == HR_BULK_REBIAS), "?");	
   //code 2:批量撤销、批量重偏向的逻辑
  VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
                                (heuristics == HR_BULK_REBIAS),
                                attempt_rebias);
  VMThread::execute(&bulk_revoke);
  return bulk_revoke.status_code();
}

在每次撤销偏向锁的时候都通过update_heuristics方法记录下来,以类为单位,当某个类的对象撤销偏向次数达到一定阈值的时候JVM就认为该类不适合偏向模式或者需要重新偏向另一个对象,update_heuristics就会返回HR_BULK_REVOKEHR_BULK_REBIAS。进行批量撤销或批量重偏向。

先看update_heuristics方法。

static HeuristicsResult update_heuristics(oop o, bool allow_rebias) {
  markOop mark = o->mark();
  //如果不是偏向模式直接返回
  if (!mark->has_bias_pattern()) {
    return HR_NOT_BIASED;
  }
 
  // 锁对象的类
  Klass* k = o->klass();
  // 当前时间
  jlong cur_time = os::javaTimeMillis();
  // 该类上一次批量撤销的时间
  jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();
  // 该类偏向锁撤销的次数
  int revocation_count = k->biased_lock_revocation_count();
  // BiasedLockingBulkRebiasThreshold是重偏向阈值(默认20),BiasedLockingBulkRevokeThreshold是批量撤销阈值(默认40),BiasedLockingDecayTime是开启一次新的批量重偏向距离上次批量重偏向的后的延迟时间,默认25000。也就是开启批量重偏向后,经过了一段较长的时间(>=BiasedLockingDecayTime),撤销计数器才超过阈值,那我们会重置计数器。
  if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
      (revocation_count <  BiasedLockingBulkRevokeThreshold) &&
      (last_bulk_revocation_time != 0) &&
      (cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
    // This is the first revocation we've seen in a while of an
    // object of this type since the last time we performed a bulk
    // rebiasing operation. The application is allocating objects in
    // bulk which are biased toward a thread and then handing them
    // off to another thread. We can cope with this allocation
    // pattern via the bulk rebiasing mechanism so we reset the
    // klass's revocation count rather than allow it to increase
    // monotonically. If we see the need to perform another bulk
    // rebias operation later, we will, and if subsequently we see
    // many more revocation operations in a short period of time we
    // will completely disable biasing for this type.
    k->set_biased_lock_revocation_count(0);
    revocation_count = 0;
  }

  // 自增撤销计数器
  if (revocation_count <= BiasedLockingBulkRevokeThreshold) {
    revocation_count = k->atomic_incr_biased_lock_revocation_count();
  }
  // 如果达到批量撤销阈值则返回HR_BULK_REVOKE
  if (revocation_count == BiasedLockingBulkRevokeThreshold) {
    return HR_BULK_REVOKE;
  }
  // 如果达到批量重偏向阈值则返回HR_BULK_REBIAS
  if (revocation_count == BiasedLockingBulkRebiasThreshold) {
    return HR_BULK_REBIAS;
  }
  // 没有达到阈值则撤销单个对象的锁
  return HR_SINGLE_REVOKE;
}

当达到阈值的时候就会通过VM 线程在safepoint调用bulk_revoke_or_rebias_at_safepoint, 参数bulk_rebias如果是true代表是批量重偏向否则为批量撤销。attempt_rebias_of_object代表对操作的锁对象o是否运行重偏向,这里是true

static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o,
                                                                   bool bulk_rebias,
                                                                   bool attempt_rebias_of_object,
                                                                   JavaThread* requesting_thread) {
  ...
  jlong cur_time = os::javaTimeMillis();
  o->klass()->set_last_biased_lock_bulk_revocation_time(cur_time);


  Klass* k_o = o->klass();
  Klass* klass = k_o;

  if (bulk_rebias) {
    // 批量重偏向的逻辑
    if (klass->prototype_header()->has_bias_pattern()) {
      // 自增前类中的的epoch
      int prev_epoch = klass->prototype_header()->bias_epoch();
      // code 1:类中的epoch自增
      klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
      int cur_epoch = klass->prototype_header()->bias_epoch();

      // code 2:遍历所有线程的栈,更新类型为该klass的所有锁实例的epoch
      for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
        GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
        for (int i = 0; i < cached_monitor_info->length(); i++) {
          MonitorInfo* mon_info = cached_monitor_info->at(i);
          oop owner = mon_info->owner();
          markOop mark = owner->mark();
          if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
            // We might have encountered this object already in the case of recursive locking
            assert(mark->bias_epoch() == prev_epoch || mark->bias_epoch() == cur_epoch, "error in bias epoch adjustment");
            owner->set_mark(mark->set_bias_epoch(cur_epoch));
          }
        }
      }
    }

    // 接下来对当前锁对象进行重偏向
    revoke_bias(o, attempt_rebias_of_object && klass->prototype_header()->has_bias_pattern(), true, requesting_thread);
  } else {
    ...

    // code 3:批量撤销的逻辑,将类中的偏向标记关闭,markOopDesc::prototype()返回的是一个关闭偏向模式的prototype
    klass->set_prototype_header(markOopDesc::prototype());

    // code 4:遍历所有线程的栈,撤销该类所有锁的偏向
    for (JavaThread* thr = Threads::first(); thr != NULL; thr = thr->next()) {
      GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(thr);
      for (int i = 0; i < cached_monitor_info->length(); i++) {
        MonitorInfo* mon_info = cached_monitor_info->at(i);
        oop owner = mon_info->owner();
        markOop mark = owner->mark();
        if ((owner->klass() == k_o) && mark->has_bias_pattern()) {
          revoke_bias(owner, false, true, requesting_thread);
        }
      }
    }

    // 撤销当前锁对象的偏向模式
    revoke_bias(o, false, true, requesting_thread);
  }

  ...
  
  BiasedLocking::Condition status_code = BiasedLocking::BIAS_REVOKED;

  if (attempt_rebias_of_object &&
      o->mark()->has_bias_pattern() &&
      klass->prototype_header()->has_bias_pattern()) {
    // 构造一个偏向请求线程的mark word
    markOop new_mark = markOopDesc::encode(requesting_thread, o->mark()->age(),
                                           klass->prototype_header()->bias_epoch());
    // 更新当前锁对象的mark word
    o->set_mark(new_mark);
    status_code = BiasedLocking::BIAS_REVOKED_AND_REBIASED;
    ...
  }

  ...

  return status_code;
}

该方法分为两个逻辑:批量重偏向和批量撤销。

先看批量重偏向,分为两步:

code 1 将类中的撤销计数器自增1,之后当该类已存在的实例获得锁时,就会尝试重偏向,相关逻辑在偏向锁获取流程小节中。

code 2 处理当前正在被使用的锁对象,通过遍历所有存活线程的栈,找到所有正在使用的偏向锁对象,然后更新它们的epoch值。也就是说不会重偏向正在使用的锁,否则会破坏锁的线程安全性。

批量撤销逻辑如下:

code 3将类的偏向标记关闭,之后当该类已存在的实例获得锁时,就会升级为轻量级锁;该类新分配的对象的mark word则是无锁模式。

code 4处理当前正在被使用的锁对象,通过遍历所有存活线程的栈,找到所有正在使用的偏向锁对象,然后撤销偏向锁。

yinjk commented

请问一下作者,文中偏向锁的撤销小节中,当线程A已经退出同步代码块了,此时线程B请求对象锁触发偏向锁撤销流程,锁对象根据是否允许重偏向被撤销为匿名偏向状态或无锁状态,若允许重偏向,线程B能直接获取到偏向锁吗,是否允许重偏向是如何判断的呢,期待解惑,万分感谢

@yinjk 当B请求锁时,如果没有触发到批量重偏向的阈值(update_heuristics方法),那会被直接撤销为无锁状态。而如果到到达了重偏向的阈值,则会触发批量重偏向,逻辑在bulk_revoke_or_rebias_at_safepoint方法中,首先会自增类中的epoch(代码),然后会调用revoke_bias(代码),且第二个参数allow_rebias为true。如果A已经退出同步块了但是还存活,则在revoke_bias中的这里,会进行重偏向。

有不明白的地方可以再留言

yinjk commented

感谢解答,文章写得很好很清楚,基本上理解了,但是还有一点点疑问,在ObjectSynchronizer::fast_enter方法中(代码),只有方法revoke_and_rebias返回值为BiasedLocking::BIAS_REVOKED_AND_REBIASE才会return,表示获取到偏向锁,否则最终都会进入到slow_enter方法(代码),该方法是轻量级锁加锁方法,所以我猜测,当线程B触发的偏向锁撤销操作如果达到了批量重偏向的阈值,在这里返回的一定是BiasedLocking::BIAS_REVOKED_AND_REBIASE,但是我现在无法验证我的猜想,因为我不知道bulk_revoke.status_code()的值,我看VMThread::execute方法也没看明白(我不太会c++),希望作者大大指点一二;还有一个问题就是,你上面的回答说

如果A已经退出同步块了但是还存活,则在revoke_bias中的这里,会进行重偏向。

但是此处只是将锁对象的mark word设置为匿名偏向状态,而未通过CAS将偏向线程设置为当前B线程的ID,这个逻辑是在哪里实现的呢

@yinjk 先回答第二个问题。

但是此处只是将锁对象的mark word设置为匿名偏向状态,而未通过CAS将偏向线程设置为当前B线程的ID,这个逻辑是在哪里实现的呢

非常抱歉,我上面的回答中关于重偏向的代码引用有误。

下面为正确答案:

对于B线程来说,逻辑在这里

对于其他case来说,是在下一次获得锁时发生([这里])(http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/runtime/biasedLocking.cpp#l576)。
这里其他case是指:发生批量重偏向时,该class的所有已经创建好的且不在同步块中锁对象。因为在批量重偏向时,class的epoch+1了,但是这些对象的epoch还是原值。

@yinjk 关于第一个问题,应该是说:有且只有调用revoke_and_rebias的线程被重偏向为另一个线程了,返回的才是BiasedLocking::BIAS_REVOKED_AND_REBIASE(大致扫了下代码,应该是这样)。然后决定bulk_revoke.status_code()值的代码在这里VMThread::execute得主要作用是将对应方法(bulk_revoke所在类的doit)放到VM Thread中执行,关于VM Thread的介绍可以看这里

yinjk commented

非常感谢作者耐心解答,根据指示,问题已经得到解决。

zyllt commented

感谢,最近一直在学习偏向锁方面的知识,自己也看了源码还是无法理解一些细节,拜读了此文之后疑问全解,感觉浑身通透

@zyllt 有帮助就好~

zyllt commented

@yinjk 先回答第二个问题。

但是此处只是将锁对象的mark word设置为匿名偏向状态,而未通过CAS将偏向线程设置为当前B线程的ID,这个逻辑是在哪里实现的呢

非常抱歉,我上面的回答中关于重偏向的代码引用有误。

下面为正确答案:

对于B线程来说,逻辑在这里

对于其他case来说,是在下一次获得锁时发生([这里])(http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/runtime/biasedLocking.cpp#l576)。%E3%80%82)
这里其他case是指:发生批量重偏向时,该class的所有已经创建好的且不在同步块中锁对象。因为在批量重偏向时,class的epoch+1了,但是这些对象的epoch还是原值。

有个疑问,如果没有达到批量重偏向应该执行的是单个重偏向,这里直接返回了BiasedLocking::BIAS_REVOKED,没有让B线程去竞争偏向锁。猜测可能是 VMThread::execute(&revoke);这里的问题,对VMThread不是很熟悉,可以说明一下么,感谢~~

      // 下面代码最终会在VM线程中的safepoint调用revoke_bias方法
      VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
      VMThread::execute(&revoke);
      return revoke.status_code();

@zyllt
首先从概念上来说只有批量重偏向,没有单个重偏向。批量重偏向需要用到VMThread,因为要操作其他线程的栈,所以需要在借助VMThread在safepopint中执行。

对于非批量重偏向的步骤如下:
1.如果线程A还存活且还在同步块中,则将锁对象升级为轻量级锁,同时需要修改线程A的栈,代码在这里。线程A不存活或不在同步块中,则先将锁对象设置为无锁状态,代码在这里

2.在之后的slow_enter中,如果锁对象是无锁状态,则会升级为轻量级锁。否则升级为重量级锁。

也就是说,如果一个锁已经偏向线程A,当线程B尝试获得该锁时,无论线程A是什么状态,该锁都会升级成轻量级锁或重量级锁(不考虑批量重偏向的情况)。

如果你还不太理解,建议先看下上篇文章补充下概念上的知识。

zyllt commented

@farmerjohngit 感谢回答,确实是我理解错了。

您的文章写的非常详细,受教了!我有一个问题,始终没有明白,在偏向锁入口code10,为什么在这个逻辑里处理偏向锁重入
// 如果偏向线程不是当前线程或没有开启偏向模式等原因都会导致success==false
if (!success) {
不是只有对同一个线程来讲才有锁重入的概念吗?既然如此,为什么偏向线程不是当前线程呢?还是我对锁重入的概念理解错了?麻烦您能指导一下,这个问题困惑我好久了,谢谢!

@ValiantYuan 走到这里说明已经是轻量级锁的逻辑了

@farmerjohngit

@ValiantYuan 走到这里说明已经是轻量级锁的逻辑了

谢谢,明白了。

DanFL commented

您好,我想请教一下,假如偏向锁持所线程Thread A已经不存活了,Thread B触发VMThread将偏向锁重置为匿名偏向后,Thread B在又何时重新CAS获取到偏向锁?

@DanFL 可以看上面对 @zyllt 的回答:

也就是说,如果一个锁已经偏向线程A,当线程B尝试获得该锁时,无论线程A是什么状态,该锁都会升级成轻量级锁或重量级锁(不考虑批量重偏向的情况)。

所以B普通情况下不会再获得该偏向锁。

而升级轻量级锁或重量级锁的逻辑在这里

DanFL commented

是指只要Thread B在HR_SINGLE_REVOKE case下,因为revoke_bias()方法传的allow_rebias都是false,所以不会重偏向

@DanFL 可以看上面对 @zyllt 的回答:

也就是说,如果一个锁已经偏向线程A,当线程B尝试获得该锁时,无论线程A是什么状态,该锁都会升级成轻量级锁或重量级锁(不考虑批量重偏向的情况)。

所以B普通情况下不会再获得该偏向锁。

而升级轻量级锁或重量级锁的逻辑在这里

是指只要Thread B在HR_SINGLE_REVOKE case下,因为revoke_bias()方法传的allow_rebias都是false,所以不会重偏向?

还有几个问题也想一并请教一下

  1. epoch这个值,每次都是自增都是发生在safepoint里执行的,假如锁对象epoch过期,也只是将类对象中的epoch值更新到锁对象中。那锁对象中的epoch值有何作用?并没有在其他处用到,假如是为了记录锁批量重偏向次数,只保留类对象中的epoch不就可以了么?
  2. prototype_header是存在类对象当中的,类对象本身作为对象应该也有属于自己的MardWord,是么?若是的话,这两者之间有什么区别么?
  3. 关于批量重偏向,依我的理解,假如Thread B获取所,第一次发生锁撤销,revocation_count从0到1,就进入HR_SINGLE_REVOKE case进行锁升级了。如果不考虑重置revocation_count,同一个类的锁对象只有第20次到40次之间的锁撤销才有可能重偏向
  4. 下述中提到epoch值过期也要进入到锁撤销,而不是CAS重偏向失败才进入的么?

以上是偏向锁加锁的流程(包括部分轻量级锁的加锁流程),如果当前锁已偏向其他线程||epoch值过期||偏向模式关闭||获取偏向锁的过程中存在并发冲突,都会进入到InterpreterRuntime::monitorenter方法, 在该方法中会对偏向锁撤销和升级。
中提到epoch值过期也要进入到锁撤销,而不是CAS重偏向失败才进入的么?

@DanFL 最近比较忙,你的问题我慢慢回答哈

是指只要Thread B在HR_SINGLE_REVOKE case下,因为revoke_bias()方法传的allow_rebias都是false,所以不会重偏向?

epoch这个值,每次都是自增都是发生在safepoint里执行的,假如锁对象epoch过期,也只是将类对象中的epoch值更新到锁对象中。那锁对象中的epoch值有何作用?并没有在其他处用到,假如是为了记录锁批量重偏向次数,只保留类对象中的epoch不就可以了么?

epoch的作用是在触发批量重偏向阈值后,下次获得锁时,通过判断class的epoch和obj的epoch决定要不要直接重偏向。 这点在本文中也说过了,建议你将本文开头处的monitorenterrevoke_and_rebias相关代码在看一遍,我在注释中都有说明

下述中提到epoch值过期也要进入到锁撤销,而不是CAS重偏向失败才进入的么?

这里我写的有问题,是重偏向失败才进入

prototype_header是存在类对象当中的,类对象本身作为对象应该也有属于自己的MardWord,是么?若是的话,这两者之间有什么区别么?

如果你说的是Java层的类对象,那是的,因为类对象本身也是对象。至于你问的区别,markword是对象的(包括类对象),Native层对应的是oop->mark();而prototype_header是类的,Native层的klass->prototype_header()

如果你说的是Native层的Klass,那不是的,klass.hpp中并没有markword只有prototype_header

水平不够 看源码实在是受不住啊 全靠注释看下来了 有个小问题 在锁升级的时候:

将偏向线程所有相关Lock Record的Displaced Mark Word设置为null,然后将最高位的Lock Record的Displaced Mark Word 设置为无锁状态,最高位的Lock Record也就是第一次获得锁时的Lock Record(这里的第一次是指重入获取锁时的第一次)

偏向锁重入也会插入一次Lock Record嘛?那为什么上面偏向锁重入那里的注释是什么也没做呢?
然后是括号里的内容 为什么是一次重入获取的锁 第一次偏向的时候不就分配了一个Lock Record嘛 为什么用的是重入加的Lock Record呢?

@pigeonsoar

偏向锁重入也会插入一次Lock Record嘛?那为什么上面偏向锁重入那里的注释是什么也没做呢?
code1 和 code2处已经拿到一个空闲的Lock Record,然后指向当前锁对象了。

然后是括号里的内容 为什么是一次重入获取的锁 第一次偏向的时候不就分配了一个Lock Record嘛 为什么用的是重入加的Lock Record呢?

不太记得了 不排除我搞错了

如果要撤销的锁偏向的不是当前线程,会将该操作push到VM Thread中等到safepoint的时候再执行。
等待safepoint的这个过程中,当前线程是继续往下走到slow_enter方法里面吗?还是阻塞直到升级成为轻量级锁?

@May0302 在VM_RevokeBias的模式下,VMThread::execute是阻塞的

如果一个系统都用object类作为锁,是不是很可能导致这个object锁被撤销?

看了您的文章受益匪浅,但是也有以下几个问题,期待您的解答,谢谢!

  1. 每次进入monitor_enter就创建一个lock record是哪段代码,没找到啊
    2.偏向锁释放后lock record怎么办,就留在栈帧中吗?
  2. 偏向锁升级时发现当前持有当前对象的线程已经不再使用,那么会将其置为无锁状态,然后升级,此时这个无锁状态中有哪些字段?还有hashcode吗?
  3. 轻量级锁解锁后,用cas替换对象的markword为栈帧中的displaced mark word,此时displaced mark word应该是锁升级前的,那么此时轻量级锁是不是就完全释放,下一次加锁前是偏向/无锁状态,而不是直接进入轻量级锁?这样不就是锁降级了?

还有一个问题,对象mark word中的锁标志位是在哪块修改的,只看到CAS修改指针地址,没看到对锁标志位修改,谢谢!

比如程序员如何知道锁的状态呢?有没有办法直观看到某个时候锁的状态,比是不是偏向锁,是不是重量级锁?

看了受益匪浅!!有个问题想问问
在批量重偏向的时候会对klass的epoch自增,然后在对klass所有锁对象的epoch自增。在这个过程中是否会有影响到其他锁对象的线程安全性?例如刚对klass的epoch自增了,然后立马有个线程过来尝试判断 else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) ,接下来的cas代码问看了看应该是可以直接成功的,这样如果线程a还在同步块中的话,线程b通过这里也获得了偏向锁(强调是klass的epoch刚自增)。还有就是为啥对klass的epoch自增,然后要对klass所有锁对象的epoch自增?意义在哪,感觉跟epoch过时可以重偏向有点冲突,读源码能力有限,望解答解答

将撤销偏向锁的操作放到进入SafePoint时执行,VMThread::execute(&revoke)是会阻塞当前线程的执行吗?
VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
VMThread::execute(&revoke);
return revoke.status_code();

WLWL commented

会走到该方法的逻辑有很多,我们只分析最常见的情况:假设锁已经偏向线程A,这时B线程尝试获得锁。

上面的code 1,code 2B线程都不会走到,最终会走到code 4处,如果要撤销的锁偏向的是当前线程则直接调用revoke_bias撤销偏向锁,否则会将该操作push到VM Thread中等到safepoint的时候再执行。

这里的当前线程 指的是线程A? @farmerjohngit

偏向锁是在 同步代码块之后释放吗?

你好,对象头中,轻量级锁的标识是 00 ,为什升级为轻量级锁的时候,却把Displaced header设置为锁状态的 01,这样轻量级锁释放的时候,对象头不是变成01(无锁)了吗。不应该是00(轻量级锁)吗

大神你好,看了很多遍,受益匪浅,奈何实在不懂C++代码。我想请教下,Lock Record是在执行_monitorenter前就已经在栈中创建好了吗?C++代码中没看到有这块的内容。当然你的博客中有提及

9,这一步已经是轻量级锁的逻辑了。从上图的mark word的格式可以看到,轻量级锁中mark word存的是指向Lock Record的指针。这里构造一个无锁状态的mark word,然后存储到Lock Record(Lock Record的格式可以看第一篇文章)。设置mark word是无锁状态的原因是:轻量级锁解锁时是将对象头的mark word设置为Lock Record中的Displaced Mark Word,所以创建时设置为无锁状态,解锁时直接用CAS替换就好了。

并不是构造一个无锁状态的mark word,只是把最后一位置为1而已,避免是轻量级或者重量级锁

在调用 hashcode 方法时轻量级锁也需要锁升级吗?我理解轻量级锁里 Lock Word 的 displaced mark word 不是可以存 hashcode 嘛

else {
  // 走到这里说明当前要么偏向别的线程,要么是匿名偏向(即没有偏向任何线程)
  // code 8:下面构建一个匿名偏向的mark word,尝试用CAS指令替换掉锁对象的mark word
  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);)
    if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
      // CAS修改成功
      if (PrintBiasedLockingStatistics)
        (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
    }
  else {
    // 如果修改失败说明存在多线程竞争,所以进入monitorenter方法
    CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
  }
  success = true;
}

这里有一个细节,避免有的同学跟我产生一样的误解,所以在评论里提一下,注意看 CAS 操作的参数,旧值是匿名偏向的 mark word ,也就是说这个 CAS 操作的本意是尝试把一个匿名偏向改为偏向当前本线程,如果此时该锁对象已经偏向某个线程,那么这个 CAS 操作肯定是失败的,就会进入到锁撤销的逻辑(即 InterpreterRuntime::monitorenter 方法里)。
我之前产生的误解是:“不管当前锁对象是否已经偏向某个线程,都会直接 CAS 尝试修改偏向为本线程”。这样做是不合理的,因为前一个线程很有可能还在执行同步代码块,这里如果 CAS 修改成功就表明当前线程获取偏向锁成功,就违反了锁的互斥性。

epoch在markWord中占用的是2bit位,也就是最大值为11 = 3,它是如何被增加到40以后修改成去掉偏向锁的呢?这个是bit不是byte呀!

Lock Record是在执行_monitorenter前就已经在栈中创建好了吗?C++代码中没看到有这块的内容。当然你的博客中有提及

是在私有线程栈找到一个最近并且空闲的区域,创建一个Lock Record,里面属性为null。
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
BasicObjectLock* entry = NULL;

你好,对象头中,轻量级锁的标识是 00 ,为什升级为轻量级锁的时候,却把Displaced header设置为锁状态的 01,这样轻量级锁释放的时候,对象头不是变成01(无锁)了吗。不应该是00(轻量级锁)吗

轻量级锁释放的时候,即跳出临界区,要把对象设置为无锁的状态

没有办法直观看到某个时候锁的状态,比是不是偏向锁,是不是重量级锁
在pom.xml添加中:


org.openjdk.jol
jol-core
0.9

log.debug(ClassLayout.parseInstance(l).toPrintable());
这个可以打印对象头 查看到对象头信息
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 68 f0 5e 20 (01101000 11110000 01011110 00100000) (543092840)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 63 8c 01 f8 (01100011 10001100 00000001 11111000) (-134116253)
12 4 (loss due to the next object alignment)

<!--sun公司 专门解析对象实例,查看对象布局-->
<dependency>
 <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.9</version>
</dependency>

你好,作者,有一些问题,不是很明白。
假设偏向延迟关闭的情况下,t1线程 进入代码之后,释放了。t2线程来进入代码,升级成轻量锁,释放了锁。对象l 变成一个无锁的状态00000001,这时只有t3线程来加锁,success=false , 会进入if (!success) ,构建在内存的无锁markword 01,比较当前持有对象l 的markword 与内存中产生是否相同,相同则l 对象的对象头指向当前线程的锁记录。
这步没有问题,但是有疑问是CAS加锁成功后,l对象的markword 原来01 ,但我们知道轻量锁加锁的时候,会把对象头改为00,但我看了好久,都没有找到相对应的代码?CAS已经成功了,好像也不会进入膨胀过程里CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
不知道JVM是怎么把对象头的01无锁改为00轻量锁的?

想请问一下:
(1)当线程第一次获得偏向锁的时候,是否会在栈中添加一个空的Lock Record,若不会,则当其他线程来竞争时才会往持有锁的线程添加Lock Record吗?否则,怎么进行升级轻量级锁。
【我的理解:要添加一个空的Lock Record,其中只设置obj指向锁的对象】

(2)当偏向锁重入的时候,是否要继续添加Lock Record?
【我的理解,同样需要,Lock Record和第一次添加的相同,若不添加Lock Record怎么确定锁重入的次数】

是的。要添加一个空的Lock Record,其中只设置obj指向锁的对象。但是在不同的栈帧里面。synchronized{        //1      synchronized{          //2      }   //3  }  //4 只要进入 synchronized 对应 _monitorenter,只要退出synchronized 对应 _monitorexit;所以两个Lock Record 并没有冲突的。

噢,感谢作者!我大概明白了,总的来说就是:

  1. 如果偏向锁第一次进入,经过所有检测后,会在当前线程栈帧中找到内存地址最高的可用Lock Record,Displaced Mark Word会将线程ID存入,同时obj指向对象,并修改对象头为线程ID

  2. 如果是偏向锁重入,则只会添加一个DMW为空的Lock Record,其中obj还是会指向对象

  3. 当偏向锁释放时,只会从底到高,将obj相符的Lock Record的obj设置为null,不会改变对象Mark Word的threadId

最后一个小疑问就是,我在代码中似乎没看到偏向锁重入时会添加一个空的lock Record(虽然大概率是我理解错了)
// code 5:如果偏向的线程是自己且epoch等于class的epoch if (anticipated_bias_locking_value == 0) { // **already biased towards this thread, nothing to do** if (PrintBiasedLockingStatistics) { (* BiasedLocking::biased_lock_entry_count_addr())++; } success = true; }
我的理解是偏向锁重入时,也是走code5,可是在这里面并没有做任何事情,同时success为true。

而我认为添加空Lock Record的代码只有在处理轻量级锁的流程里面,也就是if (! successs){...}中的
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) { //code 10: 如果是锁重入,则直接将Displaced Mark Word设置为null entry->lock()->set_displaced_header(NULL); } else { CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); }

希望作者给予最后的指点!谢谢!

在阅读偏向锁的源码后,自己对知识做了测试验证,其中下面有一个理解上面的确认 和 还有一个没有理解清楚的疑问,下面先说明一下测试的情况:

synchronized (lockObj) {
    System.out.println(ClassLayout.parseInstance(lockObj).toPrintable());
    // biased_lock偏向锁标识位+lock锁状态标识位=101
}

//  code 1

new Thread(() -> {
    synchronized (lockObj) {
        System.out.println(ClassLayout.parseInstance(lockObj).toPrintable()); // code 2
        //biased_lock偏向锁标识位+lock锁状态标识位=000,轻量级锁状态。
    }
}).start();

I,在code1 没有代码的代码时候,上面code2打印是轻量级锁状态000。

这个状态我的理解1:这个按照源码的推演,在执行第二个synchronized时:应该是批量重偏向的阈值还未达到,所以先阻塞进入安全点->再修改成无锁状态,->最终膨胀成轻量级锁(ObjectSynchronizer::slow_enter中在)。

II,如果在code1出加入 System.gc()后,则在code 2 打印的锁状态为偏向锁101。

这个状态我的理解2:这里为什么手动触发一次fullgc就会重偏向呢? 我猜测fullgc会进入全局安全点会将class的epoch+1,但是锁对象的不变(没有找到代码验证此猜测,也是我疑惑的点),导致2者不一致。最终在执行第二个synchronized时,由于class和锁实例的epoch不一致,会触发重偏向新线程。源码

第二个测试验证:

new Thread(() -> {
    synchronized (lockObj) {
        System.out.println(ClassLayout.parseInstance(lockObj).toPrintable());
        // biased_lock偏向锁标识位+lock锁状态标识位=101
    }
}).start();

new Thread(() -> {
    synchronized (lockObj) {
        System.out.println(ClassLayout.parseInstance(lockObj).toPrintable()); //code3
        // biased_lock偏向锁标识位+lock锁状态标识位=101
    }
}).start();

上面代码改成2个独立的线程。 这里却为什么 在code3 这里却是打印为偏向锁状态(等于发生了单次重偏向)。

我的理解3: 未知!!! 这里按理跟 ‘理解1’那里的情况是一样,应该也是 轻量级锁状态,因为lockObj都不在同步块,并且前面已经有过偏向,而且有没有出现epoch不一致的情况,这里为何就是 做了一次重偏向呢?

我用的jdk1.9测试的。 请教一下几个疑问
1,上面我的理解 1 是否正确?
2,理解2中的猜测是否正确,验证的代码在哪?
3,上面我的理解3是未知的,希望能帮忙解惑以下

@farmerjohngit
@WLWL @wangliyu0328

@xdcode2020
我使用jdk1.8测试,
我能不能看一下,你的第一个测试例子完整代码呢?
因为我在测试第一个例子时,不论是否 System.gc(),// code 2始终是轻量锁,00,只是在没有System.gc()的时候,分代年龄会是0,System.gc后,分代年龄变成1。
这是我的完整代码:

@Slf4j(topic = "enjoy")
public class TestJol {
    static LockObj lockObj=new LockObj();
    static Thread t1;
    public static void main(String[] args) throws InterruptedException {
        log.debug("front :"+ClassLayout.parseInstance(lockObj).toPrintable());
        synchronized (lockObj){
            log.debug("main :"+ClassLayout.parseInstance(lockObj).toPrintable());
        }
        System.gc();
        t1=new Thread(()->{
            synchronized (lockObj){
                log.debug("t1 :"+ClassLayout.parseInstance(lockObj).toPrintable());
            }
        });
        t1.start();
        t1.join();
        log.debug("main  end :"+ClassLayout.parseInstance(lockObj).toPrintable());
    }
}

至于第二个例子,可能会出现,这样的情况,CPU使用第一个线程 t1 后,结束后,代码在启动的线程可能还是t1。并不是重新启动另一个线程。但也可能不会出现这样的情况。根据时间片轮转,可能也不会出现这个问题。看机率。

使用博主你的代码,我测试: 在JDK9时:加了GC后,第二个同步块的锁为:101偏向锁(跟博主结果不一样), 没有加GC后,就是轻量级锁(跟博主结果一样),有点疑惑了 . 在用jdk8(jdk1.8.0_181,这个为非JDK8U版本吧)不管加不加GC,都是轻量级锁。

ps:我为什么要用JDK9,是因为看源码就如博主所说jdk8u和jdk8 偏向锁的源码存在差别,在核对与jdk8u以上的版本是一致的(特意对比了一下 jdk9 和jdk8u的几个偏向锁的核心函数的源码,逻辑都是一样的,只是打印日志做了些调整),所以采用JDK9也进行了一次对比测试。

想向博主和大家先确定一下这个认识是否正确?

​ 按照jdk8 偏向锁的源码,如果第一个线程A偏向后(重偏向阈值未到达,并且第一个线程A已跳出同步块),另外一个线程B再进入同步块,中我阅读的理解是不会重偏向,而是升级为同步锁,这个理解是否正确?

以上理解的源码调用栈分析依据:

第一步:revoke_and_rebias函数,因为class和对象的epoch是一致的(之前没有进入过安全点),所以会走这个分支阻塞等待进入安全点: 源码

第二步:安全点到达后,最终会调用revoke_bias函数,因为main线程存活,但已经跳出同步块,所以会走这个设置无锁状态:源码二

第三步:进入ObjectSynchronizer::slow_enter中升级成轻量级锁。


👇👇👇

JVM配置:-XX:BiasedLockingStartupDelay=0 关闭偏向锁延迟效果

2个测试的jdk版本:

image-20201204182154004

测试代码(使用的博主的):

	static LockObj lockObj = new LockObj();
    static Thread t1;

    public static void main(String[] args) throws InterruptedException {
        // Thread.sleep(10000);
        System.out.println("1:" + ClassLayout.parseInstance(lockObj).toPrintable());

        synchronized (lockObj) {
            System.out.println("main :" + ClassLayout.parseInstance(lockObj).toPrintable());
        }
        System.gc(); //下面有加与不加 这行代码的测试结果.
        t1 = new Thread(() -> {
            synchronized (lockObj) {
                System.out.println("t1 :" + ClassLayout.parseInstance(lockObj).toPrintable());
            }
        });
        t1.start();
        t1.join();
        System.out.println("main  end :" + ClassLayout.parseInstance(lockObj).toPrintable());
    }

加了System.gc();

JDK9的输出:

"C:\Program Files\Java\jdk-9.0.4\bin\java.exe" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:60094,suspend=y,server=n -XX:BiasedLockingStartupDelay=0 -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IdeaIC2020.2\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\86133\IdeaProjects\untitled1\target\classes;C:\Users\xxx\.m2\repository\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2020.2.3\lib\idea_rt.jar" ThreadTest
Connected to the target VM, address: '127.0.0.1:60094', transport: 'socket'
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
1:LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 d7 00 20 (10010010 11010111 00000000 00100000) (536926098)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

main :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 90 a7 7e (00000101 10010000 10100111 01111110) (2124910597)
      4     4        (object header)                           15 02 00 00 (00010101 00000010 00000000 00000000) (533)
      8     4        (object header)                           92 d7 00 20 (10010010 11010111 00000000 00100000) (536926098)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 32 23 (00000101 00000000 00110010 00100011) (590479365)
      4     4        (object header)                           15 02 00 00 (00010101 00000010 00000000 00000000) (533)
      8     4        (object header)                           92 d7 00 20 (10010010 11010111 00000000 00100000) (536926098)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

main  end :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 32 23 (00000101 00000000 00110010 00100011) (590479365)
      4     4        (object header)                           15 02 00 00 (00010101 00000010 00000000 00000000) (533)
      8     4        (object header)                           92 d7 00 20 (10010010 11010111 00000000 00100000) (536926098)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Disconnected from the target VM, address: '127.0.0.1:60094', transport: 'socket'

Process finished with exit code 0

JDK8的输出:

"C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:60117,suspend=y,server=n -XX:BiasedLockingStartupDelay=0 -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IdeaIC2020.2\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\untitled1\target\classes;C:\Users\86133\.m2\repository\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2020.2.3\lib\idea_rt.jar" ThreadTest
Connected to the target VM, address: '127.0.0.1:60117', transport: 'socket'
1:LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

main :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 38 62 03 (00000101 00111000 01100010 00000011) (56768517)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           28 f4 ea 1b (00101000 11110100 11101010 00011011) (468382760)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

main  end :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Disconnected from the target VM, address: '127.0.0.1:60117', transport: 'socket'

Process finished with exit code 0

不加System.gc();

JDK9的输出:

"C:\Program Files\Java\jdk-9.0.4\bin\java.exe" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:60161,suspend=y,server=n -XX:BiasedLockingStartupDelay=0 -javaagent:C:\Users\86133\AppData\Local\JetBrains\IdeaIC2020.2\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\86133\IdeaProjects\untitled1\target\classes;C:\Users\86133\.m2\repository\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2020.2.3\lib\idea_rt.jar" ThreadTest
Connected to the target VM, address: '127.0.0.1:60161', transport: 'socket'
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
1:LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 d7 00 20 (10010010 11010111 00000000 00100000) (536926098)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

main :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 60 b3 0d (00000101 01100000 10110011 00001101) (229859333)
      4     4        (object header)                           c6 01 00 00 (11000110 00000001 00000000 00000000) (454)
      8     4        (object header)                           92 d7 00 20 (10010010 11010111 00000000 00100000) (536926098)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           90 f3 ef 7a (10010000 11110011 11101111 01111010) (2062545808)
      4     4        (object header)                           99 00 00 00 (10011001 00000000 00000000 00000000) (153)
      8     4        (object header)                           92 d7 00 20 (10010010 11010111 00000000 00100000) (536926098)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

main  end :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 d7 00 20 (10010010 11010111 00000000 00100000) (536926098)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Disconnected from the target VM, address: '127.0.0.1:60161', transport: 'socket'

Process finished with exit code 0

JDK8的输出:

"C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:60152,suspend=y,server=n -XX:BiasedLockingStartupDelay=0 -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IdeaIC2020.2\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\untitled1\target\classes;C:\Users\86133\.m2\repository\org\openjdk\jol\jol-core\0.9\jol-core-0.9.jar;C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2020.2.3\lib\idea_rt.jar" ThreadTest
Connected to the target VM, address: '127.0.0.1:60152', transport: 'socket'
1:LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

main :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 38 c5 02 (00000101 00111000 11000101 00000010) (46479365)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           c8 f0 52 1b (11001000 11110000 01010010 00011011) (458420424)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

main  end :LockObj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Disconnected from the target VM, address: '127.0.0.1:60152', transport: 'socket'

Process finished with exit code 0

@wangliyu0328

@wangliyu0328
至于你说的第二个问题的分析原因,我之前也考虑过。 特意打印过线程ID,是2个不同的线程ID。 而且不是机率问题,是每次都是发生了重偏向

你好,作者,有一些问题,不是很明白。
假设偏向延迟关闭的情况下,t1线程 进入代码之后,释放了。t2线程来进入代码,升级成轻量锁,释放了锁。对象l 变成一个无锁的状态00000001,这时只有t3线程来加锁,success=false , 会进入if (!success) ,构建在内存的无锁markword 01,比较当前持有对象l 的markword 与内存中产生是否相同,相同则l 对象的对象头指向当前线程的锁记录。
这步没有问题,但是有疑问是CAS加锁成功后,l对象的markword 原来01 ,但我们知道轻量锁加锁的时候,会把对象头改为00,但我看了好久,都没有找到相对应的代码?CAS已经成功了,好像也不会进入膨胀过程里CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
不知道JVM是怎么把对象头的01无锁改为00轻量锁的?

@wangliyu0328 同样的疑惑,怀疑是不是Cas修改了record后也会同时修改锁状态?我还提了一个类似的问题

@xdcode2020
我昨天下载jdk1.9 测试,正如你所说第一个例子测试手动GC,将会线程t1将会重偏向自己。jdk1.8并不会。但我发现 jdk1.9 System.gc();手动GC一次,会清除对象头数据,使lockObj 对象 00000101 00000000 00000000 00000000。
当线程t1来加锁使,就会发生t1偏向自己的情况。我现在还在找相关代码,并不知道System.gc() 里面到底做了什么。这应该是jdk9的优化。
”按照jdk8 偏向锁的源码,如果第一个线程A偏向后(重偏向阈值未到达,并且第一个线程A已跳出同步块),另外一个线程B再进入同步块,中我阅读的理解是不会重偏向,而是升级为同步锁“
是这样理解的。最终会进入源码
升级成轻量级锁。
你的第二个例子验证:
2个独立的线程。code3 这里却是打印为偏向锁状态(等于发生了单次重偏向)。我这里使用jdk1.9 2个独立线程,测试的时候,有没有发生重偏向情况。第一个线程是重偏向,第二个线程是轻量锁。我这里测试,手动GC,只会影响main主线程,会发生清除对象头数据。

@scn7th
第二个问题,偏向锁升级时发现当前持有当前对象的线程已经不再使用,那么会将其置为无锁状态,然后升级,此时这个无锁状态中有哪些字段?还有hashcode吗?

无锁:没有使用:25位,hash:31位 ,没有使用:1位 GC年龄:4位 偏向标识:1 锁状态:01
如果你一开始代码就没有对对象设置hashcode hashcode 全部为0;

第三个问题 轻量级锁解锁后,用cas替换对象的markword为栈帧中的displaced mark word,此时displaced mark word应该是锁升级前的,那么此时轻量级锁是不是就完全释放下一次,加锁前是偏向/无锁状态,而不是直接进入轻量级锁?这样不就是锁降级了?
不是锁降级,锁的降级是从重量锁--->轻量锁--->偏向锁。
t1线程来加锁,退出后,释放锁,对象l 为偏向t1。t2线程来加锁,需要把t1的锁,撤销了,变成一把无锁的001,之后对象升级为轻量锁00.displaced mark word实际存的的确是锁升级前的值,但锁升级前,已经是无锁的001。需要进入_monitorexit。

@baixinping0 升级为轻量锁前,就是把Displaced header设置为锁状态的 01,轻量级锁释放的时候,对象头变成01(无锁)了。轻量锁升级的时候,CAS把锁标识设置为00。但轻量锁释放的时候,为01的。

@weiaiSanli

epoch在markWord中占用的是2bit位,也就是最大值为11 = 3,它是如何被增加到40以后修改成去掉偏向锁的呢?这个是bit不是byte呀!
是通过一个 int revocation_count变量,为该类偏向锁撤销的次数。代码 在这里
默认值为40,在globals.hpp里设置的。

@aLibeccio
在调用 hashcode 方法时轻量级锁也需要锁升级吗?我理解轻量级锁里 Lock Word 的 displaced mark word 不是可以存 hashcode 嘛
不是太明白的问题。
如果是偏向锁,对象头mark word 由54位线程id+2位 epoch+未标识1位+GC年龄4位+偏向标识1位+锁标识2位。如果调用 hashcode 方法,hashcode 就占了31位,线程id 就放不下了。只能从偏向锁升级为轻量锁,如果发现锁竞争,就要升级为重量锁。轻量级锁需要锁升级时候,应该是有锁竞争的情况。

DZQOX commented

@farmerjohngit 您好,我有一个疑问就是

  anticipated_bias_locking_value =
              (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
              ~((uintptr_t) markOopDesc::age_mask_in_place);

这里从klass的prototype_header中获取了一些信息,来判断偏向锁是否指向当前线程,epoch是否过期等,但是等偏向锁指向当前线程

if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {

后,并没有看见更新klass的prototype_header更新,那偏向锁重入就会有问题,请问prototype_header是在哪里更新的?

@farmerjohngit 您好,我有一个疑问就是

  anticipated_bias_locking_value =
              (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
              ~((uintptr_t) markOopDesc::age_mask_in_place);

这里从klass的prototype_header中获取了一些信息,来判断偏向锁是否指向当前线程,epoch是否过期等,但是等偏向锁指向当前线程

if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {

后,并没有看见更新klass的prototype_header更新,那偏向锁重入就会有问题,请问prototype_header是在哪里更新的?

  1. 偏向锁偏向某个线程时不会修改prototype_header
  2. 偏向锁的重入和LockRecord有关,不知道问题在哪
  3. klass的prototype_header只会在批量重偏向和批量撤销时修改

我也看了下这块的代码,可以参考下这个:https://github.com/HenryChenV/my-notes#synchronized%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90

DZQOX commented

@farmerjohngit 您好,我有一个疑问就是

  anticipated_bias_locking_value =
              (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
              ~((uintptr_t) markOopDesc::age_mask_in_place);

这里从klass的prototype_header中获取了一些信息,来判断偏向锁是否指向当前线程,epoch是否过期等,但是等偏向锁指向当前线程

if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {

后,并没有看见更新klass的prototype_header更新,那偏向锁重入就会有问题,请问prototype_header是在哪里更新的?

  1. 偏向锁偏向某个线程时不会修改prototype_header
  2. 偏向锁的重入和LockRecord有关,不知道问题在哪
  3. klass的prototype_header只会在批量重偏向和批量撤销时修改

我也看了下这块的代码,可以参考下这个:https://github.com/HenryChenV/my-notes#synchronized%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90

谢谢答疑。
对于您的第二点

偏向锁的重入和LockRecord有关,不知道问题在哪

我的疑问是,偏向锁偏向某个线程时不会修改prototype_header那么重入时下面的分支语句该怎么走?

if  (anticipated_bias_locking_value == 0) {
//分支1: 不会进入到这里,因为偏向锁偏向某个线程时不会修改prototype_header并没有指向当前线程,
prototype_headeranticipated_bias_locking_value !=0
...
}
else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
 // 分支2
...
}
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
   // 分支3
...
}
else {
// 分支4: 走到这里说明当前要么偏向别的线程,要么是匿名偏向(即没有偏向任何线程)
...
}

在没有修改prototype_header的时候,分支1肯定不会走到,而分支4按楼主描述是处理匿名偏向的,那会走到分支3还是分支2?

你好,有一个小的细节问题,想要请教一下。在锁撤销部分的偏向标志被关闭时的代码(就是这段else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {)进行了CAS
我不太理解为什么这里的CAS在构造header时需要去取prototype_header的相关信息,我感觉按理来说这里应该是尝试恢复无锁状态,那么应该和if(!success)中类似的方法去构造替换lockeet->mark_addr()的header才对。

简单来说,问题就是:在偏向模式关闭,则尝试撤销偏向锁时,为什么要替换lockee markword的,是class中的mark word,而不是构造一个无锁的header?

还望赐教。

@DanchuoZhong 偏向锁批量撤销 :当一个线程撤销次数20次时,会统一把这个对象 A a的类, 只要含有A这个类的锁,都撤销为无锁状态。CAS在构造header时需要去取prototype_header的相关信息,是去取类的信息,而不仅仅是对象a 的锁信息。lockeet->mark_addr()是取是对象的mark地址。

XHxin commented

请教大佬一个问题。
现在有一个场景:线程t1加锁然后释放(偏向锁),然后t2来加锁再释放(轻量锁),然后t3来加锁(轻量锁)。
当t2释放的时候,mark word应该是如下图紫色部分吧,接着t3来加锁
image
t3来加锁执行到源码中(如下图)的这个判断的时候,是会返回true吗?
image
我请教了别人,得到的回复如下图(红框部分),这使我很困惑。
image

如果t3(轻量锁)来加锁,这个判断是true的话,那它接着执行里面的多个判断,在当前场景下里面的if和else if不是都不成立吗?接着会执行else代码块
image
image

请大神解惑!感谢~

@XHxin
你好,对于mark->has_bias_pattern()是否为true,含义是 Java有没有把偏向延迟禁用;因为在dk6默认开启偏向锁,即程序刚启动创建的对象是不会开启偏向锁的,几秒后后创建的对象才会开启偏向锁的;
作者这里是假设禁用偏向延迟,如果是禁用偏向延迟,if(mark->has_bias_pattern()) 是为ture;
//关闭延迟开启偏向锁 -XX:BiasedLockingStartupDelay=0

有一个场景:线程t1加锁然后释放(偏向锁),然后t2来加锁再释放(轻量锁),然后t3来加锁(轻量锁);
t3进入
success=false,走轻量锁逻辑。

开局就没看懂= =
//entry不为null,代表还有空闲的Lock Record
为啥需要空闲的LockRecord

偏向锁Lock Record怎么生成的呀

请问代码注释里success==false的逻辑是不是有点问题?我的理解是只有对象头不是偏向锁状态或者class标记为不可偏向,才会导致success==false;如果偏向其他线程,虽然会调用monitorenter,但是success被修改成true,不会重新走success==false的流程

请问代码注释里success==false的逻辑是不是有点问题?我的理解是只有对象头不是偏向锁状态或者class标记为不可偏向,才会导致success==false;如果偏向其他线程,虽然会调用monitorenter,但是success被修改成true,不会重新走success==false的流程

我也是这么想的,偏向锁获取那段代码code 9的success=false只有一种情况就是klass的prototype_header中是否偏向+锁标志不是101,这种情况就会走code 9普通轻量级锁的路线,偏向其他线程的话应该走的是code 8里CAS失败的else部分。。。

看了文章有两个地方一直困惑我,麻烦请教一下:
lock record最开始是什么时候被put 到线程栈的?
偏向锁从内存低位置向上寻找空闲lock record,什么情况下会找不到?找不到的话会怎样?

必须给个👍 透彻!

你好,有个疑问想请教下,当A,B线程同时获取锁时,假设当前锁对象是匿名可偏向的,A线程CAS成功了,获取到锁了,B线程失败了,就会进入这里CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);假设是开启了偏向并且没有触发批量重偏向和撤销,最终会启动VM线程执行撤销偏向锁吧,那启动VM线程接下来怎么执行呢?是执行这里UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);再次获取锁么?谢谢了

轻量级锁Lock record的指针地址和重量级锁Object monitor的指针地址都是00结尾的二进制吗?即4的倍数。 本人Java,不懂请教。

你好,有个疑问想请教下,当A,B线程同时获取锁时,假设当前锁对象是匿名可偏向的,A线程CAS成功了,获取到锁了,B线程失败了,就会进入这里CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);假设是开启了偏向并且没有触发批量重偏向和撤销,最终会启动VM线程执行撤销偏向锁吧,那启动VM线程接下来怎么执行呢?是执行这里UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);再次获取锁么?谢谢了

会执行到这一块代码, 具体的作者也有些 revoke的逻辑在fast_enter里

RT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  ...
  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);
  }
  ...
IRT_END```

看了文章有两个地方一直困惑我,麻烦请教一下: lock record最开始是什么时候被put 到线程栈的? 偏向锁从内存低位置向上寻找空闲lock record,什么情况下会找不到?找不到的话会怎样?

得到答案了吗? 我也有点懵逼