java의 동기화는 크게 두 가지로 분류
- 암묵적인 동기화(synchronized keyword)
- 명시적인 동기화(concurrent.locks.Lock)
이 두가지는 구현 방식이 다르다.
- synchronized는 jvm의 monitorenter / monitorexit 인스트럭션을 호출
- Lock 은 hotspot에서 바로 native intrinsic function 을 호출
이 글에선 synchronized 의 구현을 살펴본다.
jvm-hotspot 소스코드는 openjdk8 을 참조하였으므로 다른 구현체(oracle, android 등)에서는 상황이 다를 수도 있지만 실제로 그럴 것 같지는 않다.
synchronized keyword
알다시피 synchronized keyword는 두 가지 경우에 쓰인다.
- synchronized method
- synchronized block
두 경우 모두 lock의 획득/반환은 jvm bytecode 중 monitorenter/monitorexit 인스트럭션으로
수행한다.
글에 착오가 있어 수정한다.
- synchronized block은 컴파일러에 의해 monitorenter / monitorexit 인스트럭션으로 변환이 되는 반면,
- synchronized method 는 bytecodeInterpreter가 method를 수행하는 시점에 동기화 여부를 판별하지 monitorenter/monitorexit 인스트럭션으로 컴파일되지는 않는다.
두 경우 모두 InterpreterRuntime::monitorenter() InterpreterRuntime::monitorexit() 메쏘드를 호출하기 때문에 최종적으로 lock을 획득하는 방식은 같지만 메커니즘은 다르다. hotspot 소스코드를 주의깊게 보지 않아 이런 혼동이 왔다. 이 글을 통해 잘못된 정보가 전달될 수 있으므로 앞으로는 조심하겠다.
jvm spec
synchronized method
synchronized method 에서 주의깊게 살펴 볼 내용은 biased lock 이라는 성능 향상을 위한 기법이다. document에 의하면 un-contented thread(=경쟁상태가 아닌 thread)의 성능 향상 효과가 있다고 하며 실제로 그렇다.
biased lock
biased lock이란 대부분의 java 쓰레드는 동기화를 위한 object가 1개인 경우가 많은데에서 착안,
object header 에 thread ID 와 biased 여부를 확인하는 flag 를 두어
동일 thread 가 연속적으로 critical section에 접근하는 경우 (= resource 경쟁 상태가 아닌경우)
atomic operation 을 수행하지 않는 lock 을 제공하여 수행 속도를 향상시키는 기법이다.
요약하면, biased lock = atomic operation을 하지 않는 lock = 흉내만 내는 lock 이다.
Dice-Moir-Scherer QR lock 최초 논문
hotspot에서의 biased lock 을 설명한 오라클 블로그
openJDK wiki 중 synchronization
implementation
openJDK8에서 bytecode interpreter가 synchronized method를 해석하는 구현. 모든 invoke 된 method는 이 구문에 진입하는데, synchronized keyword가 있으면 다음의 순서로 단계적으로 lock 획득을 시도한다.
- lock 최초 획득 – 이때 오브젝트 헤더에 thread id 와 bias flag 를 설정한다.
- biased lock 획득 시도 – 오브젝트 헤더와 동일한 thread 접근 시
- reboke bias – biased lock 획득을 실패
- thin lock 시도 – aging count 증가시킨다
- inflate lock 시도
- re-bias
hotspot/share/vm/interpreter/bytecodeInterpreter.cpp line:683
//bytecode interpreter 가 method 진입 case method_entry: { ... 중략 ... // synchronized method 이면 Lock if (METHOD->is_synchronized()) { ... 중략 ... // 가장 먼저 biased lock 이 가능한지 확인한다. // revoke 가능하면 revoke // 아니면 re-bias // 아니면 anonymously bias if (mark->has_bias_pattern()) { uintptr_t thread_ident; uintptr_t anticipated_bias_locking_value; thread_ident = (uintptr_t)istate->thread(); anticipated_bias_locking_value = (((uintptr_t)rcvr->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) & ~((uintptr_t) markOopDesc::age_mask_in_place); if (anticipated_bias_locking_value == 0) { // Already biased towards this thread, nothing to do. if (PrintBiasedLockingStatistics) { (* BiasedLocking::biased_lock_entry_count_addr())++; } success = true; } else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) { // Try to revoke bias. markOop header = rcvr->klass()->prototype_header(); if (hash != markOopDesc::no_hash) { header = header->copy_set_hash(hash); } if (Atomic::cmpxchg_ptr(header, rcvr->mark_addr(), mark) == mark) { if (PrintBiasedLockingStatistics) (*BiasedLocking::revoked_lock_entry_count_addr())++; } } else if ((anticipated_bias_locking_value & epoch_mask_in_place) != 0) { // Try to rebias. markOop new_header = (markOop) ( (intptr_t) rcvr->klass()->prototype_header() | thread_ident); if (hash != markOopDesc::no_hash) { new_header = new_header->copy_set_hash(hash); } if (Atomic::cmpxchg_ptr((void*)new_header, rcvr->mark_addr(), mark) == mark) { if (PrintBiasedLockingStatistics) { (* BiasedLocking::rebiased_lock_entry_count_addr())++; } } else { CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception); } success = true; } else { // Try to bias towards thread in case object is anonymously biased. markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place | (uintptr_t)markOopDesc::age_mask_in_place | epoch_mask_in_place)); if (hash != markOopDesc::no_hash) { header = header->copy_set_hash(hash); } markOop new_header = (markOop) ((uintptr_t) header | thread_ident); // Debugging hint. DEBUG_ONLY(mon->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);) if (Atomic::cmpxchg_ptr((void*)new_header, rcvr->mark_addr(), header) == header) { if (PrintBiasedLockingStatistics) { (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++; } } else { CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception); } success = true; } } // biased lock 이 실패하면 기존 방식대로 lock을 획득한다. // Traditional lightweight locking. if (!success) { markOop displaced = rcvr->mark()->set_unlocked(); mon->lock()->set_displaced_header(displaced); bool call_vm = UseHeavyMonitors; if (call_vm || Atomic::cmpxchg_ptr(mon, rcvr->mark_addr(), displaced) != displaced) { // Is it simple recursive case? if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) { mon->lock()->set_displaced_header(NULL); } else { CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception); } } } } THREAD->clr_do_not_unlock();
아래 코드는 monitorenter / monitorexit 구현이다. 궁극적으로 synchronized method나 synchronized block 모두 InterpreterRuntime::monitorenter() 메소드를 호출한다.
hotspot/share/vm/interpreter/interpreterRuntime.cpp line:606
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); } 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 //%note monitor_1 IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem)) #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object"); if (elem == NULL || h_obj()->is_unlocked()) { THROW(vmSymbols::java_lang_IllegalMonitorStateException()); } ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread); // Free entry. This must be done here, since a pending exception might be installed on // exit. If it is not cleared, the exception handling code will try to unlock the monitor again. elem->set_obj(NULL); #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif IRT_END
synchronized block
synchronized block은 해당하는 block의 시작과 끝을 java compiler가 monitorenter / monitorexit 로 변환한다.
아래 예제코드는 synchronized block 과 synchronized method 를 bytecode 로 변환하면 어떻게 되는지 보여준다.
public class SynchronizedExample { public static void main(String[] args) { Foo foo = new Foo(); synchronized (foo) { ++foo.foo; } methodFoo(foo); } static synchronized void methodFoo(Foo foo) { ++foo.foo; } static class Foo { public int foo; } }
main 메쏘드. synchronized block 은 monitorenter 로 진입, monitorexit 로 끝나지만 synchronized method는 메소드를 invoke할 뿐이다.
0 new #2 <SynchronizedExample$Foo> 3 dup 4 invokespecial #3 <SynchronizedExample$Foo.<init>> 7 astore_1 8 aload_1 9 dup 10 astore_2 11 monitorenter //synchronized block 시작 12 aload_1 13 dup 14 getfield #4 <SynchronizedExample$Foo.foo> 17 iconst_1 18 iadd 19 putfield #4 <SynchronizedExample$Foo.foo> 22 aload_2 23 monitorexit // synchronized block 끝 24 goto 32 (+8) 27 astore_3 28 aload_2 29 monitorexit 30 aload_3 31 athrow 32 aload_1 33 invokestatic #5 <SynchronizedExample.methodFoo> // method invoke 36 return
synchronized 메소드
0 aload_0 1 dup 2 getfield #5 <SynchronizedExample$Foo.foo> 5 iconst_1 6 iadd 7 putfield #5 <SynchronizedExample$Foo.foo> 10 return
결론
Lock object 를 명시적으로 사용하는 것 보다 synchronized keyword 를 사용하였을 때 유리한 경우는 다음과 같다.
- 리소스가 공유될 필요는 있으나, 빈도가 적다.(=경쟁적이지 않다.)
- 공유 리소스에 한 번 접근하였던 thread가 재진입하는 경우가 빈번하다.