本文出自:【InTheWorld的博客】 (欢迎留言、交流)
在多线程的Java程序中,Synchronized关键字是经常出现的。这篇文章里,我们就来研究一下它的实现原理。比如以下的示例程序:
public class SynchronizedTest { int syncFunc() { synchronized(this) { int a = 0; return a; } } }
对应的字节码如下:
Compiled from "SynchronizedTest.java" public class SynchronizedTest { public SynchronizedTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return int syncFunc(); Code: 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: iconst_0 5: istore_2 6: iload_2 7: aload_1 8: monitorexit 9: ireturn 10: astore_3 11: aload_1 12: monitorexit 13: aload_3 14: athrow Exception table: from to target type 4 9 10 any 10 13 10 any }
Java编译器为synchronized关键字生成了monitorenter和monitorexit字节码。这两个关键字把临界区包裹起来,实现了函数的线程安全。所以我们需要研究一下monitorenter和monitorexit字节码的工作原理。换句话说,就是要找到JVM是如何解释执行这两个字节码的。
谈到Java解释器就说来话长了,因为JVM中有多个Interpreter的实现。虽然它们的效率有比较大的差异,但可用性都是有保证的,毕竟Java都二十多年了。这里为了简单起见,我就拿比较初级的BytecodeInterpreter分析了。这个字节码解释器有一个重要的run()方法,它就是实现字节码解析和执行的函数。虽然功能听起来挺高大上的,但实际上就是一个很大的switch语句。不信你来看代码!
while (1) { opcode = *pc; switch (opcode) { CASE(_nop): UPDATE_PC_AND_CONTINUE(1); /* Push miscellaneous constants onto the stack. */ CASE(_aconst_null): SET_STACK_OBJECT(NULL, 0); UPDATE_PC_AND_TOS_AND_CONTINUE(1, 1); /* Push a 1-byte signed integer value onto the stack. */ 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++; } if (entry != NULL) { entry->set_obj(lockee); markOop displaced = lockee->mark()->set_unlocked(); entry->lock()->set_displaced_header(displaced); if (Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) { // Is it simple recursive case? if (THREAD->is_lock_owned((address) displaced->clear_lock_bits())) { entry->lock()->set_displaced_header(NULL); } else { CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); } } UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1); } else { istate->set_msg(more_monitors); UPDATE_PC_AND_RETURN(0); // Re-execute } } }
这段代码被各种删减之后,你会发现它是如此的简单。while循环加switch,确定不是《程序设计基础》第二章的例题?虽然这段代码已经非常简单了,但是你依然可以清晰的看到我们的主角——_monitorenter,这个重要的字节码。它的兄弟字节码_moniterexit,我们先按下不表。等我们搞清楚_moniterenter,你会发现_moniterexit只是一个逆向操作而已。
1. lightweight locked(轻度锁)
然而这个moniterenter却很不简单呀!先看if (entry != NULL)之前的代码,这段代码的功能就是尝试分配一个可用的entry。关于这个entry的使用,就到了if语句之后的部分了。这段代码看似简单,但还是需要一些背景知识才可能理解的。首先需要介绍的是Mark Word,这是每一个Object对象都拥有的一个域。在32位的机器上,Mark Word的长度也是32位,它的具体含义如下图:
图中的每一行代表一个独立状态,状态之间是互斥的。第一种状态是Object的正常状态,第二种状态是轻度锁的状态第三种是moniter重度锁状态,第五种是偏向锁状态。我们首先把重点放在前三种状态上,来研究Object的锁是如何工作的。为了方便理解,贴两图出来。下面这张图是进入if语句之前的状态:
这里的Lock record就是刚刚提到的entry变量,而Object对象头部的mark word也表示它还是个“单身汉”。接下来,当前线程会尝试进入轻度锁状态。这个过程如下图所示:
if语句内部的前三行代码实际上就是完成了Lock record的赋值,这其中包括根据Object的mark word设置header,以及把entry的obj(也就是owner)指向Object。然后就到了激动人心的时刻,该CAS出场了,能不能拿到轻量锁就在此一举了!
这个CompareAndSwap的作用就是——比较Object的mark word和Lock record的header是否相同。如果相同,那么就把entry的地址赋值给mark word;如果不同,说明Object已经名花有主了,然后尝试其他方式。这里有一个非常有趣的trick,值得和大家一说。为什么轻量锁状态的tag bits是00,而不是其他值呢?答案就是,在CAS的成功操作中,会把entry的地址赋值给mark word。一般来说,地址值的低两位会有多种组合,但是如果我们在构造entry是按4字节对齐,那么它地址的低两位就一定是00。而且4字节对齐会更容易实现,所以就把tag bits规定为这样了。
2. monitor重度锁
前面的代码中,如果CAS成功,那么就直接进入后续的字节码执行了,所以我们要研究CAS失败的情况。如果失败了,首先会检查是不是当前线程重入了,如果是,那么也相当于成功了。如果判断也不是当前线程重入,那就到了InterpreterRuntime::monitorenter(THREAD, entry), handle_exception)的表演时刻了。
实际上InterpreterRuntime::monitorenter()方法也会再尝试一次CAS,因为一段时间之后,锁可能已经被释放了。如果还是失败,就没办法了——进入monitor重度锁吧!这个操作是由ObjectMonitor::enter()完成的。
void ATTR ObjectMonitor::enter(TRAPS) { Thread * const Self = THREAD ; void * cur ; Atomic::inc_ptr(&_count); EventJavaMonitorEnter event; { // Change java thread status to indicate blocked on monitor enter. JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this); DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt); if (JvmtiExport::should_post_monitor_contended_enter()) { JvmtiExport::post_monitor_contended_enter(jt, this); } OSThreadContendState osts(Self->osthread()); ThreadBlockInVM tbivm(jt); Self->set_current_pending_monitor(this); // TODO-FIXME: change the following for(;;) loop to straight-line code. for (;;) { jt->set_suspend_equivalent(); EnterI (THREAD) ; if (!ExitSuspendEquivalent(jt)) break ; exit (false, Self) ; jt->java_suspend_self(); } Self->set_current_pending_monitor(NULL); } Atomic::dec_ptr(&_count); assert (_count >= 0, "invariant") ; Self->_Stalled = 0 ; if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) { ObjectMonitor::_sync_ContendedLockAttempts->inc() ; } }
这个函数会在for循环中不断检查是否拿到了Moniter锁,如果拿到了就成功的执行完了这个monitorenter字节码了。这里就先不介绍monitorexit了。还有就是偏向锁也没有介绍,等后续有需要的话再写了。
参考:
http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf
线程在调用synchronized修饰的代码块或者方法,首先获取锁,并且是先获取lightweight locked,失败了再获取monitor重度锁?
更完整和准确的说,应该是偏向锁-》轻量锁-》monitor重锁这样一个顺序。从左到右,锁的消耗递增。
还有一个疑问就是函数:
void ATTR ObjectMonitor::enter(TRAPS);这个函数会在for循环中不断检查是否拿到了Moniter锁;
我原来是这样理解的:某个线程去获取Moniter锁,如果没有获取到(说明被其他线程获取了)就应该阻塞了呀。从void ATTR ObjectMonitor::enter(TRAPS)函数来看,应该是个某个线程没有获取到Moniter锁(被其他线程获取了)然后会一直循环获取?
确实会不断获取,但不是死循环,它会把自己挂起来,被唤醒后再检查。这部分逻辑我没有细看,但我觉得应该是这样。
感谢回复!
有个问题:假设线程A在执行同步操作的过程中没有竞争,且需要对同一个对象多次重入,现在知道第一次CAS操作成功后,会在当前栈帧中新建一个lcok record,请问第二次重入时还会继续创建一个新的lock record吗?
你看下面这三句话,以及之后的6行代码。
// 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
结论是,即使是重入也需要新建一个lock record。在cmpxchg_ptr这里也能看出来,在cas失败的时候,也还是会先判断该线程是不是已经拥有锁了。这里看起来确实有点不太必要,我也没想明白这样做的原因。
两个地方不是很明白:
1.关于这句语句的含义
markOop displaced = lockee->mark()->set_unlocked();
,这是直接将锁对象的markword设置为无锁状态吗?
2.如果这句
Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced)
CAS成功了,那该线程就直接获得锁了吗? 偏向锁的逻辑呢?不应该是先尝试偏向锁,再轻量级锁吗?被这两个问题困扰了很久,期待你的回复。