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 인스트럭션으로
수행한다.

글에 착오가 있어 수정한다.

  1. synchronized block은 컴파일러에 의해 monitorenter / monitorexit 인스트럭션으로 변환이 되는 반면,
  2. 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 획득을 시도한다.

  1. lock 최초 획득 – 이때 오브젝트 헤더에 thread id 와 bias flag 를 설정한다.
  2. biased lock 획득 시도 – 오브젝트 헤더와 동일한 thread 접근 시
  3. reboke bias – biased lock 획득을 실패
    1. thin lock 시도 – aging count 증가시킨다
    2. inflate lock 시도
  4. 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가 재진입하는 경우가 빈번하다.

“java synchronization internal” 에 하나의 답글

  1. 좋은글 감사합니다

댓글 남기기

인기 검색어

01010011에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

Continue reading