从源码看世界:偏向锁从入门到放弃

前言

最近跟架构师讨论java应用级别的基础镜像时有一项jvm参数引起我的注意: -XX:-UseBiasedLocking ,即关闭偏向锁,是的,你没看错,标题的“从入门到放弃”不是开玩笑,是真的放弃。当时有点懵B这不是jvm的锁优化其中一项么,即使请教了架构师后仍然不够清晰,因此自己研究了一翻后,给大家汇报一下我的成果,望大神们多提意见

建议观看文章前先了解以下内容:

  • synchronized的使用
  • hotspot对synchronized的优化
  • hashcode的使用

以下是该文章脑图:

为何诞生,又为何放弃

为何诞生?

Biased locking is an optimization technique used in the HotSpot Virtual Machine to reduce the overhead of uncontended locking. It aims to avoid executing a compare-and-swap atomic operation when acquiring a monitor by assuming that a monitor remains owned by a given thread until a different thread tries to acquire it. The initial lock of the monitor biases the monitor towards that thread, avoiding the need for atomic instructions in subsequent synchronized operations on the same object. When many threads perform many synchronized operations on objects used in a single-threaded fashion, biasing the locks has historically led to significant performance improvements over regular locking techniques.[1]

翻译成人话就是以前我们synchronized加锁时,经常是单线程反复进入同步区域,基于此提出偏向锁可以减少同步操作从而提高性能。

为什么会得出单线程操作这个条件呢?

首先要了解偏向锁是在java 6引入的锁优化,又联想到早期原始的同步集合(Hashtable和Vector)以及字节输入读取流(ByteArrayInputStream和StringReader)均大量使用到synchronized修饰方法,尽管在使用过程中并没有多线程争用却依然需要走一遍monitor创建与释放过程,此时提出的偏向锁和轻量级锁正是为了提高不同场景下性能的优化手段

既然是优化为何又要放弃呢?

自从java 5开始提供了更多粒度更细的线程安全集合和操作,理论上要么无锁要么一定程度上出现锁竞争,此时再等偏向锁升级就有点浪费资源了,当然根本原因还是在于其实现的细节,下面我们一起来了解下偏向锁的实现原理

偏向锁实现

名词

  • 无锁:不支持偏向锁的初始状态
  • 偏向锁:偏向于特定线程获取的锁
  • 可偏向:支持偏向锁的初始状态
  • 重偏向:清除旧的线程信息并支持重新偏向
  • 匿名偏向:被重偏向清除掉偏向线程信息的状态
  • 偏向锁撤销:非偏向线程尝试抢占锁对象时进行的行为

认识对象头

了解锁之前绕不开对象头结构,这是源码注释中给的对象头结构(建议从右到左观察):

// markOop.hpp #47,64位
unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
size:64 ----------------------------------------------------->| (CMS free block)

单纯的看上图,还是显得十分抽象,贴心的 openjdk 官网提供了可以查看对象内存布局的工具 JOL (java object layout)

<dependency>
    <groupid>org.openjdk.jol</groupid>
    <artifactid>jol-core</artifactid>
    <version>0.16</version>
</dependency>

以下是使用例子:

