偏向锁原理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了偏向锁原理相关的知识,希望对你有一定的参考价值。

参考技术A

本文介绍偏向锁相关原理,并不限定于Java中的偏向锁,但是Java中偏向锁的实现也是相同的原理,本文主要是对参考文献( Quickly Reacquirable Locks )中偏向锁实现重点部分的翻译,加入了自己的理解,参考文献称偏向锁为可快速获取的锁(QRL,Quickly Reacquirable Locks)。如何快速获取将会在第2节介绍过相关数据结构之后介绍。偏向锁主要是为了提高绝大部分情况下不存在竞争、只有一个线程在尝试获取锁的场景,通过相关数据结构可以减少CAS操作的数量,提高应用性能。偏向锁在有多个线程竞争获取时,会变成(论文中称为revoke,撤销)普通的锁(或默认锁),在Java中则会变先变为轻量级锁。

相对于一般的锁来说,偏向锁使用了两个额外的域(这里不能等同于Java类成员域,在Java中其实是使用对象头中的Mark Word保存的)。第一个域是用来记录锁当前状态的域,可能的状态包括 NEURAL , BIASED , REVOKING , DEFAULT 。

注意:撤销和释放(release)锁不同,这里的撤销(revoke)指的是从偏向锁退化为普通锁的过程。

初始状态下锁处于 NEURAL 状态,当该锁被第一次获取时,获取锁的线程会将锁状态变为 BIASED 。此时如果有另一个线程尝试获取同一个锁,那么其最终会将锁状态变为 DEFAULT ,在变为 DEFAULT 状态之前该锁会根据撤销协议先处于一个暂时的中间状态 REVOKING 。当锁状态为 BIASED 时,状态中会有一个域用来记录当前持有锁的线程标识符(后文使用线程ID代替)。对于一个不可再偏向(non-rebiasable,表示一旦撤销之后不可再次回到偏向状态)的锁,则锁状态只能按照 NEURAL -> BIASED -> REVOKING -> DEFAULT 的状态进行切换,不可回退。但是偏向锁也可实现为可重偏向的锁(rebiasable)。

实现偏向锁需要额外增加的第二个域是一个可以指示布尔类型的位(bit)即可,用来表示偏向锁当前持有者对锁是获取状态还是释放状态。有了这个标识,当偏向锁被获取并且没有撤销时,当前持有者线程的获取和释放操作只要简单的改变该域的状态即可。

注意,一个线程获取偏向锁首先需要成为偏向锁的持有者(holder),即将自己的线程ID通过CAS写入锁状态域中,然后再获取锁(acquire),持有并不一定获取了锁,持有之后必须改变上面布尔标识位才获取了锁,但是获取锁之前必须要先成为锁的持有者;释放(release)锁之后也需要有其他线程显示竞争之后当前持有者才会失去持有者身份,锁的持有者可以通过改变上面介绍的布尔位进行锁的获取和释放动作。

将锁从初始的 NEUTRAL 状态转变为 BIASED 状态可以仅通过一次CAS操作完成。这里使用CAS操作是必须的,因为需要避免多个线程同时尝试获取 NEUTRAL 状态锁并置其状态为 BIASED 。

现在我们介绍第1节概述中 快速获取 的含义,根据上面的数据结构介绍,当一个线程获取了偏向锁之后,会在锁中记录线程ID,锁中也会有一个标识位用来表示该锁目前是释放的还是被获取的。因此以后线程在获取和释放锁时,只要检测锁是否记录自己的线程ID即可,如果检测成功,表示线程已经是偏向锁的持有者,直接通过改变上述布尔类型标识位域进行获取和释放锁即可。如果测试失败(表示该锁目前持有者不是自己),则再测试一下锁当前状态,如果还是 BIASED 状态,则使用CAS竞争锁,如果CAS成功,则尝试使用CAS将锁状态中线程ID置为自己的线程ID,这样后续在获取和释放锁时就不用使用CAS操作了。上面测试过程没有使用CAS操作,因此提高了性能,实现了 快速获取 锁。

实现上面atomic-free(表示尽可能减少CAS这样的原子操作)偏向锁的难点就在于如何协调获取偏向锁和撤销偏向锁的过程。必须处理偏向锁获取和释放同时发生偏向锁撤销时的多线程竞争问题,可以通过使用CAS将锁的状态改为 REVOKING 来避免后一种竞争:通过CAS将锁状态改为 REVOKING 也可能会有多线线程同时进行,但是CAS保证只有一个线程会成功改变锁状态,称为真正的撤销者(revoker),其他的线程则尝试着获取默认锁(默认锁即偏向锁撤销完成之后变成 DEFAULT 状态的锁)。

下面介绍偏向锁操作过程(获取、释放、撤销等)的四种主要场景:

