监视器锁 synchronized
一、synchronized 的原理
JVM 基于进入和推出Monitor对象来实现方法和同步代码块,但两者的实现细节不同。
- synchronize 修饰的同步代码块:使用monitorenter 和 monitorexit 指令实现;
- synchronize 修饰的方法并没有 monitorenter 和 monitorexit 指令 ,而取代之的是 ACC_SYNCHRONIZED标识,该标志指明了该方法是一个同步方法,从而执行相应的同步调用。
Monitorenter 指令实现编译后到同步代码块开始的位置,而 monitorexit 是插入在方法结束出和异常处,JVM要保证每个monitorter 必须要有一个与之对应的monitorexit 。任何一个对象都可以和一个monitor 相关联,且当一个执行 monitorenter 指令的时候,会尝试获取synchronized 圆括号中对象中相关联的 monitor的所有权,即尝试获取这个对象的锁(这里的是重量级锁,为没有后面提到的偏向锁和轻量级锁)
(1)synchronized修饰的方法
public synchronized void method() { int i = 0; }
javap反编译后:
public synchronized void method(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=1, locals=2, args_size=1 0: iconst_0 1: istore_1 2: return LineNumberTable: line 6: 0 line 7: 2 LocalVariableTable: Start Length Slot Name Signature 0 3 0 this Lcom/lock/SyncTest; 2 1 1 i I
对于同步方法,JVM会讲方法设置 ACC_SYNCHRONIZED 标志,调用的时候 JVM 根据这个标志判断是否是同步方法。
(2)synchronized修改的代码块
public void method() { synchronized (this) { int i = 0; } }
javap反编译后:
public void method(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: iconst_0 5: istore_2 6: aload_1 7: monitorexit 8: goto 14 11: aload_1 12: monitorexit 13: athrow 14: return Exception table: from to target type 4 8 11 any 11 13 11 any LineNumberTable: line 6: 0 line 7: 4 line 6: 6 line 9: 14 LocalVariableTable: Start Length Slot Name Signature 0 15 0 this Lcom/lock/SyncTest;
对于Synchronized的同步代码块,JVM会在进入代码块之前加上monitorenter ,如果进入monitor成功,线程便获取了锁,一个对象的monitor同一时刻只能被一个线程锁占有;
上面为什么有两个monitorexit呢?第一个是正常退出,第二个是异常退出,保证不会死锁。
monitorenter和 monitorexit源码剖析
openjdk\hotspot\src\share\vm\interpreter\interpreterRuntime.cpp
// Synchronization // // The interpreter's synchronization code is factored out so that it can // be shared by method invocation and synchronized blocks. //%note synchronization_3 //%note monitor_1 // 参数:当前线程、当前对象 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"); // 是否开启偏向锁功能,JVM参数:+UseBiasedLocking表示开启偏向锁 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
// ---------------------------------偏向锁的获取-------------------------------------------- // Fast Monitor Enter/Exit // This the fast monitor enter. The interpreter and compiler use // some assembly copies of this code. Make sure update those code // if the following function is changed. The implementation is // extremely sensitive to race condition. Be careful. 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); } // 对象头是否有偏向标记,如果没有提示偏向锁将被取消,执行slow_enter assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } // slow_enter (obj, lock, THREAD) ; } // --------------------------------轻量级锁的获取--------------------------------------------- // Interpreter/Compiler Slow Case // This routine is used to handle interpreter/compiler slow case // We don't need to use fast path here, because it must have been // failed in the interpreter/compiler code. void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { // 对象头 markOop mark = obj->mark(); assert(!mark->has_bias_pattern(), "should not see bias pattern here"); // 判断是否为无锁状态 if (mark->is_neutral()) { // Anticipate successful CAS -- the ST of the displaced mark must // be visible <= the ST performed by the CAS. // 锁指向该对象头 lock->set_displaced_header(mark); // CAS加锁操作,如果成功则结束,否则继续执行后续代码... if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } // Fall through to inflate() ... // 如果是轻量级锁,并且该锁被当前线程持有(上面已经判断过是否可偏向,所以这里直接跳过偏向锁的判断) } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { assert(lock != mark->locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock"); // 锁指向null lock->set_displaced_header(NULL); return; } #if 0 // The following optimization isn't particularly useful.(不是最佳选择,一般不会走到这里,可忽略) // 如果是重量级锁,并且该锁被当前线程持有 if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) { lock->set_displaced_header (NULL) ; return ; } #endif // The object header will never be displaced to this lock, // so it does not matter what the value is, except that it // must be non-zero to avoid looking like a re-entrant lock, // and must not look locked either. lock->set_displaced_header(markOopDesc::unused_mark()); // 膨胀(即升级为重量级锁) ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); }
二、Monitor监视器锁
其中轻量级锁和偏向锁是Java6对synchronized锁进行优化后增加的,我们稍后会进行分析。这里我们主要分析重量级锁,也就是通常所说的synchronized对象锁,锁标识为10,其中指针指向monitor对象(也称之为管程或者监视器锁)的起始地址。每个对象都存在一个monitor与之关联,对象与其monitor之间也存在着多种实现方式,如monitor可以与对象一起创建或者销毁或当前线程试图获取锁时自动生成,但一个monitor被某线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是有ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; // 记录当前持有锁的线程ID _WaitSet = NULL; // 等待池:处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 锁池:处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList ,用来保存ObjectWaiter 对象列表,每个等待锁的线程都会被封装成ObjectWaiter 对象,_owner 指向持有ObjectMonitor 对象的线程,当多个线程同时访问同一同步代码块或者同步方法时,首先会进入 _EntryList 队列,当线程获取到monitor 后进入_Owner 区域并把 monitor中的 _Owner 变量设置为当前线程,同时monitor 中的计数器count 加1,若线程调用wait() 方法,将释放当前持有的monitor,_owner变量恢复为null,count 自减 1 ,同时该线程进入_WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示:
每个对象都有一个Monitor对象相关联,Monitor对象中记录了持有锁的线程信息、等待队列等。Monitor对象包含以下三个字段:
- _owner 记录当前持有锁的线程ID
- _EntryList 是一个队列,记录所有阻塞等待锁的线程(阻塞队列,锁池)
- _WaitSet 也是一个队列,记录调用 wait() 方法并还未被通知的线程(等待池)
当线程持有锁的时候,线程id等信息会拷贝进owner字段,其余线程会进入阻塞队列entrylist,当持有锁的线程执行wait方法,会立即释放锁进入waitset,当线程释放锁的时候,owner会被置空,公平锁条件下,entrylist中的线程会竞争锁,竞争成功的线程id会写入owner,其余线程继续在entrylist中等待。
采用Synchronized给对象加锁会使线程阻塞,因而会造成线程状态的切换,而线程状态的切换必须要操作系统来执行,因此需要将用户态切换为内核态,这个切换的过程是十分耗时的都需要操作系统来帮忙,有可能比用户执行代码的时间还要长。
所以,monitor对象存在于每一个java对象的对象头(存储指针的指向),synchronized锁便是通过这种方式获取的,也是为什么java中任何对象都可以作为锁的原因,同时也是 notify/notifyAll/wait 方法等存在于顶级对象Object中的原因。
Wait/Notify/NotifyAll 源码
openjdk\hotspot\src\share\vm\runtime\synchronizer.cpp
// ----------------------------------------------------------------------------- // Wait/Notify/NotifyAll // NOTE: must use heavy weight monitor to handle wait() void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) { // 是否开启偏向锁 if (UseBiasedLocking) { // 撤销偏向锁 BiasedLocking::revoke_and_rebias(obj, false, THREAD); assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } if (millis < 0) { TEVENT (wait - throw IAX) ; THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative"); } // 锁膨胀(升级为重量级锁) ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj()); DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis); // 调用ObjectMonitor的wait方法 monitor->wait(millis, true, THREAD); /* This dummy call is in place to get around dtrace bug 6254741. Once that's fixed we can uncomment the following line and remove the call */ // DTRACE_MONITOR_PROBE(waited, monitor, obj(), THREAD); dtrace_waited_probe(monitor, obj, THREAD); } void ObjectSynchronizer::notify(Handle obj, TRAPS) { if (UseBiasedLocking) { // 撤销偏向锁 BiasedLocking::revoke_and_rebias(obj, false, THREAD); assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } markOop mark = obj->mark(); // 如果是轻量级锁,并且当前线程拥有该锁,直接返回 if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { return; } // 锁膨胀(升级为重量级锁),并调用ObjectMonitor的notify方法 ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD); } // NOTE: see comment of notify() void ObjectSynchronizer::notifyall(Handle obj, TRAPS) { if (UseBiasedLocking) { // 撤销偏向锁 BiasedLocking::revoke_and_rebias(obj, false, THREAD); assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } markOop mark = obj->mark(); // 如果是轻量级锁,并且当前线程拥有该锁,直接返回 if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { return; } // 锁膨胀(升级为重量级锁),并调用ObjectMonitor的notifyAll方法 ObjectSynchronizer::inflate(THREAD, obj())->notifyAll(THREAD); }
三、synchronized的可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,也叫递归锁,请求将会成功, 在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:
public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; static int j=0; @Override public void run() { for(int j=0;j<1000000;j++){ //this,当前实例对象锁 synchronized(this){ i++; increase();//synchronized的可重入性 } } } public synchronized void increase(){ j++; } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。