public static void main(String[] args) throws InterruptedException {
    Test0 o = new Test0();
    System.out.println("未进入同步块,MarkWord 为:");
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    System.out.println("start");
    synchronized(o) {
        System.out.println(("进入同步块,MarkWord 为:"));
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
    System.out.println(("退出同步块,MarkWord 为:"));
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    System.out.println("end");
}

执行结果如下:

作者批注:thin lock是轻量级锁。

咦,在认知中 不是 先偏向锁再升级到轻量锁 吗?

延迟偏向

If biased locking is enabled, schedule a task to fire a few seconds into the run which turns on biased locking for all currently loaded classes as well as future ones. This is a workaround for startup time regressions due to a large number of safepoints being taken during VM startup for bias revocation. Ideally we would have a lower cost for individual bias revocation and not need a mechanism like this. // BiasedLocking::init#92

作者批注:这也是我一直推崇的注释方式:注释不仅要解释what,更重要的是解释why

通过参数-XX:BiasedLockingStartupDelay进行设置,默认4000ms

验证例子:

public static void main(String[] args) throws InterruptedException {
	Test0 o = new Test0();
	Thread.sleep(5000L);
	// ……

执行结果如下:

延迟偏向实现

偏向锁的实现都在 biasedLocking.cpp 里,而延迟偏向具体实现则在 init() 中:

if (UseBiasedLocking) {
  if (BiasedLockingStartupDelay &gt; 0) {
    EnableBiasedLockingTask* task = new EnableBiasedLockingTask(BiasedLockingStartupDelay);
    task-&gt;enroll();
  } else {
    VM_EnableBiasedLocking op(false);
    VMThread::execute(&amp;op);
  }
}
作者批注:task-&gt;enroll()可以理解跟java Thread的start()一样

EnableBiasedLockingTask 实现了虚方法task,里面创建 VM_EnableBiasedLocking 对象,通过 VMThread 执行,而 VM_EnableBiasedLocking 的核心正是将所有已加载的类捞出来并把它头属性改为偏向锁,因此延迟偏向之后创建的对象自然均为偏向锁了

有没有一种可能,在定时任务正在执行时正好有个类需要加载,这样是不是会逃过被修改呢?

放心,这个hotspot也考虑到了:

// vmThread.cpp #681
if (op->evaluate_at_safepoint() && !SafepointSynchronize::is_at_safepoint()) {
  // 设置安全点
  SafepointSynchronize::begin();
  op->evaluate();
  SafepointSynchronize::end();
}
作者批注:通过定时任务的方式给了我们一些启发,为了追求快速启动无法将所有事情都在启动时处理,利用此方式补充加载必要信息

那延迟偏向之前加载的类创建出来的对象是什么锁?

第一个例子已经给了答案,其实是所以新创建的对象都是从无锁开始的,具体代码如下:

// Klass::Klass() #203
set_prototype_header(markOopDesc::prototype());

static markOop prototype() {
  // 设置空的hashcode和无锁
  return markOop( no_hash_in_place | no_lock_in_place );
}

那延迟偏向之后呢?

初始化完无锁后,在后面 SystemDictionary::update_dictionary 进行修改:

// systemDictionary.cpp 2172
if (UseBiasedLocking && BiasedLocking::enabled()) {
  if (k->class_loader() == class_loader()) {
    k->set_prototype_header(markOopDesc::biased_locking_prototype());
  }
}

调用链如图:

偏向过程

首先,所谓的偏向锁是指锁会偏向于 第一个获取它 的线程

If the bias pattern is present, the contents of the rest of the header are either the JavaThread* of the thread to which the lock is biased, or NULL, indicating that the lock is "anonymously biased". The first thread which locks an anonymously biased object biases the lock toward that thread. If another thread subsequently attempts to lock the same object, the bias is revoked. // biasedLocking.hpp 56

举个例子,水瓶座的女生第一次谈恋爱后,她的心就会一直装着初恋,之后不管分手多少次只要初恋提出复合都会答应,直到出现男二这种设定就会被打破

重点:即强调的是空间竞争,而非时间竞争

验证例子:

public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000L);
        Test2 o = new Test2();
        System.out.println("未进入同步块,MarkWord 为:");
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        synchronized(o) {
            System.out.println(("主线程进入同步块,MarkWord 为:"));
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
        Thread.sleep(2000L);
        new Thread(() -> {
            synchronized(o) {
                System.out.println(("另一个线程进入同一个对象同步块,MarkWord 为:"));
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }, "another thread").start();
    }

执行结果:

下面从源码中分析偏向加锁过程(即初恋获取女生的心的过程)

因HotSpot使用模板解释器,monitorenter指令的入口在TemplateTable::monitorenter,但模板解释器都是汇编代码,不易读,经大神指点后从bytecodeInterpreter字节码解释器学习过中原理

// bytecodeInterpreter.cpp #1816 (仅列出关键代码)
CASE(_monitorenter): {
// 找到一个空闲的监视器或者一个已经分配给这个对象的监视器
BasicObjectLock* entry = NULL;
while (most_recent != limit ) {
  if (most_recent->obj() == NULL) entry = most_recent;
  else if (most_recent->obj() == lockee) break;
  most_recent++;
}
// 存在空闲的Lock Record
if (entry != NULL) {
  // 将Lock Record的obj指针指向锁对象
  entry->set_obj(lockee);
  int success = false;
  // 可偏向
  if (mark->has_bias_pattern()) {
    // 对象头、类信息、线程标识位操作
    anticipated_bias_locking_value =
      (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
      ~((uintptr_t) markOopDesc::age_mask_in_place);
    if  (anticipated_bias_locking_value == 0) {
      // 已经偏向当前线程,啥也不用做
      success = true;
    }
    else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
      // 该类已不支持偏向,尝试撤销偏向
      markOop header = lockee->klass()->prototype_header();
      if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
    }
    else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
      // epoch过期,重新偏向
      markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
      if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
      }
      else {
        // cas失败说明存在竞争,进入锁升级
        CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
      }
      success = true;
    }
    else {
      // 尝试偏向该线程,只有匿名偏向能成功
      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));
      markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
      if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
      }
      else {
        // cas失败说明存在竞争,进入锁升级
        CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
      }
      success = true;
    }
  }
  if (!success) {
  // 偏向锁失败,升级锁
  }
} else {
  // 没拿到lock record,重新执行
  istate->set_msg(more_monitors);
  UPDATE_PC_AND_RETURN(0);
}

