本文要点
本文主要讲的是synchronized 锁膨胀的原理,会从源代码上面向大家展开锁膨胀的逻辑和从源代码上面来推敲设计者为什么要这样做。
synchronized 分析
因为网上已经有比较多的文章讲整个synchronized的分析,所以这篇文章主要专注回答几个点。
在看下面例子前,我们先看下整个markword 在64位系统里面是如何表达的。
synchronized的偏向锁性能是否优于轻量级锁
在源码分析前首先看如下代码:
*/
public class OneSingleThread {
public static void main(String[] args) {
OneSingleThread oneSingleThread = new OneSingleThread();
long before = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
synchronized (oneSingleThread) {
}
}
long after = System.currentTimeMillis();
System.out.println(after - before);
}
}
第一次运行的时候:我们关闭偏向锁
-XX:-UseBiasedLocking -XX:BiasedLockingStartupDelay=0
第二次运行的时候vm 参数
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
从实验对比我们能发现在单线程情况下,偏向锁的性能还是有提高的,大概非偏向锁,100w次运行,性能能优化26毫秒。但是这里有一个伪命题,就是我们为什么要在循环里面用锁,当这个锁进入释放,能够达到100w次(同一个线程),是不是我们程序本身应该优化。
synchronized的偏向锁什么情况会做膨胀为轻量级锁
package com.test;
import org.openjdk.jol.info.ClassLayout;
/**
* @Author: 彭雨佳
* @Date: 2021/12/5 11:53 上午
*/
public class TwoThreadDemo {
public static TwoThreadDemo lock = new TwoThreadDemo();
public static void main(String[] args) {
Thread a = new Thread() {
@Override
public void run() {
System.out.println("thread 1 锁前" + ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
System.out.println("thread 1 锁中" + ClassLayout.parseInstance(lock).toPrintable());
}
System.out.println("thread 1 解锁后 " + ClassLayout.parseInstance(lock).toPrintable());
}
};
Thread b = new Thread() {
@Override
public void run() {
System.out.println("thread 2 锁前" + ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
System.out.println("thread 2 锁中" + ClassLayout.parseInstance(lock).toPrintable());
}
System.out.println("thread 2 解锁后 " + ClassLayout.parseInstance(lock).toPrintable());
}
};
a.start();
//保证线程1执行完,再执行线程2
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
b.start();
}
}
上面的代码我们模拟了下线程的交替运行情况。
我们启动参数同样是如下:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
thread 1 锁前com.test.TwoThreadDemo 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
thread 1 锁中com.test.TwoThreadDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 60 83 59 (00000101 01100000 10000011 01011001) (1501782021)
4 4 (object header) f7 7f 00 00 (11110111 01111111 00000000 00000000) (32759)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
thread 1 解锁后 com.test.TwoThreadDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 60 83 59 (00000101 01100000 10000011 01011001) (1501782021)
4 4 (object header) f7 7f 00 00 (11110111 01111111 00000000 00000000) (32759)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
thread 2 锁前com.test.TwoThreadDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 60 83 59 (00000101 01100000 10000011 01011001) (1501782021)
4 4 (object header) f7 7f 00 00 (11110111 01111111 00000000 00000000) (32759)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
thread 2 锁中com.test.TwoThreadDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 98 8a 9c 06 (10011000 10001010 10011100 00000110) (110922392)
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
thread 2 解锁后 com.test.TwoThreadDemo 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
通过打印出来的信息,我们可以看出,线程1在执行的过程中使用的偏向锁,而在线程2的时候因为该锁已经偏向了线程1,这个时候就会膨胀为轻量级锁。
总结:
从上述现象我们可以观察到,什么样的情况下能用到偏向锁了?
只有在开启了偏向锁的时候,且当前锁对象未偏向任何线程,或者偏向的线程为自己的时候,才会继续使用偏向锁。
什么又是批量重偏向了
可以看下下面这个例子
package com.test;
import org.openjdk.jol.info.ClassLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
/**
* @Author: 彭雨佳
* @Date: 2021/12/2 8:42 下午
*/
public class BatchBiase {
static Thread A;
static Thread B;
static int loopFlag = 20;
public static void main(String[] args) {
final List<BatchBiase> list = new ArrayList<>();
A = new Thread() {
@Override
public void run() {
for (int i = 0; i < loopFlag; i++) {
BatchBiase object = new BatchBiase();
list.add(object);
// System.out.println("A加锁前第" + i + " 次" + ClassLayout.parseInstance(object).toPrintable());
synchronized (object) {
// System.out.println("A加锁中第"+i+ "次" + ClassLayout.parseInstance(object).toPrintable());
}
System.out.println("A加锁结束第"+i+"次" + ClassLayout.parseInstance(object).toPrintable());
}
System.out.println("============线程A 都是偏向锁=============");
LockSupport.unpark(B);
}
};
B = new Thread() {
@Override
public void run() {
//防止竞争 先睡眠线程B
LockSupport.park();
for (int i = 0; i < loopFlag; i++) {
BatchBiase object = list.get(i);
//因为从list当中拿出都是偏向线程A
System.out.println("B加锁前第"+i+" 次" + ClassLayout.parseInstance(object).toPrintable());
synchronized (object) {
//20次撤销偏向锁偏向线程A;然后升级轻量级锁指向线程B线程栈当中的锁记录
//后面的发送批量偏向线程B
// System.out.println("B加锁中第"+i+" 次" + ClassLayout.parseInstance(object).toPrintable());
}
//因为前19次是轻量级锁,释放之后为无锁不可偏向
//但是第20次是偏向锁 偏向线程B 释放之后依然是偏向线程B
System.out.println("B加锁结束第"+i+" 次" + ClassLayout.parseInstance(object).toPrintable());
}
}
};
A.start();
B.start();
}
}
上面程序主要做的一件事,就是对同一个class的多个对象进行加减锁,
A线程运行完之后,B线程再运行
可以看到当B线程运行到第20个循环的时候,B线程又获取到了偏向锁。
这是为什么了,我们带着疑问来看jvm到底是如何处理的。
synchronized源码解析
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
}
}
上面的逻辑主要关注两个点,就是获取偏向锁成功的条件
- epoch没有过期,锁对象的偏向的线程是自己的时候,则直接获取锁成功,直接返回。
- epoch 过期了, 且锁对象是支持偏向锁模式的,那么尝试cas。
- 锁对象未偏向任何锁即匿名偏向的时候,尝试cas。
其余情况下都会走入撤销偏向锁的逻辑
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
//这个方法不能在saftpoint的时候调用
assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");
//获取锁对象的对象头
markOop mark = obj->mark();
//判断mark是否为可偏向状态,且没有偏向该线程
//attempt_rebias 为false的会走入下个流程
if (mark->is_biased_anonymously() && !attempt_rebias) {
//hash code 会找到这里来,从而破坏掉可偏向状态
markOop biased_value = mark;
//创建一个非偏向的markword
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
//通过cas的方式将非偏向的状态给这个赋值
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();
//找到起class对象的模版
markOop prototype_header = k->prototype_header();
//如果class对象模版是非偏向的
if (!prototype_header->has_bias_pattern()) {
//尝试通过cas,将object 改为撤销偏向
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()) {
//走到这里表示对象偏向状态过期
if (attempt_rebias) {
//thread 不为空
assert(THREAD->is_Java_thread(), "");
markOop biased_value = mark;
//设置线程id
markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
//通过cas方式进行偏向处理
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()) {
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_RevokeBias revoke(&obj, (JavaThread*) THREAD);
VMThread::execute(&revoke);
return revoke.status_code();
}
}
//这里是批量撤销和批量重偏向的地方
assert((heuristics == HR_BULK_REVOKE) ||
(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();
}
这里有几个重要的资讯,
- hashcode 会影响到偏向锁,导致偏向锁撤销。
- 批量撤销需要走入safe point,通过vm thread来执行批量撤销。
- 少数撤销的情况下可以不用vm thread介入,比如不涉及到竞争的时候,hashcode这种场景可以进入撤销的逻辑。
/ 走到这里都是撤销判断的逻辑
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();
//class 被撤销的次数
int revocation_count = k->biased_lock_revocation_count();
//这个逻辑表达之内如果在单位时间取消偏向的次数低于某个预值,则重新设置为0
//BiasedLockingBulkRebiasThreshold =20
//BiasedLockingBulkRevokeThreshold =40
//BiasedLockingDecayTime=25000
if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
(revocation_count < BiasedLockingBulkRevokeThreshold) &&
(last_bulk_revocation_time != 0) &&
(cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
k->set_biased_lock_revocation_count(0);
revocation_count = 0;
}
//无需无限制增加这个count
if (revocation_count <= BiasedLockingBulkRevokeThreshold) {
revocation_count = k->atomic_incr_biased_lock_revocation_count();
}
//如果超过阙值则要执行批量撤销
if (revocation_count == BiasedLockingBulkRevokeThreshold) {
return HR_BULK_REVOKE;
}
//等于这个值的时候执行批量偏向,(就是单位时间降下来了)
if (revocation_count == BiasedLockingBulkRebiasThreshold) {
return HR_BULK_REBIAS;
}
return HR_SINGLE_REVOKE;
}
上面的代码能获得一个信息
- kclass 会纪录一个撤销次数,任意这个kclass的所属对象都能会导致kclass的撤销次数增加。
- 每个周期内,增加到一定次数都会有一次批量重偏向和批量撤销。这里就正好说明为什么第三个例子用上了偏向锁。
总结
因为已经有很多其他网文已经分析过该代码,这篇文章主要结合实际的例子来解释偏向锁获取的一个过程。至于偏向锁撤销的源码分析以及轻量级锁膨胀的逻辑,请看下面几个链接。