参考文献后面还介绍了可再偏向(rebiasable)锁的实现原理,即在合适的时机再次尝试将锁从 DEFALUT 状态置为 BIASED 状态,这里不再介绍,有兴趣可以看看参考文献。

Dave D., Villiam S.; Quickly Reacquirable Locks

synchronize偏向锁底层实现原理

1 偏向锁的意义

无多线程竞争时,减少不必要的轻量级锁执行路径。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一条线程去多次获得锁,为了让线程获得锁的性能代价更低而引入了偏向锁。

偏向锁主要用来优化同一线程多次申请同一个锁的竞争,即当对象被当做同步锁并有一个线程抢到了锁时,则在Mark Word设置该线程的线程ID、是否偏向锁设置1、锁标志位设置01等信息,此时的Mark Word 存储的就是偏向锁状态信息。

在:

  • 创建一个线程并在线程中执行循环监听的场景下

  • 或单线程操作一个线程安全集合时

同一线程每次都需获取和释放锁,每次操作都会发生用户态与内核态的切换。

获取偏向锁的场景:

在自己的线程栈生成一条Lock Record,然后Object Reference指向对象头,此时Lock Record与对象头就建立了联系:

① : 先判断Mard Word的Thread ID是否有值

  • 没有,则表示当前资源没有被其他线程占用,把当前线程ID等信息记录到Mark Word(这需CAS,可能多条线程修改Mark Word,需要保证原子性)
  • 有,则表示当前资源被线程占用,需要判断该线程是不是自己
    • 该线程ID是自己的,则表示可重入,直接获取(此时在自己的线程栈中继续生成一条新的Lock Record)
    • 该线程ID不是自己的,说明出现其他线程竞争,当前持有偏向锁的线程就需要撤销了,即当其他线程尝试获取偏向锁才释放锁

轻量级锁的获取及释放依赖多次的CAS操作,而偏向锁只依赖一次CAS置换ThreadID

一旦出现多个线程竞争时必须撤销偏向锁,所以:

撤销偏向锁消耗的性能必须 < 之前节省下来的CAS原子操作的性能消耗

不然得不偿失!

JDK6默认开启偏向锁,可通过-XX:-UseBiasedLocking禁用偏向锁。

2 偏向锁的获取

偏向锁的入口,synchronizer.cpp 文件的

ObjectSynchronizer::fast_enter

BiasedLocking::revoke_and_rebias实现

2.1 markOop mark = obj->mark()

获取对象的markOop数据mark,即对象头的Mark Word

2.2 判断mark是否为可偏向状态

mark的偏向锁的锁标志位为 01

2.3 判断mark中JavaThread的状态

  • 若指向当前线程,则执行同步代码块
  • 若为空,则走4
  • 若指向其它线程,则走5

2.4 执行CAS原子指令

设置mark中JavaThread为当前线程ID。

若CAS成功,则执行同步代码块,否则走5。

2.5 执行CAS失败

说明当前存在多个线程竞争锁,当达到全局安全点(safepoint),获得偏向锁的线程就会被挂起,撤销偏向锁,并升级为轻量级锁。

升级完成后被阻塞在安全点的线程继续执行同步代码块。

BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) 
  assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");

  // We can revoke the biases of anonymously-biased objects
  // efficiently enough that we should not cause these revocations to
  // update the heuristics because doing so may cause unwanted bulk
  // revocations (which are expensive) to occur.
  // step1
  markOop mark = obj->mark();
  if (mark->is_biased_anonymously() && !attempt_rebias) 
    // We are probably trying to revoke the bias of this object due to
    // an identity hash code computation. Try to revoke the bias
    // without a safepoint. This is possible if we can successfully
    // compare-and-exchange an unbiased header into the mark word of
    // the object, meaning that no other thread has raced to acquire
    // the bias of the object.
    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();
    if (!prototype_header->has_bias_pattern()) 
      // This object has a stale bias from before the bulk revocation
      // for this data type occurred. It's pointless to update the
      // heuristics at this point so simply update the header with a
      // CAS. If we fail this race, the object's bias has been revoked
      // by another thread so we simply return and let the caller deal
      // with it.
      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;
     else if (prototype_header->bias_epoch() != mark->bias_epoch()) 
      // The epoch of this biasing has expired indicating that the
      // object is effectively unbiased. Depending on whether we need
      // to rebias or revoke the bias of this object we can do it
      // efficiently enough with a CAS that we shouldn't update the
      // heuristics. This is normally done in the assembly code but we
      // can reach this point due to various points in the runtime
      // needing to revoke biases.
      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;
        
      
    
  

  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()) 
      // A thread is trying to revoke the bias of an object biased
      // toward it, again likely due to an identity hash code
      // computation. We can again avoid a safepoint in this case
      // since we are only going to walk our own stack. There are no
      // races with revocations occurring in other threads because we
      // reach no safepoints in the revocation path.
      // Also check the epoch because even if threads match, another thread
      // can come in with a CAS to steal the bias of an object that has a
      // stale epoch.
      ResourceMark rm;
      if (TraceBiasedLocking) 
        tty->print_cr("Revoking bias by walking my own stack:");
      
      EventBiasedLockSelfRevocation event;
      BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD, NULL);
      ((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
      assert(cond == BIAS_REVOKED, "why not?");
      if (event.should_commit()) 
        event.set_lockClass(k);
        event.commit();
      
      return cond;
     else 
      EventBiasedLockRevocation event;
      VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
      VMThread::execute(&revoke);
      if (event.should_commit() && (revoke.status_code() != NOT_BIASED)) 
        event.set_lockClass(k);
        // Subtract 1 to match the id of events committed inside the safepoint
        event.set_safepointId(SafepointSynchronize::safepoint_counter() - 1);
        event.set_previousOwner(revoke.biased_locker());
        event.commit();
      
      return revoke.status_code();
    
  

  assert((heuristics == HR_BULK_REVOKE) ||
         (heuristics == HR_BULK_REBIAS), "?");
  EventBiasedLockClassRevocation event;
  VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
                                (heuristics == HR_BULK_REBIAS),
                                attempt_rebias);
  VMThread::execute(&bulk_revoke);
  if (event.should_commit()) 
    event.set_revokedClass(obj->klass());
    event.set_disableBiasing((heuristics != HR_BULK_REBIAS));
    // Subtract 1 to match the id of events committed inside the safepoint
    event.set_safepointId(SafepointSynchronize::safepoint_counter() - 1);
    event.commit();
  
  return bulk_revoke.status_code();

3 偏向锁的撤销

只有当其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

偏向锁的撤销由BiasedLocking::revoke_at_safepoint实现:

void BiasedLocking::revoke_at_safepoint(Handle h_obj) 
  assert(SafepointSynchronize::is_at_safepoint(), "must only be called at safepoint");
  oop obj = h_obj();
  HeuristicsResult heuristics = update_heuristics(obj, false);
  if (heuristics == HR_SINGLE_REVOKE) 
    revoke_bias(obj, false, false, NULL, NULL);
   else if ((heuristics == HR_BULK_REBIAS) ||
             (heuristics == HR_BULK_REVOKE)) 
    bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
  
  clean_up_cached_monitor_info();

  1. 偏向锁的撤销动作必须等待全局安全点(safepoint,GC时会让所有线程阻塞的停顿点)
  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  3. 撤销偏向锁,恢复到无锁(标志位 01)或轻量级锁(标志位 00)状态

偏向锁在Java 1.6后默认启用,但在应用程序启动几s后才激活,可关闭延迟:

-XX:BiasedLockingStartupDelay=0

若确定应用程序中所有锁通常情况下处于竞争状态,可关闭偏向锁:

XX:-UseBiasedLocking=false(默认打开)

偏向锁的释放

遍历线程栈的所有Lock Record,把ObjectReference切断,即ObjectReference = null.

把ObjectReference置null,但锁对象的对象头的Mark Word还是没改变,依然偏向之前的线程,那还是没释放锁的嘛,的确是,线程退出临界区时候,并没有释放偏向锁,这么做是为 : 当再次需要获取锁时,只需要简单判断是否是重入,即可快速获取锁,而不用每次都CAS,这也是偏向锁在只有一个线程访问锁的情景下高效的核心。

总结

  • 当出现锁资源访问的时候,都会在当前线程栈生成一条Lock Record,并且ObjectReference将指向锁对象的对象头 的Mark Word,该设置可能出现多线程,需CAS操作
  • 多线程情况下竞争同一个锁资源,偏向锁的撤销会影响效率
  • 偏向锁的重入计数依靠线程栈里Lock Record个数
  • 偏向锁撤销失败,最终会升级为轻量级锁
  • 偏向锁退出时并没有修改Mark Word,也就是没有释放锁
  • 偏向锁相对轻量级锁来说,当同一线程去再次获取锁的时候,不用进行CAS操作,提高了性能.(轻量级锁在同一线程情况下每次去获取锁,在无锁的状态下,每次都要进行一次CAS操作)
  • 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁
  • 偏向锁的撤销是很复杂,成为理解代码的障碍,也阻碍了对同步系统重构,而且现如今基本都是多核系统,偏向锁的劣势越来越明显,所以在Java 15废弃了偏向锁

以上是关于偏向锁原理的主要内容,如果未能解决你的问题,请参考以下文章

synchronize偏向锁底层实现原理

synchronized原理及1.6之后的锁升级优化

Java偏向锁实现原理(Biased Locking)

Java锁synchronized关键字学习系列之偏向锁升级

Java synchronized原理

synchronized实现原理及其优化-(自旋锁,偏向锁,轻量锁,重量锁)