当男二想要得到女生的心时,遇到以下几种情况:

  • case 1:初恋已扑街
  • case 2:初恋仍在,但已经单方面放弃女生
  • case 3:初恋仍在,并且仍与女生拍拖

case 1和2很简单,直接追求女生即可,虽然女生不会马上答应并且答应后也不保证长厢厮守;但case 3就比较麻烦了,需要约上初恋干一架,这就需要等到初恋赴约才能进行了,这就是非偏向线程尝试抢占对象时进行的偏向撤销,具体逻辑在 biasedLocking.revoke_bias

static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread, JavaThread** biased_locker) {
  markOop mark = obj->mark();
  // 不支持偏向
  if (!mark->has_bias_pattern()) {
    return BiasedLocking::NOT_BIASED;
  }
  JavaThread* biased_thread = mark->biased_locker();
  if (biased_thread == NULL) {
    // 初恋已扑街
    if (!allow_rebias) {
      obj->set_mark(unbiased_prototype);
    }
    return BiasedLocking::BIAS_REVOKED;
  }
  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;
  }
  // 到达这里证明初恋仍存活,并且仍与女生拍拖
  // 从初恋中查出最近获得女生的心的记录,这里由于不在男二的控制范围内,为了保证初恋不受其它事情干扰,需要安全点
  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) {
    // 跟初恋干架
    highest_lock->set_displaced_header(unbiased_prototype);
    obj->release_set_mark(markOopDesc::encode(highest_lock));
    assert(!obj->mark()->has_bias_pattern(), "illegal mark state: stack lock used bias bit");
  } else {
    if (allow_rebias) {
      obj->set_mark(biased_prototype);
    } else {
      // Store the unlocked value into the object's header.
      obj->set_mark(unbiased_prototype);
    }
  }
  return BiasedLocking::BIAS_REVOKED;
}

可以看到撤销过程需要安全点保证,导致撤销成本比较高,如果创建了大量对象并执行了初始的同步操作,然后在其它线程中竞争这些对象,此时会导致大量的偏向锁撤销操作。那是否有两全其美的方案呢?

批量撤销与重偏向

这点hotSpot也考虑到了:

