자바에서 스레드 간의 동기화를 위해서 synchronized 키워드를 사용하는 것이 가장 기본적인 방법일 것이다. 이 synchronized 키워드를 사용하는 Lock을 Intrinsic Lock 혹은 Built-in Lock이라 하며, Java 1.5부터 java.util.concurrent.locks 패키지를 통해서 제공되는 Explicit Lock과 대비된다. 둘 다 공유 자원을 접근하는 Critical Section을 보호하기 위한 목적으로 사용하긴 하는데, 이 둘의 차이는 무엇일까?
이 포스팅에서는 가장 기본적인 두 가지 Lock, 자바의 Built-in(Intrinsic) Lock인 Monitor와 명시적 Lock인 ReentrantLock에 대해서 비교해 보고자 한다. Oracle Hotspot JDK 1.8을 기준으로 정리하는 것이니 참고하시길 바란다.
Lock의 역할
Lock을 사용하는 이유는 무엇일까?
첫 번째는 Critical Section에 대해서 상호 배제(Mutual Exclusion)을 제공하는 것이다. 즉, 공유하는 데이터에 대해서 여러 스레드 중에서 한 스레드만 접근할 수 있도록 허용 및 제한하는 것을 의미한다.
두 번째는 스레드 간의 협업 또는 동기화(Cooperation)의 역할을 수행한다. 어떠한 작업을 완료하거나 특정 상태에 도달할 경우 다른 스레드에 알려주어 해당 스레드가 동작할 수 있도록 하는 것이다.
세 번째는 Lock을 사용할 경우 부수적인 효과로 발생하는 데이터의 가시성(Visibility) 이다. 이는 lock/unlock을 호출하기 전에 변경된 데이터에 대해서 메모리로 반영하여 것을 의미한다. 또한, 옵티마이저에 의해서 메모리 접근 순서가 변경되는 것을 막는다. 물론, 데이터 가시성을 위해서 Lock을 사용하는 사람은 없긴 하겠지만 Lock의 기능 중 하나이기 때문에 참고만 하도록 하자.
사용상의 차이점
Monitor는 Critical Section을 보호하기 위해 메서드나 코드 블록 단위에 “synchronized” 키워드를 사용하는 반면 ReentrantLock은 lock()과 unlock() API 명시적으로 사용한다.
항목 |
Monitor(Intrinsic Lock) |
ReentrantLock(Explicit Lock) |
Mutual Exclusion |
public synchronized void func() { … … } |
public void func() { lock.lock(); try { … } finally { lock.unlock() } } |
public void func() { … synchronized(this) { … } } |
||
Wait for Cooperation |
synchronized(this) { … this.wait(); … } |
Condition c = lock.newConditition(); lock.lock() try { … c.await(); … } finally { lock.unlock(); } |
Signal for Cooperation |
synchronized(this) { … this.notify(); … } |
Condition c = lock.newConditition(); try { … c.signal(); … } finally { lock.unlock(); } |
동작(구현)의 차이점
Intrinsic Lock(Built-in Lock)의 동작 방식
Intrinsic Lock은 메모리에 저장되는 오브젝트 헤더의 데이터와 모니터 객체를 기반으로 동작한다.
오브젝트 헤더의 가장 첫 데이터인 Mark Word는 Intrinsic Lock과 Garbage Collection을 위한 정보를 저장하기 위해 사용된다. Mark Word는 Biased와 Tag 필드의 데이터에 의해 5 가지 상태 중 하나를 표현하는데 Unlock, Biased, Light-Wight Locked, Heavy-Weight Locked, Marked For GC 상태이다. 이 중에서 Biased(Biasable), Light-Weight Locked, Heavy-Weight Locked 상태가 Lock을 획득하였음을 표현하고 있다. Biased, Light-Weight Locked, Heavy-Weight Locked 순서로 Lock 획득과 반환의 성능이 우수하며, 우리가 일반적으로 알고 있는 Monitor에 의한 전통적인 방식은 Heavy-Weight Lock을 의미한다. 또한, 최대한 Biased Lock을 획득하려고 하지만, 그렇지 못할 경우(Lock Contention의 정도에 따라서) Light-Weight Lock, Heavy-Weight Lock 순서로 Lock을 Upgrade 하여 획득을 시도한다.
각각의 Lock의 동작 방식에 대해서 알아보도록 하자.
Heavy-weight lock
가장 전통적인 Lock의 동작 방식이다. Lock을 획득하기 위해서 Monitor Object를 사용하고 Monitor 획득에 성공할 경우 계속해서 실행되며, Monitor 획득에 실패할 경우 Entry-Set에서 실행을 대기하게 된다(State.BLOCKED 상태) . 만약 wait()를 사용하여 대기할 경우에는 Wait-Set Queue에서 실행을 대기하게 된다. 이러한 전통적인 Lock 동작 방식은 운영체제가 제공하는 Lock 매커니즘을 사용하게 되는데, 이는 락을 획득하지 못하 경우 User space에서 Kernel Space로 Context 스위치가 발생하고, 추후에 Lock을 획득하여 동작하는 경우에는 Kernel Space에서 User Space로의 Context Switch가 발생한다.
Light-weight lock
경험적으로 Lock을 획득하는 과정에서 다른 스레드와 경합하는 일이 많지 않고 실제 경합하더라도 다른 스레드가 Lock을 빠르게 내려놓기 때문에 대기할 일은 많지 않다고 한다. 그래서 Spin-Lock 방식으로 Lock을 획득하기 위해서 CPU를 소모하면서 대기하는 작업을 수행한다. 내부적으로는CAS(Compare And Swap)을 사용한다. 하지만, 이는 실제 Lock을 획득하는 과정에서 경쟁 스레드가 존재할 경우 Heavy-Weight Lock보다 더 많은 자원을 소모하고 느릴 수 있다.
Biased lock
Java 1.6에서 소개된 Lock Optimization 기법이다. Lock 획득 시 경쟁이 자주 발생하지 않는 상황(즉, 주로 한 스레드에서만 Lock 획득을 자주 하는 상황)에서의 성능 향상을 위해서 도입되었다. 한 스레드가 Lock을 획득하고 반납 후 재 획득하는 사이에 다른 스레드가 획득한 이력이 없을 경우, 별도의 오퍼레이션 없이(실제 Object의 헤더에서 자신의 Thread ID를 가리키고 있는지는 검사한다) Lock을 획득하는 것이 가능하다.
기능 상의 차이점
|
Monitor (Intrinsic Lock) |
ReentrantLock (Explcit LocK) |
재진입 |
가능 |
가능 |
락 시도 |
미제공 |
제공 |
Fairness |
미제공 |
제공 |
Interruption in locked |
불가능 |
가능 |
Lock 접근 스레드 정보 |
미제공 |
제공 |
Trying lock
ReentrantLock의 경우 tryLock() API를 사용하여 Lock 획득을 시도할 수 있다.
Fairness
ReentrantLock의 생성자에 fainess 관련 파라미터를 ‘True’로 설정할 경우, 스레드 간의 경합에서 공정성을 유지하여 starvation 문제를 피할 수 있다. 하지만, 사용하지 않는 경우에 비해서 성능 하락이 올 수 있다.
Interruption in locked
Lock을 획득한 상태에서 interrupt가 발생할 경우 InterruptedException의 발생 여부인데, ReentrantLock의 경우에는 해당 예외가 발생할 수 있도록 lockInterruptibly(true)로 lock을 획득 할 수 있다. 이 경우에는 lock을 반납하는 unlock() API를 주의해서 호출해야 한다.
Lock 접근 스레드 정보
ReetnrantLock의 경우엔 현재 락 획득을 대기하는 스레드 목록 등의 정보를 확인 가능하다.
성능상의 차이점
https://mechanical-sympathy.blogspot.com/2011/11/java-lock-implementations.html
https://mechanical-sympathy.blogspot.com/2011/11/biased-locking-osr-and-benchmarking-fun.html
그 밖의 차이점
VisualVM으로 스레드를 모니터링할 경우, Monitor 획득을 대기하는 경우에는 'Wait'으로 표기되고 Explicit Lock을 획득하기 위해서 대기하는 경우에는 'Park' 상태로 표시된다.
결론: 어떤 것을 사용해야 하는가?
성능적인 면에서, Lock Contention이 자주 발생하지 않고 적은 개수의 스레드의 Lock Contention이 발생하는 상황이라면 Monitor를 사용하는 것이 성능에 이득일 것이다. 하지만, 다수의 스레드가 빈번히 Lock Contention을 발생하는 상황에서는 ReentrantLock을 사용하는 것이 이득이다.
기능적인 면에서, 일반적인 Mutual Exclusion과 Cooperation을 위한 대부분의 경우 Monitor를 사용하면 되겠지만, 정교한 제어(Fairness, Try Lock, Interruption in locked,…)를 위해서는 ReentrantLock과 같은 명시적 Lock을 사용하면 좋을 것이다.
[References]
https://www.programmersought.com/article/407747922/
https://www.programmersought.com/article/407747922/
https://01010011.blog/2017/01/20/java-synchronization-internal/
https://medium.com/swlh/difference-between-java-monitor-and-lock-4677c1b6715f
https://howtodoinjava.com/java/multi-threading/multithreading-difference-between-lock-and-monitor/
https://medium.com/swlh/difference-between-java-monitor-and-lock-4677c1b6715f
https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html
https://mechanical-sympathy.blogspot.com/2011/11/java-lock-implementations.html
https://mechanical-sympathy.blogspot.com/2011/11/biased-locking-osr-and-benchmarking-fun.html
https://www.programmersought.com/article/407747922/
https://01010011.blog/2017/01/20/java-synchronization-internal/
https://medium.com/swlh/difference-between-java-monitor-and-lock-4677c1b6715f
https://howtodoinjava.com/java/multi-threading/multithreading-difference-between-lock-and-monitor/
댓글