1、反汇编方式理解synchronized原理
(1)源码
public class Test { private static Object obj = new Object(); public static void main(String[] args) { synchronized (obj) { System.out.println("1"); } } public synchronized void test() { System.out.println("a"); } }
(2)反汇编查看字节码指令
在monitorenter和monitorexit之间执行的是代码逻辑
(3)monitorenter
每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:
- 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
- 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
- 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量:owner拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待
(4)monitorexit
能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个
2、查看JVM源码
(1)下载源代码(因为synchronized的源代码是c++写的)
选择版本:
选择格式:
(2)查看源代码
在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主要数据结构如下:
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; }
- _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方法而被阻塞的线程会被放在该队列中。
3、monitor竞争
执行monitorenter时,会调用InterpreterRuntime.cpp(位于:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函数。具体代码可参见HotSpot源码
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");
对于重量级锁,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 ; } // 省略一些代码 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); }
- 通过CAS尝试把monitor的owner字段设置为当前线程。
- 如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions ++ ,记录重入的次数。
- 如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。
- 如果获取锁失败,则等待锁的释放
4、monitor等待
竞争失败等待调用的是ObjectMonitor对象的EnterI方法(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下所示:
void ATTR ObjectMonitor::EnterI (TRAPS) { Thread * Self = THREAD ; // Try the lock - TATAS if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } if (TrySpin (Self) > 0) { assert (_owner == Self , "invariant") ; assert (_succ != Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; }//以上代码是在没有获得锁的情况下再次尝试获取锁 // 省略部分代码 // 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ; ObjectWaiter node(Self) ; Self->_ParkEvent->reset() ; node._prev = (ObjectWaiter *) 0xBAD ; node.TState = ObjectWaiter::TS_CXQ ; // 通过CAS把node节点push到_cxq列表中 ObjectWaiter * nxt ; for (;;) { node._next = nxt = _cxq ; if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ; // Interference - the CAS failed because _cxq changed. Just retry. // As an optional optimization we retry the lock. if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } } // 省略部分代码 for (;;) { // 线程在被挂起前做一下挣扎,看能不能获取到锁 if (TryLock (Self) > 0) break ; assert (_owner != Self, "invariant") ; if ((SyncFlags & 2) && _Responsible == NULL) { Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; } // park self if (_Responsible == Self || (SyncFlags & 1)) { TEVENT (Inflated enter - park TIMED) ; Self->_ParkEvent->park ((jlong) RecheckInterval) ; // Increase the RecheckInterval, but clamp the value. RecheckInterval *= 8 ; if (RecheckInterval > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; // 通过park将当前线程挂起(由于用户和系统的需要,例如,终端用户需要暂停程序研究其执行情况或对其进行修改、OS为了提
高内存利用率需要将暂时不能运行的进程(处于就绪或阻塞队列的进程)调出到磁盘),等待被唤醒 Self->_ParkEvent->park() ; } if (TryLock(Self) > 0) break ; // 省略部分代码 } // 省略部分代码 }
当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁,TryLock方法实现如下:
int ObjectMonitor::TryLock (Thread * Self) { for (;;) { void * own = _owner ; if (own != NULL) return 0 ; if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) { // Either guarantee _recursions == 0 or set _recursions = 0. assert (_recursions == 0, "invariant") ; assert (_owner == Self, "invariant") ; // CONSIDER: set or assert that OwnerIsThread == 1 return 1 ; } // The lock had been free momentarily, but we lost the race to the lock. // Interference -- the CAS failed. // We can either return -1 or retry. // Retry doesn't make as much sense because the lock was just acquired. if (true) return -1 ; } }
- 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
- 在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
- node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒。
- 当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。
5、monitor释放
- 当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor的exit方法中。(位于:src/share/vm/runtime/objectMonitor.cpp)
- 退出同步代码块时会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。
- 根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由unpark完成
- 被唤醒的线程,会回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,继续执行monitor的竞争。
6、monitor是重量级锁
可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语言中是一个重量级(Heavyweight)的操作。
内核:可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。
用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。
系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。
用户态与内核态的划分主要是为了保护系统,用户态程序的崩溃不会影响内核
所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);但是当它调用系统调用执行某些操作时,例如 I/O调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内核态)。 系统调用的过程可以简单理解为:
- 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈, 以此表明需要操作系统提供的服务。
- 用户态程序执行系统调用。
- CPU切换到内核态,并跳到位于内存指定位置的指令。
- 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
- 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。
由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在synchronized未优化之前,效率低的原因。