性能文章>Synchronized之轻量级锁自旋骗局>

Synchronized之轻量级锁自旋骗局原创

3年前
52431112

        之前笔者分析了synchronized的偏向锁源码,我们今天继续来看synchronized的轻量级锁逻辑。关于轻量级锁,网上有很多说法都是轻量级锁在发生竞争时会进行自旋,但是经过笔者对源码的学习,并没有发现轻量级锁的自旋逻辑。笔者甚至去jdk6和jdk15中都进行了一番搜索,发现也不存在自旋的逻辑,关于轻量级锁的自旋这个说法,笔者曾经也深信不疑,但是从实际源码中出发,并不存在自旋的逻辑。本篇博客笔者会把整个轻量级锁部分的源码拿出来进行分析,希望能帮助大家以后正确的理解synchronized轻量级锁。

一.偏向锁撤销

        我们都知道synchronized中的轻量级锁是由偏向锁升级而来的(关于偏向锁的源码在笔者之前的博客:https://my.oschina.net/u/3645114/blog/5306767中)。所以轻量级锁源码的入口必然在偏向锁后面,事实上再偏向锁升级成轻量级锁之前,还需要进行偏向锁的撤销,偏向锁加锁的方法在interp_masm_x86_64.cpp的lock_object方法中,我们就从这里开始,话不多说,直接看代码:

void InterpreterMacroAssembler::lock_object(Register lock_reg) {
  if (UseHeavyMonitors) {
    call_VM(noreg,
            CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
            lock_reg);
  } else {
    Label done;

    const Register swap_reg = rax; // Must use rax for cmpxchg instruction
    const Register obj_reg = c_rarg3; // Will contain the oop

    const int obj_offset = BasicObjectLock::obj_offset_in_bytes();
    const int lock_offset = BasicObjectLock::lock_offset_in_bytes ();
    const int mark_offset = lock_offset +
                            BasicLock::displaced_header_offset_in_bytes();

    Label slow_case;

    movptr(obj_reg, Address(lock_reg, obj_offset));
    //这里是偏向锁逻辑,关于偏向锁的这部分逻辑笔者之前的博客已经分析过了,这里就不进行分析了
    if (UseBiasedLocking) {
      biased_locking_enter(lock_reg, obj_reg, swap_reg, rscratch1, false, done, &slow_case);
    }
    // 之前的博客提到过撤销偏向和对象非偏向模式(即已经是轻量级锁)会走这里——升级轻量级锁
    // 这里偏向锁的撤销(Revoke) 操作并不是将对象恢复到无锁可偏向的状态
    // 而是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态
    // 偏向锁的撤销,是轻量级锁的前提。
    // 将1写入swap_reg寄存器
    movl(swap_reg, 1);

    //将对象的markword 与1或运算并存入swap_reg寄存器
    //此处锁标记位是 01 即无锁,因为执行到这里只有撤销偏向锁和对象非偏向锁两种情况
    //这两种情况下都为00 | 01,即得无锁
    orptr(swap_reg, Address(obj_reg, 0));

    //将swap_reg中的数据存到lockrecord中的markword位置
    //这里将lockrecord中的displaced header(本质是一个markword)设置为无锁的原因是
    //等到解锁时会将displaced header中的markword替换回对象头上
    //这时对象应该是无锁的.
    movptr(Address(lock_reg, mark_offset), swap_reg);

    //汇编lock指令
    if (os::is_MP()) lock();
    //比较交换(cas) 将对象头替换成指向lockrecord的指针
    cmpxchgptr(lock_reg, Address(obj_reg, 0));

    //成功则表示已经是轻量级锁且加锁成功直接结束,失败则证明有竞争(可能是偏向锁竞争也可能是轻量级锁竞争)
    //进入slow_case逻辑
    jcc(Assembler::zero, done);

    ......

    bind(slow_case);

    //slow_case逻辑
    call_VM(noreg,
            CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
            lock_reg);

    bind(done);
  }
}

关于slow_case,笔者之前的博客也提到过,其实是只要发生锁的竞争,就会进入到slow_case部分,我们可以看到,这里的竞争不只是偏向锁竞争,也包括轻量级锁竞争。

还有一点需要注意的,从代码中我们可以看到,持有轻量级锁的lockRecord中的displaced header,即lockRecord中的锁对象(关于这个锁对象,笔者之前的博客也提到过,这里就不展开)按照许多网上的说法,这里应该是保存对象的markword,而轻量级锁对象的markword锁标记位应该是00,但实际上源码在这里将其还原成了无所状态保存到了lockRecord,原因就如笔者注释所说——为了方便解锁时替换回对象的markword

我们继续看源码:call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),lock_reg) 这个方法(这里是模板解释器调用了c++的方法,后面的源码都是c++相对来说比较好理解一些)可以理解为是调用 InterpreterRuntime::monitorenter方法,其参数为lock_reg寄存器,即之前找到的lockRecord:

