JVM锁简介:偏向锁、轻量级锁和重量级锁

转自:https://www.aimoon.site/blog/2018/05/21/biased-locking/

 比较复杂,简略见另一篇:https://www.cnblogs.com/twoheads/p/10148598.html

JVM中的java对象头

注意:在没有特殊说明的情况下,都是32 bits为例。

上一小节主要介绍了java中synchronized关键字的使用方法,而在这一小节中将介绍一下synchronized 在JVM中的实现基础——java对象头中的Mark Word

表1   Java对象头的长度

内容说明备注
Mark Word 存储对象的Mark Word信息 -
Class Metadata Address 存储指向对象存储类型的指针 -
Array Length 数组的长度 只有数组对象有该属性

synchronized使用的锁是存放在Java对象头中的Mark Word中,OpenJDK中的markOop.hpp 头文件详细介绍了Mark Word的内容,下面将分析32 bits的JVM中的Mark Word的构成。

表2   32位JVM的Mark Word存储结构

锁状态23 bits2 bits4 bits1 bit2 bits
轻量级锁 指向栈中锁记录的指针 00
无锁状态 hash code 分代年龄 0 01
偏向锁 Thread ID epoch 分代年龄 1 01
重量级锁 指向监视器(monitor)的指针 10
GC标记 0 11

:最后两位为锁标记位,倒数第三位是偏向标记,如果是1表示是偏向锁;合并单元格的位数就是 该字段的位数,例如hash code共25(23+2)位。

另外,对于偏向锁,如果Thread ID = 0,表示未加锁

JVM锁的类型及对比

Java 1.6对synchronized进行了大幅度的优化,其性能也有了大幅度的提升。Java 1.6引入 “偏向锁”和“轻量级锁”的概念,减少了获得锁和释放锁的消耗。在Java 1.6之后,加上原有的重量 级锁,锁一共有4种状态,分别是:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态。锁只能 按照上述的顺序进行升级操作,锁只要升级之后,就不能降级。

下面将分别介绍一下偏向锁、轻量级锁和重量级锁,并探索一下偏向锁升级为轻量级锁(revoke bias) 的流程和轻量级锁升级为重量级锁(inflate)的流程。偏向锁、轻量级锁的状态转化及对象 Mark Word的关系如下图所示。图片来源:Synchronization and Object Locking文章中的配图

图1   偏向锁、轻量级锁的状态转化及对象Mark Word的关系

1. 偏向锁

偏向锁是Java 1.6新添加的内容,并且是jdk默认启动的选项,可以通过-XX:-UseBiasedLocking 来关闭偏向锁。另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0来关闭延迟。

如果JVM支持偏向锁,那么将按照下图所示的流程分配对象,加偏向锁。图片来源:Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing第3页的配图

图2   偏向锁中的Mark Word的状态转化图

注意:这是简化版的流程图,因为偏向锁的图中缺少了epoch字段。

1.1 偏向锁的加锁

如果JVM支持偏向锁,那么在分配对象时,分配一个可偏向而未偏向的对象(Mark Word的最后3位 为101,并且Thread ID字段的值为0)。

然后,当一个线程访问同步块并获取锁时,将通过CAS(Compare And Swap)来尝试将对象头中的 Thread ID字段设置为自己的线程号,如果设置成功,则获得锁,那么以后线程再次进入和退出 同步块时,就不需要使用CAS来获取锁,只是简单的测试一个对象头中的Mark Word字段中是否存储 着指向当前线程的偏向锁;如果使用CAS设置失败时,说明存在锁的竞争,那么将执行偏向锁的撤销操作 (revoke bias),将偏向锁升级为轻量级锁。

:代码请查看biasedLocking.cpp中的revoke_and_rebias方法

1.2 偏向锁的升级

下面结合代码(有缩减)分析一下偏向锁升级为轻量级锁的过程,这里暂时不考虑批量撤销偏向 (bulk revocation)的情况。详细代码请查看biasedLocking.cpp中的revoke_bias方法。 偏向锁的撤销操作需要在全局检查点(global safepoint)执行,在全局检查点上没有 线程执行字节码。