We implement "bulk rebias" and "bulk revoke" operations using a "bias epoch" on a per-data-type basis. If too many bias revocations are occurring for a particular data type, the bias epoch for the data type is incremented at a safepoint, effectively meaning that all previous biases are invalid. The fast path locking case checks for an invalid epoch in the object header and attempts to rebias the object with a CAS if found, avoiding safepoints or bulk heap sweeps (the latter which was used in a prior version of this algorithm and did not scale well). If too many bias revocations persist, biasing is completely disabled for the data type by resetting the prototype header to the unbiased markOop. // biasedLocking.hpp 56

简单来说就是利用计数器判断短期内撤销偏向的级别,根据级别进行重偏向或永久撤销,下面通过例子验证

同一个线程抢占30次:

public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000L);
    // 首先我们创建一个list,来存放锁对象
    List<Test3> list = new LinkedList<>();
    // 加锁对象数
    int count = 30;
    // 线程1先抢占一次锁
    new Thread(() -> {
        for (int i = 0; i < count; i++) {
            Test3 test = new Test3();
            list.add(test);
            synchronized (test) {
                System.out.println("第" + (i + 1) + "次加锁-线程1");
                System.out.println(ClassLayout.parseInstance(test).toPrintable()); // 打印对象头信息
            }
        }
    }, "线程1").start();
    // 让线程1跑一会儿
    Thread.sleep(2000);
    // 线程2重抢线程1的锁
    new Thread(() -> {
        for (int i = 0; i < count; i++) {
            Test3 test = list.get(i);
            synchronized (test) {
                System.out.println("第" + (i + 1) + "次加锁-线程2");
                System.out.println(ClassLayout.parseInstance(test).toPrintable()); // 打印对象头信息
            }
        }
    }, "线程2").start();
    // 让线程2跑一会儿
    Thread.sleep(2000);
    System.out.println("新建的对象头:");
    System.out.println(ClassLayout.parseInstance(new Test3()).toPrintable());
    LockSupport.park();
}

执行结果:

线程1部分结果:

线程2前19次部分结果:

线程2第20次以后部分结果:

线程2之后新建的对象头:

三角恋玩腻了,这次我们玩四角恋,即引入线程3抢线程2的锁,并且将加锁次数增加到40次:

public static void main(String[] args) throws InterruptedException {
        int count = 40;
        // 与上面代码一致
        // 让线程2跑一会儿
        Thread.sleep(2000);
        // 线程3抢20次锁
        new Thread(() -> {
            for (int i = 20; i < count; i++) {
                Test4 test = list.get(i);
                synchronized (test) {
                    System.out.println("第" + (i + 1) + "次加锁-线程3");
                    System.out.println(ClassLayout.parseInstance(test).toPrintable()); // 打印对象头信息
                }
            }
        }, "线程3").start();
        // 让线程3跑一会儿
        Thread.sleep(2000);
        System.out.println("最后新建出来的对象头:");
        System.out.println(ClassLayout.parseInstance(new Test4()).toPrintable());
}

线程3的结果很显然,前面已经证明过了:

然而线程3之后新建出来的对象却很有意思,居然不再支持偏向锁:

这正是上面提到的利用计数器控制锁级别,而它的实现在 biasedLocking.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();
  if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
      (revocation_count <  BiasedLockingBulkRevokeThreshold) &&
      (last_bulk_revocation_time != 0) &&
      (cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
    // 进入重偏向之后需要在BiasedLockingDecayTime内计数,否则重置;BiasedLockingDecayTime默认25秒
    k->set_biased_lock_revocation_count(0);
    revocation_count = 0;
  }
  // 未到达永久撤销时自增,默认40次
  if (revocation_count <= BiasedLockingBulkRevokeThreshold) {
    revocation_count = k->atomic_incr_biased_lock_revocation_count();
  }
  // 到达20次进行重偏向
  if (revocation_count == BiasedLockingBulkRevokeThreshold) {
    return HR_BULK_REVOKE;
  }
  // 到达40次进行永久撤销
  if (revocation_count == BiasedLockingBulkRebiasThreshold) {
    return HR_BULK_REBIAS;
  }
  return HR_SINGLE_REVOKE;
}

