在多线程的Java程序中,Synchronized关键字是经常出现的。这篇文章里,我们就来研究一下它的实现原理。 比如以下的示例程序:
aspectj
代码解读
复制代码
public class SynchronizedTest {
int syncFunc() {
synchronized(this) {
int a = 0;
return a;
}
}
}
对应的字节码如下:
yaml
代码解读
复制代码
Compiled from "SynchronizedTest.java"
public class SynchronizedTest {
public SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()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语句。不信你来看代码!
reasonml
代码解读
复制代码
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()完成的。
rust
代码解读
复制代码
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了。还有就是偏向锁也没有介绍,等后续有需要的话再写了。