:偏向锁的撤销的入口函数是biasedLocking.cpp中的revoke方法, 之后会通过VMThread调用revoke_bias方法

static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias,
                            bool is_bulk, JavaThread* requesting_thread) {
  markOop mark = obj->mark();
  // 检查是否可偏向
  if (!mark->has_bias_pattern()) {
    return BiasedLocking::NOT_BIASED;
  }
  uint age = mark->age();
  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) {
    // 可偏向但是未偏向的情况
    // 可能的使用场景为:因计算hash code而撤销偏向
    if (!allow_rebias) {
      obj->set_mark(unbiased_prototype);
    }
    return BiasedLocking::BIAS_REVOKED;
  }
  // 判断对象现在偏向的线程是否还存在
  // 即对象头中Mark Word中Thread ID字段指向的线程是否存在
  bool thread_is_alive = false;
  if (requesting_thread == biased_thread) {
    // 请求的线程拥有偏向锁
    thread_is_alive = true;
  } else {
   // 请求的线程不拥有偏向锁,递归查询
    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) {
    if (allow_rebias) {
      obj->set_mark(biased_prototype);
    } else {
      obj->set_mark(unbiased_prototype);
    }
    return BiasedLocking::BIAS_REVOKED;
  }
  // 拥有偏向锁的线程仍然存活
  // 检查该线程是否拥有锁:
  //    如果拥有锁,那么需要升级为轻量级锁,然后将displaced mark word复制到线程栈中;
  //    如果不再拥有锁,如果允许重偏向,那么将mark word中的Thread ID 重新置0;
  //                 如果不允许重偏向,那么将mark work设置为无锁状态,即最后两位为01

  // cached_monitor_info 是该线程拥有的锁对象的信息,按照从加锁顺序的逆序排列
  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);
    if (mon_info->owner() == obj) {
      // Assume recursive case and fix up highest lock later
      markOop mark = markOopDesc::encode((BasicLock*) NULL);
      highest_lock = mon_info->lock();
      highest_lock->set_displaced_header(mark);
    }
  }
  if (highest_lock != NULL) {
    // 线程拥有锁
    // Fix up highest lock to contain displaced header and point
    // object at it
    highest_lock->set_displaced_header(unbiased_prototype);
    // Reset object header to point to displaced mark.
    // Must release storing the lock address for platforms without TSO
    // ordering (e.g. ppc).
    obj->release_set_mark(markOopDesc::encode(highest_lock));
  } else {
    // 线程不再拥有锁
    if (allow_rebias) {
      obj->set_mark(biased_prototype);
    } else {
      obj->set_mark(unbiased_prototype);
    }
  }
  return BiasedLocking::BIAS_REVOKED;
}

小结: 撤销偏向的操作需要在全局检查点执行。我们假设线程A曾经拥有锁(不确定是否释放锁), 线程B来竞争锁对象,如果当线程A不在拥有锁时或者死亡时,线程B直接去尝试获得锁(根据是否 允许重偏向(rebiasing),获得偏向锁或者轻量级锁);如果线程A仍然拥有锁,那么锁 升级为轻量级锁,线程B自旋请求获得锁。整个撤销偏向(revoke bias)的伪代码如下所示:

// 撤销流程的伪代码,在全局检查点执行该操作
if mark word 存储的不是可偏向状态:
    return;     // 如果不是偏向锁,那么没有撤销偏向的必要
else:
    if Thread ID 指向的线程不存活:
        if 允许重偏向:
            退回可偏向但未偏向的状态   // Thread ID为0
        else:
            偏向撤销,变为无锁状态
    else:
        if Thread ID 指向的线程,仍然拥有锁:
            升级为轻量级锁,将mark word复制到线程栈中,然后stack pointer指向最老的相关锁记录
        else:
            if 允许重偏向:
                退回可偏向但未偏向的状态   // Thread ID为0
            else:
                偏向撤销,变为无锁状态