从源码得知永久撤销需要在一段时间内达到撤销次数,假设上面的四角恋例子开头把线程2之后的睡眠时间增加到30秒会发生什么呢? 这个留给读者们来尝试吧

另外细心的同学应该会发现,在“三角恋”最后新建的对象头尽管仍然支持偏向,但数值却是0x0000000000000105,与初始值0x0000000000000005不一样,翻查头信息对应的位置是epoch等于1, 那这个epoch字段的作用是什么呢? 我们可以从注释中了解一二:

Indicates epoch in which this bias was acquired. If the epoch changes due to too many bias revocations occurring, the biases from the previous epochs are all considered invalid. // markOopDesc.hpp#185

由于对象的头对象都是继承自class,epoch也不例外。当发生批量重偏向时,class的epoch会自增一,同时遍历所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。

// BiasedLocking::bulk_revoke_or_rebias_at_safepoint#351
if (bulk_rebias) {
  // 该类支持偏向
  if (klass->prototype_header()->has_bias_pattern()) {
    // 获取原epoch
    int prev_epoch = klass->prototype_header()->bias_epoch();
    // 自增1
    klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
    int cur_epoch = klass->prototype_header()->bias_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()) {
          // 设置新值
          owner->set_mark(mark->set_bias_epoch(cur_epoch));
        }
      }
    }
  }
}

那什么时候才会出来对象的epoch与class的不一致呢? 自然是在批量偏向之前创建且不在加锁状态的对象了。

性能对比

讲了这么多理论,下面来尝试下开启与关闭偏向的例子来进行性能测试

运行环境:

  • 机器:MacBook Pro
  • CPU:2 GHz 四核Intel Core i5
  • 内存:32 GB 3733 MHz

测试代码:

public static void main(String[] args) throws InterruptedException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 首先我们创建一个list,来存放锁对象
        List<List<Test8>> list = new LinkedList<>();
        int count = 100;
        for (int i = 0; i < count; i++) {
            list.add(new LinkedList<>());
            for (int j = 0; j < count; j++) {
                list.get(i).add(new Test8());
            }
        }
        // 一共开启200个线程,每个线程对100个对象进行加锁,每个对象被2个线程互相竞争
        for (int o = 0; o < 2; o++) {
            for (int i = 0; i < count; i++) {
                int finalI = i;
                new Thread(() -> {
                    for (int j = 0; j < count; j++) {
                        Test8 test = list.get(finalI).get(j);
                        synchronized (test) {
                            System.out.println("第" + (j + 1) + "次加锁-线程" + j);
                        }
                    }
                }, "线程" + i).start();
            }
        }
        // 让线程跑一会儿
        Thread.sleep(1000);
        stopWatch.stop();
        System.out.println("运行耗时:" + (stopWatch.getTotalTimeMillis() - 1000));
    }

执行12次的结果:

结论:去掉最长与最短的耗时,支持偏向的平均值为 108.6 ,不支持偏向的平均值为 97.7 ,大概提升10%

数据因环境而异,仅供参考

总结

完整的偏向流程:

偏向锁最终被放弃的原因:

  • 撤销锁成本大,需要等待安全点
  • 逻辑过于复杂,并且到处都耦合偏向锁的逻辑,后期维护成本高
  • 编程思维的改变,锁范围的缩小与锁竞争的频率增加都会导致偏向失效,反而降低程序性能

偏向锁撤销需要安全点原因:对象头需要存储 线程信息 (方便偏向),因此需要在线程栈帧中遍历寻找lock record,在非safepoint时栈帧是 动态

偏向锁适用范围:单一线程 反复进入 同步块

思维扩展

看到这里也许读者仍然有不少疑问:既然偏向锁撤销如此复杂,那轻量锁就没有这种烦恼吗,为什么不直接升级到重量锁?偏向锁需要将线程ID写入对象头,此时与hashcode空间冲突,是否会导致hashcode的变化?下面我们来一一探究

轻量锁过程