//调用了interpreterRuntime.cpp下的方法
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  ......
  //elem是传入的lockRecord,将线程和lockRecord中的obj封装成句柄
  Handle h_obj(thread, elem->obj());
  //判断jvm偏向锁参数
  if (UseBiasedLocking) {
    //会先进入快速处理方法,参数是刚刚封装的句柄和lockRecord中的锁对象
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  ......
IRT_END

//继续看ObjectSynchronizer::fast_enter方法
//attempt_rebias参数表示是否接受重偏向,这里是true
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 {
      //安全点撤销
      BiasedLocking::revoke_at_safepoint(obj);
    }
 }
 //轻量级锁竞争逻辑
 slow_enter (obj, lock, THREAD) ;
}

 到这里我们可以看到会先执行偏向锁的撤销和重偏向逻辑,然后会根据结果判断是否进入轻量级锁竞争逻辑。我们一步一步来,先看偏向锁的撤销和重偏向逻辑,而偏向锁的撤销和重偏向逻辑又分为两个分支,一个是在全局安全点,一个是在非全局安全点。这两个方法其实逻辑是差不多的,我们重点分析非全局安全点的方法:

//撤销偏向锁 attempt_rebias参数表示是否会重偏向这里传入的是true
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
 
  markOop mark = obj->mark();
  //判断是偏向模式,但是尚未偏向其他线程,这里attempt_rebias是true所以不会执行
  if (mark->is_biased_anonymously() && !attempt_rebias) {
    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();
    //判断类不是偏向模式,即现在是处于批量撤销延迟阶段,需要修复对象的markword恢复成klass的markword非偏向模式
    if (!prototype_header->has_bias_pattern()) {
      //将类的markword cas替换到对象的markword,撤销偏向锁
      //逻辑还是cas替换,就不再详细分析
      markOop biased_value       = mark;
      markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
      return BIAS_REVOKED;
    //如果类的epoch不等于对象的epoch,表明偏向已经过期
    } else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
      //进行重偏向,通常再汇编代码中完成,但是到这里是在运行期间,所以任何时刻可能会产生偏向过期
      //逻辑还是cas替换,就不再详细分析
      if (attempt_rebias) {
        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;
        }
      //因为attempt_rebias是true所以这个分支不会进入
      } 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;
        }
      }
    }
  }
  //这个方法会判断是否需要批量撤销和批量重偏向,这里不进行展开了,有兴趣的读者可以自己展开分析
  //主要会返回几个状态
  //enum HeuristicsResult {
  //    HR_NOT_BIASED    = 1,  不需要偏向
  //    HR_SINGLE_REVOKE = 2,  单个撤销
  //    HR_BULK_REBIAS   = 3,  批量重偏向
  //    HR_BULK_REVOKE   = 4   批量撤销
  // };
  HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
   //不需要偏向
  if (heuristics == HR_NOT_BIASED) {
    return NOT_BIASED;
  //单个撤销
  } else if (heuristics == HR_SINGLE_REVOKE) {
    Klass *k = obj->klass();
    markOop prototype_header = k->prototype_header();
    //线程是当前线程,且没有过期,直接撤销
    if (mark->biased_locker() == THREAD &&
        prototype_header->bias_epoch() == mark->bias_epoch()) {
      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);
      return cond;
    } else {
      //说明是其他线程持有锁,必须等到安全性点执行,声明一个任务类
      VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
      VMThread::execute(&revoke);
      return revoke.status_code();
    }
  }
  
  //批量撤销或重偏向任务类,根据 (heuristics == HR_BULK_REBIAS) 这个条件判断是否是批量撤销或者重偏向
  VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
                                (heuristics == HR_BULK_REBIAS),
                                attempt_rebias);
  VMThread::execute(&bulk_revoke);
  return bulk_revoke.status_code();
}

看到这里大家可能对批量撤销和重偏向操作有点疑惑,笔者先说明下关于批量撤销和批量重定向的意义:

1.当一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。

批量重偏向机制是为了解决这种场景
 
2.存在明显多线程竞争的场景下还使用偏向锁是不合适的,会产生大量的偏向锁加锁撤销和升级
 
批量撤销则是为了解决这第二种场景
 
