源码地址:https://github.com/nieandsun/concurrent-study.git
上篇文章《【并发编程】 — 从字节码指令的角度去理解synchronized关键字的原理》从字节码指令的角度讲解了synchronized关键字的原理,从中可以知道其实synchronized关键字真正锁的是锁对象关联的monitor对象,那
本篇文章将来讲解一下这几个问题。
源码地址:http://openjdk.java.net/ --> Mercurial --> jdk8 --> hotspot --> zip
当然如果下载不下来,也可以从我提供的源码里clone: https://github.com/nieandsun/concurrent-study.git
首先应该知道,monitor并不是我们java开发人员主动创建的一个对象。事实上可以这么理解:monitor并不是随着对象的创建而创建的,而是我们通过synchronized修饰符告诉JVM需要为我们的某个对象(锁对象)创建关联的monitor对象。
在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用C++写的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp
)。ObjectMonitor主要数据结构如下:
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;//线程的重入次数
_object = NULL;//存储改monitor的对象
_owner = NULL;//拥有该monitor的线程
_WaitSet = NULL;//处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;//多线程竞争锁时的单向列表
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
对其中几个比较重要的变量进行解释如下:
_owner:
初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程 释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程的安全。_cxq:
竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资 源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指 向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。_EntryList:
_cxq队列中有资格成为候选资源的线程会被移动到该队列中。_WaitSet:
因为调用wait方法而被阻塞的线程会被放在该队列中。锁对象与monitor中的_owner、_WaitSet以及_EntryList之间的关系可以用下图进行表示:
monitorenter
和 monitorexit
这两个字节码指令到底与monitor
之间是什么样的关系呢?
其实并没有那么不可想象!!! 以monitorenter字节码指令为例,当JVM当发现要运行此字节码指令时,就会调用C++的 InterpreterRuntime::monitorenter函数,该函数的位于HotSpot虚拟机源码InterpreterRuntime.cpp中(src/share/vm/interpreter/interpreterRuntime.cpp
),具体源码如下:
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); //JDK1.6及之后对锁进行的优化
} 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");
对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter方法
并最终调用 ObjectMonitor::enter方法(位于:src/share/vm/runtime/objectMonitor.cpp
),源码如下:
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
// 通过CAS操作尝试把monitor的_owner字段设置为当前线程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
// 线程重入,recursions++
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
// 如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
//省略一些代码。。。。
// TODO-FIXME: change the following for(;;) loop to straight-line code.
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
// 如果获取锁失败,则等待锁的释放;
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
这里暂时先不考虑JDK1.6及之后对synchronized关键字进行的优化。
以上代码的具体流程概括起来如下:
- (1) 通过CAS尝试把monitor的owner字段设置为当前线程。 ---- 调用了内核函数 Atomic::cmpxchg_ptr
- (2)如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行 recursions ++ ,记录重入的次数。
- (3)如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获 得锁并返回。
- (4) 如果获取锁失败,则等待锁的释放。
到这里我自认为
作为java程序员就没必要再进一步去深究JVM源码了,因为从上面的介绍其实已经可以知道,当JVM遇到monitorenter指令时会调用C++的代码为当前线程去抢占锁对象对应的monitor的所有权。那可以想像当JVM遇到monitorexit指令时肯定也会调用C++的代码释放monitor的所有权,并唤醒其他线程(源码为src/share/vm/runtime/objectMonitor.cpp
中的ObjectMonitor::exit方法, 有兴趣的可以自己去研究)。
做个小图进行总结一下:
我这里并不想去细究什么是用户态、内核态或者用户空间、内核空间 — 有兴趣的可以自行百度。
因此这里只简单给出两句话:
(1)争抢锁+释放锁的C++代码里会调用一些内核函数。
(2)简单理解就是,JVM调用了内核提供的函数,期间肯定要涉及到cpu从JVM(用户态)到内核态的来回切换 — 这种切换会带来大量的系统资源消耗,所以说monitor是一个重量级锁。
是否正在并发执行
,都会
涉及到用户态和内核态的切换 。
Doug Lea
最看不惯的地方 —> 下篇文章将讲一下Doug Lea在ReentrantLock中针对该问题的解决方案没有真正并发执行时
等特殊时机进行的优化,以及锁的粒度等进行的优化 —》并不是说就杜绝了用户态和内核态的切换;end