在偏向加锁过程中了解到锁升级发生在 InterpreterRuntime::monitorenter 里,沿着方法一路追踪可以看到轻量加锁过程在 ObjectSynchronizer::slow_enter 、解锁过程在 ObjectSynchronizer::fast_exit ,下面我们一起来了解下

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  // 当前为无锁状态
  if (mark->is_neutral()) {
    // 保存锁对象的头信息到lock record
    lock->set_displaced_header(mark);
    // CAS把lock record写到锁对象的头信息
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ...
  } else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    // 头信息已经有lock record且正是当前线程,无需保存头信息
    lock->set_displaced_header(NULL);
    return;
  }
  // 走到这里证明仍然有锁竞争,重要膨胀成重量级锁
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  markOop dhw = lock->displaced_header();
  markOop mark ;
  if (dhw == NULL) {
     // 头信息为空,啥也不用干,断言可能遇到的状态
     // Diagnostics -- Could be: stack-locked, inflating, inflated.
     return ;
  }
  mark = object->mark() ;
  // 真正解锁过程,利用CAS还原头信息
  if (mark == (markOop) lock) {
     if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
        TEVENT (fast_exit: release stacklock) ;
        return;
     }
  }
  // CAS失败就膨胀
  ObjectSynchronizer::inflate(THREAD,
                              object,
                              inflate_cause_vm_internal)->exit(true, THREAD);
}

总结:从轻量级锁过程可以看出,整个加锁过程也十分简单,解锁过程也仅仅将头信息还原,锁撤销时在对象头即可找到lock record,中途没有与 其它线程或栈空间 有关系,并且只要有竞争也是马上升级,因此无需使用安全点。同时, 轻量级锁没有自旋过程 ,所谓的自适应旋转等待过程其实发生在重量级锁中。

这里留个疑问给读者:轻量级锁是如何解决重入锁的释放

存在hashcode的对象加锁

经过前面的学习可以确定的是hashcode并不是在创建对象时就马上生成,而是要经过第一次调用 Object::hashCode() 或者 System::identityHashCode(Object) 才会存储在对象头中,并且生成后就不应该变更,此时如果进行加锁会发生什么呢?下面用例子来验证

public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000L);
    Test6 o = new Test6();
    System.out.println("未进入同步块,MarkWord 为:");
    System.out.println(ClassLayout.parseInstance(o).toPrintable());

    o.hashCode();
    System.out.println(("已生成 hashcode,MarkWord 为:"));
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    synchronized(o) {
        System.out.println(("进入同步块,MarkWord 为:"));
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

执行结果:

假设先进入偏向锁再调用hashcode又会发生什么呢?

public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000L);
    Test7 o = new Test7();
    System.out.println("未进入同步块,MarkWord 为:");
    System.out.println(ClassLayout.parseInstance(o).toPrintable());

    synchronized(o) {
        System.out.println(("进入同步块,MarkWord 为:"));
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        o.hashCode();
        System.out.println(("已偏向状态下,生成 hashcode,MarkWord 为:"));
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

执行结果:

实现以上逻辑的源码都来自于ObjectSynchronizer::FastHashCode,下面我们一起来窥探其原理:

intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
  if (UseBiasedLocking) {
    // 当前对象支持偏向
    if (obj->mark()->has_bias_pattern()) {
      Handle hobj (Self, obj) ;
      // 撤销偏向,因此再进入加锁时升级到轻量级锁(case 1)
      BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current());
      obj = hobj() ;
    }
  }

  ObjectMonitor* monitor = NULL;
  markOop temp, test;
  intptr_t hash;
  markOop mark = ReadStableMark (obj);
  // 无锁状态
  if (mark->is_neutral()) {
    hash = mark->hash();
    // 已存在hashcode就返回
    if (hash) {
      return hash;
    }
    // 不存在就计算
    hash = get_next_hash(Self, obj);
    temp = mark->copy_set_hash(hash);
    // CAS到头信息里
    test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);
    if (test == mark) {
      return hash;
    }
  } else if (mark->has_monitor()) {
    // 重量级锁
    monitor = mark->monitor();
    temp = monitor->header();
    hash = temp->hash();
    if (hash) {
      return hash;
    }
  } else if (Self->is_lock_owned((address)mark->locker())) {
    // 轻量级锁
    temp = mark->displaced_mark_helper();
    assert (temp->is_neutral(), "invariant") ;
    hash = temp->hash();
    if (hash) {
      return hash;
    }
  }

  // 都拿不到时就需要膨胀锁(case 2)
  monitor = ObjectSynchronizer::inflate(Self, obj, inflate_cause_hash_code);
  mark = monitor->header();
  hash = mark->hash();
  if (hash == 0) {
    // 计算hashcode
    hash = get_next_hash(Self, obj);
    temp = mark->copy_set_hash(hash);
    // CAS到头信息
    test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
    if (test != mark) {
      hash = test->hash();
    }
  }
  return hash;
}

