简介
synchronized,是Java中用于解决并发情况下数据同步访问的一个很重要的关键字。当我们想要保证一个共享资源在同一时间只会被一个线程访问到时,我们可以在代码中使用synchronized关键字对类或者对象加锁。在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了。
synchronized的基本语法
Java中每一个对象都可以作为锁,具体表现为一下三种方式
- 修饰实例方法,锁是当前实例对象。
- 静态方法,锁是当前类的Class对象。
- 修饰代码块,锁是Synchonized括号里配置的对象。
Synchronized实现原理
用户态与内核态
内核态:可以访问所有软硬件资源,包括外围设备,例如硬盘,网卡得。
用户态:只能受限的访问内存,且不允许访问外围设备。
JDK早期,synchronized 叫做重量级锁,Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要通过kernel, 系统调用(System Call),这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的get 或set方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是java语言中一个重量级的操纵。
所以,在Java SE 1.6中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化,这些操作都是在用户进程中去实现的。
CAS
Compare And Swap (Compare And Exchange),它是一种基于乐观锁的操作。它有三个操作数,内存值V,预期值A,更新值B。当且仅当A和V相同时,才会把V修改成B,返回旧值,否则什么都不做。Java中 AtomicInteger等无锁类便用到了CAS。
最终实现:
lock cmpexg汇编指令,用于比较并交换操作数,CPU对CAS的原语支持
cmpexge 指令是非原子性,最早用于单核CPU,在JVM实现atomic_linux_x86.inline.hpp如下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
//如果是 Multiprocessor 多核处理器 则增减Lock 指令,保证原子性
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
LOCK指令用于再多处理器中执行指令时对共享内存的独占使用,在执行后面指令的时候锁定一个北桥信号(不采用锁总线的方式)。
能够将当前处理器对应的缓存刷新到内存,并使其它处理器对应的缓存失效,另外还提供了有序的指令无法越过和这个内存屏障的作用
字节码
在Java中,synchronized有以下使用形式
public class SynchronizedTest {
public synchronized void syncMethod(){
System.out.println("syncMethod");
}
public void syncObject(){
synchronized (SynchronizedTest.class){
System.out.println("syncObject");
}
}
}
使用javap反编译字节码如下:
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String syncMethod
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
...
public void syncObject();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/marvin/cloud/test/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #6 // String syncObject
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
...
从上面可以看出synchronized修饰方法和代码块,生成的字节码稍有不同,方法级的同步是隐式的,即无需通过字节码指令来控制,JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。 对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。JVM保证每个monitorenter必须有一个monitorexit指令与之对应。线程执行到monitorenter指令处时,会尝试获取对象对应的锁对象的所有权。
JVM实现
在Java虚拟机(HotSpot,JDK12)中,monitorenter字节码的入口是基于C++实现的,由bytecodeInterpreter.cpp实现的,其主要实现如下:
CASE(_monitorenter): {
oop lockee = STACK_OBJECT(-1); //获取锁对象
// derefing's lockee ought to provoke implicit null check
CHECK_NULL(lockee);
// find a free monitor or one already allocated for this object
// if we find a matching object then we need a new monitor
// since this is recursive enter
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++;
}
//在当前线程的栈中找到空闲的Lock Record位置存储BasicObjectLock执行锁对象
if (entry != NULL) {
entry->set_obj(lockee);
int success = false;
uintptr_t epoch_mask_in_place = markWord::epoch_mask_in_place;
markWord mark = lockee->mark();
intptr_t hash = (intptr_t) markWord::no_hash;
//锁对象头已是偏向状态
if (mark.has_bias_pattern()) {
uintptr_t thread_ident;
uintptr_t anticipated_bias_locking_value;
thread_ident = (uintptr_t)istate->thread();
anticipated_bias_locking_value =
((lockee->klass()->prototype_header().value() | thread_ident) ^ mark.value()) &
~(markWord::age_mask_in_place);
//偏向的线程是自己 nothing to do
if (anticipated_bias_locking_value == 0) {
// already biased towards this thread, nothing to do
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::biased_lock_entry_count_addr())++;
}
success = true;
}
//如果Klass对象头偏向锁标志位为0,批量偏向锁撤销,则尝试撤销偏向锁
else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
markWord header = lockee->klass()->prototype_header();
if (hash != markWord::no_hash) {
header = header.copy_set_hash(hash);
}
//尝试撤销偏向锁,替换加锁对象的mark word为Klass的markword
if (lockee->cas_set_mark(header, mark) == mark) {
if (PrintBiasedLockingStatistics)
(*BiasedLocking::revoked_lock_entry_count_addr())++;
}
}
//如果加锁对象的epoch不等于Klass的epoch,尝试重偏向(触发了批量重偏向,偏向的线程已释放,可直接偏向当前线程)
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
// try rebias 构造一个偏向当前线程的mark word
markWord new_header( (intptr_t) lockee->klass()->prototype_header().value() | thread_ident);
if (hash != markWord::no_hash) {
new_header = new_header.copy_set_hash(hash);
}
//CAS更新锁对象的markword
if (lockee->cas_set_mark(new_header, mark) == mark) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::rebiased_lock_entry_count_addr())++;
}
//CAS失败,存在锁竞争,进入锁升级monitorenter
else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
//偏向别的线程或者是匿名偏向anonymously
else {
//构建一个匿名偏向的markword
// try to bias towards thread in case object is anonymously biased
markWord header(mark.value() & (markWord::biased_lock_mask_in_place |
markWord::age_mask_in_place |
epoch_mask_in_place));
if (hash != markWord::no_hash) {
header = header.copy_set_hash(hash);
}
//构建一个指向当前线程的markword
markWord new_header(header.value() | thread_ident);
// debugging hint
DEBUG_ONLY(entry->lock()->set_displaced_header(markWord((uintptr_t) 0xdeaddead));)
// CAS操作,以匿名偏向状态的markword为Old值进行CAS操作,如果锁对象的markword是匿名偏向状态,且无线程竞争且直接从匿名偏向到偏向当前线程
if (lockee->cas_set_mark(new_header, header) == header) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
//不是匿名偏向或者锁竞争则进入锁升级过程
else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
}
// traditional lightweight locking
if (!success) {
//如果没有开启偏向模式或无锁状态,则进行轻量级锁升级
markWord displaced = lockee->mark().set_unlocked();
//设置 当前栈中Lock Recod为无锁状态
entry->lock()->set_displaced_header(displaced);
//-XX:+UseHeavyMonitors,则call_vm=true,代表禁用偏向锁和轻量级锁
bool call_vm = UseHeavyMonitors;
//如果开启重量级锁,直接进行锁升级monitorenter,或者轻量级锁,CAS把当前对象的markword指向Lock Record的
if (call_vm || lockee->cas_set_mark((markOop)entry, displaced) != displaced) {
// Is it simple recursive case? 如果CAS不成功,可能是轻量级锁重入或者竞争
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
//锁重入 则displaced Lock Record置为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
}
}
interpreterRuntime.cpp InterpreterRuntime::monitorenter 锁升级过程:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
Markword
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面会存储什么信息呢?
Synchronized用的锁是存在Java对象头里的,HotSpot虚拟机中,对象在内存布局中可以分为三块区域,对象头、实例数据、对齐填充数据,其中对象头包括Mark Word和类指针(Klass Pointer)。
HotSpot虚拟机中将mark word划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。下图描述了在32位虚拟机(hotspot)上,在对象不同状态时 mark word各个比特位区间的含义:
当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。
synchronized优化的过程和markword息息相关,可以通过OpenJDK提供JOL工具包查询详细的对象存储结构布局信息。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
Synchronized锁优化
偏向锁
在Java中,提供了很多线程安全的工具包用Synchronized修饰,但再实际的开发中,大多数操作可能都是同一线程进行操作,比如StringBuffer的append()方法,被Synchronized修饰,很可能只有一个线程会调用相关同步方法,demo:
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
sb.append("test:" + i);
}
}
所以在JDK1.6中引入了偏向锁,提高对于一些同步操作一段时间只有一个线程访问的效率,在第一次获得锁时,只会有一个CAS操作在锁对象的markword中设置当前线程ID,之后同一线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的CAS命令。
匿名偏向锁( Anonymous BiasedLock )
当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(没有计算HasHCode),那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。
如果计算过对象的hashCode,则对象无法进入偏向状态!
轻量级锁重量级锁的hashCode存在与什么地方?
答案:线程栈中,轻量级锁的
Lock Record中,或是代表重量级锁的ObjectMonitor的成员中
偏向锁加锁与撤销
通过上面bytecodeInterpreter.cpp源码分析,偏向锁的锁和偏向的线程信息存储在Java对象头(markword)中的,偏向锁的加锁过程可以分为以下步骤:
在线程的栈空间寻找空闲的
LockRecord存放锁对象信息(BasicObjectLock),可用于后续判断偏向线程是否还在同步代码块中,通过遍历所有线程栈空间判断Lock Record是否为Null;判断当前锁对象头是否开启偏向模式,开启了偏向,判断偏向的是否是当前线程,是则锁重入nothing to do,匿名偏向,则会尝试CAS进行设置偏向线程。所以同一个线程反复进入同一个所对象的临界区的开销是很小的。
如果当前锁已偏向其他线程
||epoch值过期||偏向模式关闭||获取偏向锁的过程中存在并发冲突,都会进入到InterpreterRuntime::monitorenter方法, 在该方法中会对偏向锁撤销和升级。未开启偏向模式则直接进入轻量级锁CAS操纵;
偏向锁的撤销和升级的实现在interpreterRuntime.cpp的InterpreterRuntime::monitorenter方法:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
//如果开起了偏向锁 -XX:-UseBiasedLocking=true
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
如果开起了偏向锁 ,则会进入到 ObjectSynchronizer::fast_enter方法撤销偏向锁:
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 {
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
slow_enter(obj, lock, THREAD);
}
如果当前是在safe point安全点则由VM线程进行撤销,否则在BiasedLocking::revoke_and_rebias方法中进行撤销,撤销过程中会再次判断是否能够重偏向(Epoch过期),如果Klass中的epoch!= 对象头中epoch且允许重偏向 ,则直接Cas修改锁对象的markword偏向当前线程。否则撤销为无锁状态。
其它情况偏向其它线程|| 匿名偏向竞争 || CAS失败等都会等到safe point时,调用revoke_bias方法,遍历JVM所有线程的验证偏向线程是否还活着且还在在同步块中(遍历线程栈中的Lock Record中BasicObjectLock),还在同步块中则升级为轻量级锁,否则将偏向锁撤销为无锁状态或者匿名偏向状态,最后则进入到slow_enter尝试升级轻量级锁。
safe point这个词我们在GC中经常会提到,其代表了一个状态,在该状态下所有线程都是暂停。
批量重偏向与批量撤销(Epoch)
批量重偏向与批量撤销渊源:从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到
safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。原理以Klass为单位,为每个Klass维护解决场景批量重偏向(bulk rebias)机制,Klass对象会维护一个偏向锁撤销计数器
_biased_lock_revocation_count,每一次Klass对应的锁对象发生偏向锁撤销,该计数器+1,当这个值达到重偏向阈值BiasedLockingBulkRebiasThreshold(默认20)时,则会进行批量重偏向。每个锁对象都会有一个Epoch字段,其Klass对象_displaced_header中也会有一个Epoch,锁对象中epoch的初始值为创建该对象时Klass中的epoch的值,当发生批量重偏向时,Klass对象中epoch值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获取锁时。发现当前对象的epoch值和Klass的epoch不相等,则表示偏向的线程已经退出了同步代码块(找不到Lock Record执行+1操作),直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。当偏向锁撤销计数器_biased_lock_revocation_count的值达到批量撤销的阈值后(默认40),JVM就认为该Klass的使用场景存在多线程竞争,会标记该Klass的_displaced_header为不可偏向状态,之后,对于该Klass的实例锁,直接走轻量级锁的逻辑。
实现源码如下BiasedLocking::revoke_and_rebias:
//批量重偏向与批量撤销逻辑
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)) {
k->set_biased_lock_revocation_count(0);
revocation_count = 0;
}
// Make revocation count saturate just beyond BiasedLockingBulkRevokeThreshold
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;
}
当发生批量重偏向,则会在safe point遍历JVM所有线程,修改线程的Lock Rocord指向锁对象的epoch值和Klass对象中epoch值,再尝试重偏向,当发生批量撤销时,则将类中的偏向标记关闭同时遍历所有线程的栈,撤销该类所有锁的偏向。
偏向锁延时
默认情况 偏向锁有个时延,默认是4秒 why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
-XX:BiasedLockingStartupDelay=0 #设置延时
轻量级锁
当偏向锁发生竞争或者偏向模式关闭时,则会升级为轻量级锁,它是一种乐观锁,使用CAS操作把锁对象修改为指向线程栈中的Lock Record指针,如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此对象处于轻量级锁定状态,不涉及到操作系统层面,在用户空间实现,相对于传统的重量级互斥锁(使用操作系统互斥量加锁),轻量级锁的性能更好。
加锁步骤
在加锁前,前提是不能带有偏向特征,虚拟机需要在当前线程的栈帧中建立锁记录(Lock Record)的空间。
Lock Record中实现为BasicObjectLockBasic包含锁对象和一个_displaced_header属性,用于存储锁对象的 Mark Word 的拷贝。将锁对象的 Mark Word 复制到锁记录的
_displaced_header属性中,这个复制过来的记录叫做 Displaced Mark Word。最后CAS(Lock Record,
_displaced_header)操作把锁对象的Mark word指向Lock Record,如果CAS失败,则判断是否是锁重入,判断锁状态,轻量级锁,判断是否指向当前线程栈内Lock Reocrd,否则进入锁膨胀为重量级锁;
解锁
使用 CAS 操作把当前线程的栈帧中的
Displaced Mark Word替换回锁对象中去,如果替换成功,则解锁成功。替换失败,轻量级锁膨胀成重量级锁后再解锁
ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
轻量级锁标志位
在轻量级加锁的过程中,是把锁对象的mark word设置成立指向Lock Record的指针,没有看到修改锁状态为00的操作。
轻量级锁加锁过程中,直接锁对象整个Mark word改为指向Lock Record的起始地址,而JVM 要求对象起始地址必须是 8 字节的整数倍,所以这个地址低两位肯定会00。JVM 在将 mark word 设置为 LockRecord 的地址时,将锁标志位一并设置了。
重量级锁 ObjectMoniter
在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现的,其主要数据结构如下:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有几个关键属性:
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_cxq:多线程竞争时的单向列表
_count:抢占该锁的线程数
获取轻量级锁失败时,会先 锁膨胀为为重量级锁,会为每个对象创建自己的监视锁ObjectMonitor对象并初始化,实现代码ObjectSynchronizer::inflate如下:
ObjectMonitor* ObjectSynchronizer::inflate(Thread * Self,
oop object,
const InflateCause cause) {
assert(Universe::verify_in_progress() ||
!SafepointSynchronize::is_at_safepoint(), "invariant");
EventJavaMonitorInflate event;
for (;;) {
const markOop mark = object->mark();
assert(!mark->has_bias_pattern(), "invariant");
//已经是重量级锁状态,直接返回
if (mark->has_monitor()) {
ObjectMonitor * inf = mark->monitor();
assert(inf->header()->is_neutral(), "invariant");
assert(oopDesc::equals((oop) inf->object(), object), "invariant");
assert(ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid");
return inf;
}
//INFLATING(膨胀中)continue重试
if (mark == markOopDesc::INFLATING()) {
ReadStableMark(object);
continue;
}
// 当前轻量级锁状态,先分配一个ObjectMonitor对象,并初始化值
if (mark->has_locker()) {
ObjectMonitor * m = omAlloc(Self);
m->Recycle();
m->_Responsible = NULL;
m->_recursions = 0;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit; // Consider: maintain by type/class
// 将锁对象的mark word设置为INFLATING (0)膨胀中状态
markOop cmp = object->cas_set_mark(markOopDesc::INFLATING(), mark);
if (cmp != mark) {
omRelease(Self, m, true);
continue; // Interference -- just retry
}
markOop dmw = mark->displaced_mark_helper();
assert(dmw->is_neutral(), "invariant");
// Setup monitor fields to proper values -- prepare the monitor
m->set_header(dmw);
m->set_owner(mark->locker());
m->set_object(object);
//将锁对象头设置为重量级锁状态
guarantee(object->mark() == markOopDesc::INFLATING(), "invariant");
object->release_set_mark(markOopDesc::encode(m));
OM_PERFDATA_OP(Inflations, inc());
if (log_is_enabled(Debug, monitorinflation)) {
if (object->is_instance()) {
ResourceMark rm;
log_debug(monitorinflation)("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
p2i(object), p2i(object->mark()),
object->klass()->external_name());
}
}
if (event.should_commit()) {
post_monitor_inflate_event(&event, object, cause);
}
return m;
}
inflate中是一个for循环,主要是为了处理多线程同时调用inflate的情况,锁对象头升级为重量锁后进入ObjectMonitor::enter进行获取锁:
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD ;
//CAS操作修改_owner为当前线程
void * cur = Atomic::cmpxchg(Self, &_owner, (void*)NULL);
if (cur == NULL) {//设置成功,说明该Monitor没有被人占用
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
return ;
}
//CAS失败,但_owner是当前线程,锁重入
if (cur == Self) {
_recursions ++ ;//重入次数加1
return ;
}
//如果是轻量级锁升级的话,cur是Lock Record指针,且在当前线程的栈内,把owner设置成当前线程,
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1;
_owner = Self;
return ;
}
//省略... 执行ObjectMonitor::EnterI方法,自旋等操作
}
执行操作系统同步操作前,会先尝试直接CAS设置monitor的锁状态和自旋获得锁,获取失败则进入ObjectMonitor::EnterI方法获得锁或阻塞,获取锁步骤:
当多个线程同时访问一段同步代码时,尝试CAS设置_Owner为当前线程,成功即获得对象锁,否则通过尝试自旋一定次数加锁,最大程度上避免使用操作系统互斥量(Mutex lock)并阻塞线程。
还无法获取锁,则会将该线程封装成一个
ObjectWaiter对象插入到_cxq的队列的队首,然后调用park函数挂起当前线程。在linux系统上,park函数底层调用的是gclib库的pthread_cond_wait, 当被唤醒后再尝试获得锁
释放锁:
void ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * const Self = THREAD;
// 如果_owner不是当前线程
if (THREAD != _owner) {
// 当前线程是之前持有轻量级锁的线程。由轻量级锁膨胀后还没调用过enter方法,_owner会是指向Lock Record的指针。
if (THREAD->is_lock_owned((address) _owner)) {
// Transmute _owner from a BasicLock pointer to a Thread address.
// We don't need to hold _mutex for this transition.
// Non-null to Non-null is safe as long as all readers can
// tolerate either flavor.
assert(_recursions == 0, "invariant");
_owner = THREAD;
_recursions = 0;
} else {
assert(false, "Non-balanced monitor enter/exit! Likely JNI locking");
return;
}
}
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
return;
}
// Invariant: after setting Responsible=null an thread must execute
// a MEMBAR or other serializing instruction before fetching EntryList|cxq.
_Responsible = NULL;
#if INCLUDE_JFR
// get the owner's thread id for the MonitorEnter event
// if it is enabled and the thread isn't suspended
if (not_suspended && EventJavaMonitorEnter::is_enabled()) {
_previous_owner_tid = JFR_THREAD_ID(Self);
}
#endif
for (;;) {
assert(THREAD == _owner, "invariant");
//先释放锁,这时如果有其他线程进入同步块则能获得锁
OrderAccess::release_store(&_owner, (void*)NULL); // drop the lock
OrderAccess::storeload(); // See if we need to wake a successor
//如果没有等待的线程或已经有获取锁线程
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
return;
}
//要执行之后的操作需要重新获得锁,为后续假定继承人做好准备
if (!Atomic::replace_if_null(THREAD, &_owner)) {
return;
}
guarantee(_owner == THREAD, "invariant");
ObjectWaiter * w = NULL;
//_EntryList中的线程有更高优先级,直接唤醒_EntryList的队首线程
w = _EntryList;
if (w != NULL) {
assert(w->TState == ObjectWaiter::TS_ENTER, "invariant");
ExitEpilog(Self, w);
return;
}
// 如果_cxq and EntryList are null then just 重新循环退出
w = _cxq;
if (w == NULL) continue;
//把 _cxq批量放入_EntryList,_cxq置为NUll
for (;;) {
assert(w != NULL, "Invariant");
ObjectWaiter * u = Atomic::cmpxchg((ObjectWaiter*)NULL, &_cxq, w);
if (u == w) break;
FastScanClosure = u;
}
assert(w != NULL, "invariant");
assert(_EntryList == NULL, "invariant");
_EntryList = w;
ObjectWaiter * q = NULL;
ObjectWaiter * p;
//将cxq中的元素转移到EntryList
for (p = w; p != NULL; p = p->_next) {
guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
p->TState = ObjectWaiter::TS_ENTER;
p->_prev = q;
q = p;
}
// _succ不为null,说明已经有个继承人了,所以不需要当前线程去唤醒,减少上下文切换的比率
if (_succ != NULL) continue;
w = _EntryList;
if (w != NULL) {
// 唤醒EntryList第一个元素
guarantee(w->TState == ObjectWaiter::TS_ENTER, "invariant");
ExitEpilog(Self, w);
return;
}
}
释放锁时,默认策略是:如果EntryList为空,则将cxq中的元素按原有顺序插入到到EntryList,并唤醒第一个线程。也就是当EntryList为空时,是后来的线程先获。若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
锁消除
lock coarsening
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。
锁粗化
lock coarsening
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。
最底层实现
利用HSDIS查看汇编码
-Xcom:让JVM以编译模式执行代码,即JVM会在第一次运行时即将所有字节码编译为本地代码
-XX:+UnlockDiagnosticVMOptions:解锁诊断功能
-XX:+PringAssembly:输出反汇编后的汇编指令
lock cmpxchg 指令,最终实现
LOCK
LOCK用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容刷新到内存,并是其他处理器对应的缓存失效。
另外还提供了有序的指令无法越过这个内存屏障的作用。
Synchronized底层使用的时Lock指令,因此天然具有
可见性、顺序性、原子性。
END
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁 的顺序升级。