下图中的线程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程:图片来源:并发编程网-偏向锁的获得和偏向流程

图3   偏向锁的获得和偏向流程

1.3 epoch字段解释

上面介绍了那么多,还有一小部分的内容没有介绍,就是Mark Word中的epoch字段,epoch 主要是用于辅助批量重偏向(bulk rebiasing)和批量偏向撤销(bulk revocation)操作。 下面先讲一下什么叫做批量重偏向和批量偏向撤销。

First, there are certain objects for which biased locking is obviously unprofitable, such as producer-consumer queues where two or more threads are involved. Such objects necessarily have lock contention, and many such objects may be allocated during a program’s execution. It would be ideal to be able to identify such objects and disable biased locking only for them.

————引自论文《Eliminating Synchronization-Related Atomic Operations
with Biased Locking and Bulk Rebiasing》第3页第4部分

意思就是说,某些操作,例如生产者和消费者队列,线程之间的竞争是不可避免的,这时候就不适合使用偏向锁。 我们需要识别这些情况,然后有选择的禁用偏向锁。此时显然不是只有一个锁,而是一个类或者该类的多个对象 作为锁,我们需要对这个类的所有对象都撤销偏向锁,这个优化操作,就叫做批量撤销偏向(bulk revocation`)

换句话说,就是在某一阶段,某个类的对象频繁发生偏向撤销的操作,当次数超过预定的阈值(threshold) 时,就批量撤销该类所有对象的偏向。

Second, there are situations in which the ability to rebias a set of objects to another thread is profitable, in particular when one thread allocates many objects and performs an initial synchronization operation on each, but another thread performs subsequent work on them.

————引自论文《Eliminating Synchronization-Related Atomic Operations
with Biased Locking and Bulk Rebiasing》第3-4页第4部分

意思是说,在某些情况下,例如一个线程先分配了一定数量的对象(属于同一类型),然后对这些对象 执行同步操作,在该线程操作完成后,另一个线程接着执行同步操作,但是这两个线程的操作是顺序 执行的,不交互,我们就可以将前一个线程拥有的偏向重偏向到后一个线程,这个优化操作,就叫做 批量重偏向(bulk rebiasing)。

HotSpot通过Heuristics这个类来统计某一时间段内某个类对象的撤销偏向和重偏向的次数。

那么如何对某一特定数据类型的所有对象进行统计?HotSpot刚开始采用的方案是:遍历对象堆。 但是当堆变得比较大的时候,其扩展性就比较差,为了解决这个问题,引入了epoch的概念, 用epoch这个字段来表示偏向是否有效。

每一个可偏向的数据类型(类)都有自己响应的epoch值。注意,这个epoch number属于类。 此时,一个线程拥有偏向锁,指的是:对象头中的Mark Word的Thread ID字段指向该想线程, 并且对象实例的epoch字段和类的epoch字段值相同,如果不相同,可以认为该对象处于可偏向但 未偏向的状态。这样就不需要遍历整个对象堆,提高了效率。

注意:批量重偏向的操作,仍然需要在全局检查点(global safepoint)执行。 在全局检查点, 批量重偏向的额外操作如下:

  1. 将类中的epoch number加1。
  2. 然后,扫描所有的线程栈,定位所有仍然拥有锁的类的对象,然后将这些对象的epoch字段值, 更新为类的epoch字段值。基于Heuristics方法的考虑,这些对象的偏向可能被撤销。

另外,在引入epoch字段之后,获取偏向锁的流程如下面的伪代码所示:

// Biased locking acquisition supporting epoch-based bulk rebiasing and revocation
void lock (Object* obj, Thread* t) {
    int lw = obj->lock_word;
    if (lock_state(lw) == Biased && biasable(lw) == obj->class->biasable
       && bias_epoch(lw) == obj->class->epoch) {
       if (lock_or_bias_owner == t->id) {
           // current thread is the bias owner
           return;
       } else {
           // need to revoke the object bias
           revoke_bias(obj, t);
       }
    } else {
       // normal locking/unlocking protocal,
       // possibly with bias acquisition.
    {
}

代码来源:引自论文Eliminating Synchronization-RelatedAtomic Operations with Biased Locking and Bulk Rebiasing第5页 listing 1 和 listing 2.

2. 轻量级锁

轻量级锁,也是JDK 1.6加入的新机制,之所以成为“轻量级”,是因为它不是使用操作系统互斥量来实现锁, 而是通过CAS操作,来实现锁。当线程获得轻量级锁后,可以再次进入锁,即锁是可重入(Reentrance Lock)的。

在轻量级锁的加锁阶段,如果线程发现对象头中Mark Word已经存在指向自己栈帧的指针,即线程 已经获得轻量级锁,那么只需要将0存储在自己的栈帧中(此过程称为递归加锁);在解锁的时候,如果发现锁记录的内容为0, 那么只需要移除栈帧中的锁记录即可,而不需要更新Mark Word。

2.1 轻量级锁的加锁和解锁过程

轻量级锁的加锁流程

线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建所记录的空间,用于存储对象头中的 Mark Word的拷贝(官方称之为Displaced Mark Word),如图4所示。图片来源:PPT The Hotspot Java Virtual Machine第 29 页

图4   轻量级锁CAS操作之前堆栈和对象的状态

然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录(Lock Record)的指针, 如图5所示。如果成功,当前线程获得轻量级锁,如果失败,虚拟机先检查当前对象头的Mark Word 是否指向当前线程的栈帧,如果指向,则说明当前线程已经拥有这个对象的锁,则可以直接进入同步块 执行操作,否则表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。当竞争线程的自旋次数 达到界限值(threshold),轻量级锁将会膨胀为重量级锁。

: 轻量级锁的获取过程请查看代码synchronizer.cpp中的slow_enter方法

图片来源:PPT The Hotspot Java Virtual Machine第 30 页

图5   轻量级锁CAS操作之后堆栈和对象的状态

轻量级锁的解锁流程

轻量级锁解锁时,如果对象的Mark Word仍然指向着线程的锁记录,会使用CAS操作, 将Dispalced Mark Word替换到对象头,如果成功,则表示没有竞争发生。如果失败, 表示当前锁存在锁竞争,锁就会膨胀为重量级锁。

: 轻量级锁的获取过程请查看代码synchronizer.cpp中的fast_exit方法, 由synchronizer.cpp中的slow_exit方法调用。

2.2 轻量级锁膨胀为重量级锁

下面结合代码(有缩减)分析一下轻量级锁膨胀为重量级锁的过程。详细代码请查看 synchronizer.cpp中的inflate方法。返回的是一个monitor对象。

ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
  for (;;) {
      const markOop mark = object->mark() ;

      // The mark can be in one of the following states:
      // *  Inflated     - 直接返回
      // *  Stack-locked - 需要膨胀为重量级锁
      // *  INFLATING    - 需要等待其他线程的锁膨胀操作完成
      // *  Neutral      - 无锁状态,膨胀为重量级锁
      // *  BIASED       - 非法状态,不可能存在

      // CASE: inflated
      // 如果已经膨胀为重量级锁,那么直接返回
      if (mark->has_monitor()) {
          ObjectMonitor * inf = mark->monitor();
          return inf;
      }

      // CASE: inflation in progress - inflating over a stack-lock.
      // 这个状态很快就消失(transient)
      // 有其他线程正在将轻量级锁转换为重量级锁,只有这个线程可以完成膨胀操作,
      // 其他线程必须等待膨胀完成。虽然是自旋操作,但不会一直占用cpu资源,
      // 每隔一段时间会通过os::NakedYield方法放弃cpu资源,或通过park方法挂起
      if (mark == markOopDesc::INFLATING()) {
         TEVENT (Inflate: spin while INFLATING) ;
         ReadStableMark(object) ;           // 自旋,等待膨胀完成
         continue ;
      }

      // CASE: stack-locked
      // 可能该线程或者其他线程拥有轻量级锁,需要膨胀为重量级锁
      if (mark->has_locker()) {
          ObjectMonitor * m = omAlloc (Self) ;
          // Optimistically prepare the objectmonitor - anticipate successful CAS
          // We do this before the CAS in order to minimize the length of time
          // in which INFLATING appears in the mark.
          m->Recycle();
          m->_Responsible  = NULL ;
          m->OwnerIsThread = 0 ;
          m->_recursions   = 0 ;
          m->_SpinDuration = ObjectMonitor::Knob_SpinLimit;

          markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(),
                                                       object->mark_addr(), mark) ;
          if (cmp != mark) {
             omRelease (Self, m, true) ;
             continue ;       // Interference -- just retry
          }

          // 此时已经成功将mark word的状态改为INFLATING状态

          // 从锁拥有者的栈帧中获取displaced mark word,然后设置monitor的各个字段,并返回
          markOop dmw = mark->displaced_mark_helper() ;
          m->set_header(dmw) ;
          m->set_owner(mark->locker());
          m->set_object(object);

          // Must preserve store ordering. The monitor state must
          // be stable at the time of publishing the monitor address.
          guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ;
          object->release_set_mark(markOopDesc::encode(m));

          // Hopefully the performance counters are allocated on distinct cache lines
          // to avoid false sharing on MP systems ...
          if (ObjectMonitor::_sync_Inflations != NULL) {
            ObjectMonitor::_sync_Inflations->inc() ;
          }
          TEVENT(Inflate: overwrite stacklock);
          return m ;
      }

      // CASE: neutral
      // 将锁膨胀为重量级锁
      ObjectMonitor * m = omAlloc (Self) ;
      // prepare m for installation - set monitor to initial state
      m->Recycle();
      m->set_header(mark);
      m->set_owner(NULL);           // 没有所有者
      m->set_object(object);
      m->OwnerIsThread = 1 ;
      m->_recursions   = 0 ;
      m->_Responsible  = NULL ;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;

      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m),
            object->mark_addr(), mark) != mark) {
          m->set_object (NULL) ;
          m->set_owner  (NULL) ;
          m->OwnerIsThread = 0 ;
          m->Recycle() ;
          omRelease (Self, m, true) ;
          m = NULL ;
          continue ;
          // interference - the markword changed - just retry.
          // The state-transitions are one-way, so there's no chance of
          // live-lock -- "Inflated" is an absorbing state.
      }

      // Hopefully the performance counters are allocated on distinct
      // cache lines to avoid false sharing on MP systems ...
      if (ObjectMonitor::_sync_Inflations != NULL){
        ObjectMonitor::_sync_Inflations->inc();
      }
      TEVENT(Inflate: overwrite neutral);
      return m;
  }
}

锁膨胀要分为多种情况进行分析:

  • Inflated:说明已经是膨胀锁了,直接返回。
  • Inflating:说明其他线程在执行膨胀锁的操作,自旋等待膨胀完成,该状态是为了保证 只有一个线程执行膨胀锁的操作。
  • Stack-locked:由轻量级锁膨胀为重量级锁,首先通过CAS将Mark Word置0,即变为INFLATING 状态,然后先生成一个monitor,然后通过CAS将Mark Word指向monitor
  • Neutral:无锁状态,膨胀为重量级锁,先生成一个monitor,然后通过CAS将Mark Word指向monitor

: 锁膨胀完成后,返回对应的monitor,并且仅仅是返回monitor,不涉及线程竞争加锁和解锁。 真正的加锁操作是enter,解锁操作是exit,加锁和释放的代码在objectMonitor.cpp中。图片来源:并发编程网-轻量级锁及膨胀流程图

图6   轻量级锁及膨胀流程图

3. 重量级锁

重量级锁(heavy weight lock),是使用操作系统互斥量(mutex)来实现的传统锁。 当所有对锁的优化都失效时,将退回到重量级锁。它与轻量级锁不同竞争的线程不再通过自旋来竞争线程, 而是直接进入堵塞状态,此时不消耗CPU,然后等拥有锁的线程释放锁后,唤醒堵塞的线程, 然后线程再次竞争锁。但是注意,当锁膨胀(inflate)为重量锁时,就不能再退回到轻量级锁。

重量级锁的加锁和释放的代码在objectMonitor.cpp中。 对于重量级锁的加锁和释放过程,我没有细看,因为涉及到底层的操作系统,看起来难道比较大。

4. 锁的对比

下面对偏向锁、轻量级锁和重量级锁进行比较:

表3   各种锁的优缺点及适用场景

优点缺点适用场景
偏向锁 加锁和解锁不需要额外的消耗,与执行非同步方法仅存在纳秒级的差距 如果线程间存在竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块的情况
轻量级锁 竞争的线程不会堵塞,提高了程序的响应速度 始终得不到锁的线程,使用自旋会消耗CPU 追求响应时间,同步块执行速度非常块,只有两个线程竞争锁
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程堵塞,响应时间缓慢 追求吞吐量,同步块执行速度比较慢,竞争锁的线程大于2个

表格来源:《Java并发编程的艺术》第16页

hashcode()方法对偏向锁的影响

hashcode()方法Object类中,是一个native方法,用于计算对象的哈希值,可以和equla() 方法配合来比较对象是否相等。这里插一句,不同的对象可能会生成相同的hashcode值,所以不能 只根据hashcode值判断两个对象是否是同一对象,但是如果两个对象的hashcode值不等,则必定是两个不同的对象。 也就是说在比较对象是否为同一对象的时候,先判断两个对象的hashcode值是否真正相等, 如果不相等,那么肯定不是同一对象。如果相等,然后使用equals方法,如果相等就是同一个对象。

另外,我们通常认为该方法返回的是对象的内存地址,其实这种说法是不太准确的。例如,在 JDK 1.8中,这种方法是完全错误的。

synchroizer.cpp中的get_next_hash()方法中,介绍了几种计算hashcode的算法,如下所示(有删减):

// hashCode() generation
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  intptr_t value = 0 ;
  if (hashCode == 0) {
     // 系统生成的随机数
     value = os::random() ;
  } else if (hashCode == 1) {
     // 对对象的内存地址进行二次计算
     // This can be useful in some of the 1-0 synchronization schemes.
     intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
     value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
  } else if (hashCode == 2) {
     value = 1 ;                            // 用于敏感性测试
  } else if (hashCode == 3) {
     value = ++GVars.hcSequence ;           // 自增的序列
  } else if (hashCode == 4) {
     value = cast_from_oop<intptr_t>(obj) ; // 对象的内存地址,转为int
  } else {
     // hashCode == 5
     // Marsaglia's xor-shift scheme with thread-specific state
     unsigned t = Self->_hashStateX ;
     t ^= (t << 11) ;
     Self->_hashStateX = Self->_hashStateY ;
     Self->_hashStateY = Self->_hashStateZ ;
     Self->_hashStateZ = Self->_hashStateW ;
     unsigned v = Self->_hashStateW ;
     v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
     Self->_hashStateW = v ;
     value = v ;
  }

  value &= markOopDesc::hash_mask;
  if (value == 0) value = 0xBAD ;
  assert (value != markOopDesc::no_hash, "invariant") ;
  TEVENT (hashCode: GENERATE) ;
  return value;
}

小结: 根据上面的代码,可以知道,有如下几种算法:

  • hashCode == 0: 系统生成的随机数
  • hashCode == 1: 对对象的内存地址进行二次计算
  • hashCode == 2: 硬编码1 (用于敏感性测试)
  • hashCode == 3: 一个自增的序列
  • hashCode == 4: 对象的内存地址,转为int
  • hashCode == 5: Marsaglia’s xor-shift scheme with thread-specific state

global.hpp中,我们可以看出,JDK 1.8使用的计算hashcode的算法为5, 也就是Marsaglia’s xor-shift scheme with thread-specific state (原谅我,不知道怎么翻译-_-//),所以hashcode的值与对象的内存地址,没有什么关系。

  product(intx, hashCode, 5, "(Unstable) select hashCode generation algorithm")

:可以使用参数-XX:hashCode=[0-5]来改变默认的算法。

下面结合代码(有删减,详细代码请查看synchroizer.cpp中的FastHashCode()方法), 分析一下,计算hashcode对锁状态的影响:

intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
  if (UseBiasedLocking) {
    // 如果使用偏向锁,那么撤销偏向锁
    if (obj->mark()->has_bias_pattern()) {
      // Box and unbox the raw reference just in case we cause a STW safepoint.
      // 导致STW发生,进入全局检查点
      Handle hobj (Self, obj) ;
      BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current());
      obj = hobj() ;
    }
  }

  // hashCode() is a heap mutator ...
  ObjectMonitor* monitor = NULL;
  markOop temp, test;
  intptr_t hash;
  markOop mark = ReadStableMark (obj);

  // 此时,已经不可能是偏向锁状态,那么还有3种状态:无锁,轻量级锁和重量级锁
  if (mark->is_neutral()) {
    // 此时是无锁状态,如果有hash code,那么直接返回,否则进行计算
    hash = mark->hash();              // this is a normal header
    if (hash) {                       // if it has hash, just return it
      return hash;
    }
    hash = get_next_hash(Self, obj);  // allocate a new hash code
    temp = mark->copy_set_hash(hash); // merge the hash code into header
    // 将计算的hash code使用CAS操作,拷贝到mark word中
    test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);
    if (test == mark) {
      return hash;
    }
    // 如果CAS操作失败,那么需要膨胀为重量级锁
  } else if (mark->has_monitor()) {
    // 此时是重量级锁
    monitor = mark->monitor();
    temp = monitor->header();
    hash = temp->hash();
    if (hash) {
      return hash;
    }
    // Skip to the following code to reduce code size
  } else if (Self->is_lock_owned((address)mark->locker())) {
    // 此时是轻量级锁状态,查看displaced mark word中是否含有hash code
    // 如果没有,那么膨胀为重量级锁
    temp = mark->displaced_mark_helper();
    hash = temp->hash();      // by current thread, check if the displaced
    if (hash) {               // header contains hash code
      return hash;
    }
  }

  // 膨胀为重量值锁,然后设置hash code
  monitor = ObjectSynchronizer::inflate(Self, obj);     // 锁膨胀
  // Load displaced header and check it has hash code
  mark = monitor->header();
  hash = mark->hash();
  if (hash == 0) {
    hash = get_next_hash(Self, obj);
    temp = mark->copy_set_hash(hash); // merge hash code into header
    test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
    if (test != mark) {
      hash = test->hash();
    }
  }
  // We finally get the hash
  return hash;
}

小结: 根据上面的代码可以知道,在HotSpot, 调用hashCode(), 或者System.identityHashCode() 方法会导致对象撤销偏向锁。换句话说,如果在明显存在竞争的情况下,例如生产者/消费者队列, 可以通过简单的调用hashCode()或者System.identityHashCode()方法来禁用偏向锁。

参考文献

  1. 深入理解java虚拟机, 周志明.
  2. Java并发编程的艺术, 方腾飞, 魏鹏, 程晓明.
  3. Kenneth Russell, David Detlefs. Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing
  4. Galo Navarro. How does the default hashCode() work?
  5. Paul Hohensee. The Hotspot Java Virtual Machine, p25-35.
  6. Thomas Kotzmann, Christian Wimmer. Synchronization and Object Locking
  7. Dave Dice. Biased Locking in HotSpot
  8. ross. Java并发之彻底搞懂偏向锁升级为轻量级锁
  9. OpenJDK 1.8 虚拟机部分源代码:markOop.hpp、 biasedLocking.cpp、 basicLock.cpp、 synchronizer.cppobjectMonitor.cpp
  10. 占小狼.JVM源码分析之synchronized实现
posted @ 2018-12-20 16:14  twoheads  阅读(7020)  评论(0编辑  收藏  举报