批量操作实现的原理其实是在每个klass(class)中维护一个偏向锁计数器,每一次该klass(class)的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20,jvm参数BiasedLockingBulkRebiasThreshold控制)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
当达到重偏向阈值后,假设该kalss(class)计数器继续增长,当其达到批量撤销的阈值后(默认40,jvm参数BiasedLockingBulkRevokeThreshold控制,JVM就认为该klass(class)的使用场景存在多线程竞争,会先标记该klass(class)为非偏向模式即无锁状态,执行批量撤销(在这里就会产生批量撤销延迟,即此时有些对象是偏向模式,但是其klass是非偏向模式,当其进行加锁的时候就会通过一次cas修复makrword将其修复成非偏向模式直接走轻量级锁逻辑),之后,对于该class的锁,直接走轻量级锁的逻辑。
 
关于批量操作的过程其实就是对单一操作的for循环,jvm会遍历所有java线程,并遍历线程中的栈帧获取其中的lockRecord,判断每个lockRecord中保存的对象是否是当前klass(class)类型,如果是则进行撤销。具体代码也比较简单,笔者再这里就不进行展开。

二.轻量级锁竞争

        我们继续看轻量级锁的竞争方法ObjectSynchronizer::slow_enter:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  //判断是否是无锁(中性)
  //到这里无锁状态有两种情况:
  //1.偏向锁撤销
  //2.曾经是轻量级锁被释放了
  if (mark->is_neutral()) {
    //替换lockrecord中的displaced_header为对象的markword
    lock->set_displaced_header(mark);
    //cas替换对象markword为lockRecord地址,成功则返回证明获取轻量级锁成功,失败则进行锁膨胀逻辑
    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())) {
    //重入直接设置displaced_header为null并返回
    //表示添加一个Lock Record来表示锁的重入
    lock->set_displaced_header(NULL);
    return;
  }
  //锁膨胀逻辑
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
到这里轻量级锁的加锁逻辑就结束了,熟悉轻量级锁的朋友们可能会疑惑:
1.轻量级锁竞争只有一次cas么?就这样结束了?传说中的自旋等待呢?
        这里就是笔者所说的轻量级锁骗局了,从源码中看轻量级锁并不存在自旋逻辑,其竞争锁的逻辑简单到只有一次cas操作。
 
2.这样操作岂不是就和偏向锁一样了,为什么还要用轻量级锁呢?
        首先轻量级锁设计之初是为了应对线程之间交替获取锁的场景,而偏向锁的场景则是用于一个线程不断获取锁的场景。通过源码我们可以看出当一个线程获取偏向锁后,这个锁就会永久偏向这个线程,因为一旦发生偏向锁撤销,就代表锁要升级成为轻量级锁。虽然偏向锁在加锁时会进行一次cas操作,但是后续的获取只会进行简单的判断,不会再进行cas操作。但是轻量级锁的加锁和释放都需要进行cas操作。
 
        我们看下如果把轻量级锁使用在偏向锁的场景下对比:
        我们可以看到轻量级锁情况下每次获取都需要进行加锁和释放,每次加锁和释放都会进行cas操作,所以单个线程获取锁的情况使用偏向锁效率更高。
        
        在看下如果把偏向锁使用在轻量级锁的场景下对比:
        除了第一次偏向锁加锁,以后每次偏向锁加锁时都要触发偏向锁的撤销逻辑,通过刚刚的源码分析我们也可以看到如果线程B执行到偏向锁代码块时进行撤销偏向锁的行为因为锁是线程A持有的,所以需要等到全局安全点才可以进行撤销操作,而且撤销时也会进行一次cas操作。所以其效率肯定不如直接使用轻量级锁,轻量级锁尽管需要每次加锁后都需要释放,但是其在这种场景下不需要等到全局安全点,在此场景下相对于偏向锁更优。
 
3.轻量级锁不使用自旋不会影响效率么?
        这个问题我们可以看轻量级锁的设计场景,其是为了应对线程之间交替获取锁的场景。如果是线程交替获取锁的场景,就不会存在获取锁时cas竞争失败,如果发生cas竞争失败,则必然不是交替获取锁的场景,而是竞争更加激烈的场景,这个时候就需要锁继续膨胀升级了。所以轻量级锁竞争没有自旋的原因其实是其设计并不是用于处理过于激烈的竞争场景。
 

三.总结

        通过对源码的学习,我们可以看到轻量级锁和偏向锁是用于处理不同场景的锁,之前大家可能都认为轻量级锁在竞争失败后会自旋,然后多次尝试再获取锁,其实通过本次源码学习,我们可以看到,并没有自旋的逻辑。希望大家再看过本篇博客后,可以正确的认识轻量级锁,明白其设计意义和应对的场景。

 
 
 
点赞收藏
自成溪
请先登录,查看11条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

随机一门技术分享之Netty

随机一门技术分享之Netty

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

12
11