总结:因hashcode的空间与偏向锁冲突,为了 保证hashcode不变 不得不撤销偏向;若撤销时仍然持有锁则升级为轻量级锁,但此时还没生成hashcode,只好再次膨胀成重量级锁,个人认为这种做法是为了 防止hashcode保存前锁被升级了导致数据丢失

这里也留个疑问给读者:轻量/重量级锁是如何保证hashcode生成后不会变

授之以渔

又到了大家最喜欢的环节了,可能大家会问: 平时我怎样才能学到更多的知识,了解更多的技术呢? 这里分享一下本人的做法:

  • 你不知道你所不知道的事情:看别人的分享
  • 你知道你所不知道的事情:看源码(注释),验证别人的结论
  • 你不知道为什么:找碴,不这么干的后果
  • 你知道为什么:空杯,以自己的理解重构
  • 你优化存在的问题:是否挖出更大的坑,业界又是如何解决

Refernces

[1] JEP 374: Deprecate and Disable Biased Locking.  https://openjdk.org/jeps/374 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这是一个使用Java语言编写的应用程序的命令行运行指令。该程序是一个消息队列中间件的Broker,使用了RocketMQ框架实现。其中的参数含义如下: -server:使用JVM的server模式。在多核CPU上提高性能。 -Xms2g:JVM启动时堆内存的最小值为2G。 -Xmx2g:JVM堆内存的最大值为2G。 -XX:+UseG1GC:使用G1垃圾回收器。 -XX:G1HeapRegionSize=16m:G1垃圾回收器内存区域的大小为16M。 -XX:G1ReservePercent=25:预留25%的空间以避免太满引发的性能问题。 -XX:InitiatingHeapOccupancyPercent=30:G1在堆内存使用达到30%时会触发垃圾回收。 -XX:SoftRefLRUPolicyMSPerMB=0:清除软引用的时间间隔为0,即软引用的对象一旦没有被使用就会被立即清除。 -verbose:gc:打印GC日志。 -Xloggc:/dev/shm/rmq_srv_gc_%p_%t.log:将GC日志输出到/dev/shm/rmq_srv_gc_%p_%t.log文件中。 -XX:+PrintGCDetails:打印GC详细信息。 -XX:+PrintGCDateStamps:打印GC时间戳。 -XX:+PrintGCApplicationStoppedTime:打印应用程序停止时间。 -XX:+PrintAdaptiveSizePolicy:打印自适应策略的信息。 -XX:+UseGCLogFileRotation:启用GC日志文件轮换。 -XX:NumberOfGCLogFiles=5:GC日志文件轮换时保留的文件数目。 -XX:GCLogFileSize=30m:GC日志文件的大小为30M。 -XX:-OmitStackTraceInFastThrow:关闭快速抛出异常时的栈信息。 -XX:+AlwaysPreTouch:在JVM启动时预先分配堆内存。 -XX:MaxDirectMemorySize=15g:最大直接内存大小为15G。 -XX:-UseLargePages:不使用大页面。 -XX:-UseBiasedLocking:不使用偏向锁。 -Drocketmq.client.logUseSlf4j=true:使用SLF4J作为日志框架。 -c ../conf/broker.conf:指定Broker的配置文件路径。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值