ReentrantLock
공식문서를 보고 학습한 내용을 아카이브해보았습니다.
ReentrantLock (Java Platform SE 8 )
Acquires the lock only if it is not held by another thread at the time of invocation. Acquires the lock if it is not held by another thread and returns immediately with the value true, setting the lock hold count to one. Even when this lock has been set to
docs.oracle.com
ReentrantLock이란?
A reentrant mutual exclusionLockwith the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.
A ReentrantLock is owned by the thread last successfully locking, but not yet unlocking it. A thread invoking lock will return, successfully acquiring the lock, when the lock is not owned by another thread. The method will return immediately if the current thread already owns the lock. This can be checked using methods isHeldByCurrentThread(), and getHoldCount().
The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation. Note however, that fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock. Also note that the untimed tryLock() method does not honor the fairness setting. It will succeed if the lock is available even if other threads are waiting.
ReentrantLock은 synchronized 키워드처럼 동시에 하나의 스레드만 임계 영역에 들어올 수 있도록 해주는 락(lock)입니다. 둘 다 재진입 가능(reentrant) 하기 때문에, 이미 락을 획득한 스레드가 다시 같은 락을 요청해도 막히지 않고 그냥 통과할 수 있습니다.
예를 들어, 어떤 스레드 A가 lock()을 호출해서 락을 잡고 있는데, 그 A 스레드가 다시 lock()을 호출하더라도 문제없이 진행됩니다.
나중에 unlock()을 그만큼 여러 번 호출해야 완전히 락을 풀 수 있습니다.
이건 isHeldByCurrentThread(), getHoldCount() 같은 메서드로 확인할 수도 있습니다.
ReentrantLock은 Lock이 반환될 경우 기다리는 스레드는 어떤 순서로 Lock을 흭득할 수 있을까?
이에 대한 의문에 대한 답은 아래와 같습니다.
공정성(Fairness) 설정
ReentrantLock 생성자에는 공정성 설정을 할 수 있는 옵션이 있습니다.
- new ReentrantLock(true) 처럼 true로 설정하면, 락을 기다리는 순서대로(=먼저 기다린 스레드부터) 락을 획득합니다.
- → 즉, 대기 줄이 있고, 줄 선 순서대로 처리되는 구조
- 반면, 기본값인 false로 하면, 락을 기다리는 스레드 중 운 좋은 놈이 먼저 가져갑니다.
- → 성능은 좋지만, 운이 나쁘면 계속 못 잡는 기아(starvation) 현상이 생길 수도 있습니다.
단, 아무리 공정하게 설정했더라도 운영체제의 스레드 스케줄링에 따라 예상치 못한 순서가 발생할 수 있고, tryLock()처럼 대기하지 않고 바로 시도하는 메서드는 공정성 설정을 따르지 않습니다.
예시 코드
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
- .lock()을 통해서 락 흭득을 시도합니다.
- 이후 finally 문을 활용하여, 해당 메서드의 흐름이 종료되면 락을 반환합니다.
- 만일 락이 반환되지않는다면, 해당 메서드를 사용하는 스레드는 무한대기를 하게 될 것입니다.
직렬화와 같은 스레드의 재진입 횟수 제한
In addition to implementing the Lock interface, this class defines a number of public and protected methods for inspecting the state of the lock. Some of these methods are only useful for instrumentation and monitoring.
Serialization of this class behaves in the same way as built-in locks: a deserialized lock is in the unlocked state, regardless of its state when serialized.
This lock supports a maximum of 2147483647 recursive locks by the same thread. Attempts to exceed this limit result in Error throws from locking methods.
직렬화(Serialization) 관련 설명
이 락은 자바의 기본 락(synchronized 사용 시처럼)과 동일하게 직렬화 시 상태가 유지되지 않습니다.
- 예를 들어, ReentrantLock을 어떤 객체와 함께 파일에 저장하고 다시 불러오면,
- 락은 무조건 풀린 상태(unlocked) 로 초기화됩니다.
- 즉, 직렬화 전에 누가 락을 잡고 있었는지는 저장되지 않습니다.
→ 복원된 객체에서 락은 새로 시작하는 것과 같음.
재진입 횟수 제한
ReentrantLock은 같은 스레드가 여러 번 락을 걸 수 있는 재진입 기능을 지원합니다.
그런데 이걸 무제한으로 허용하면 문제가 생길 수 있으니, 최대 2,147,483,647번까지만 재진입이 가능합니다. (2의 31승 - 1, 즉 Integer.MAX_VALUE)
- 한 스레드가 lock()을 너무 많이 호출해서 이 숫자를 넘으면,
- lock() 메서드에서 Error 예외가 던져집니다.
→ 현실에서는 거의 발생하지 않지만, 이론적으로 무한 루프나 실수로 재귀 락을 너무 많이 걸었을 때 방어용으로 걸어둔 제한이에요.
따라서 Syncronized와 같이 lock을 활용할 수 있는 객체입니다. 그러나 어떤 점이 다를까요?
Syncronized와 ReentrantLock의 차이
synchronized와 ReentrantLock은 모두 상호 배제를 보장하는 동기화 수단으로, 특정 코드 블록이나 메서드에 대해 동시에 하나의 스레드만 접근 가능하도록 제어할 수 있습니다. 그러나 다음과 같은 차이점이 있습니다.
1. 명시적(lock/unlock) vs 암묵적 진입/해제
- synchronized는 블록이나 메서드에 진입하면 자동으로 락이 걸리고, 블록을 벗어나면 자동으로 해제됩니다.
- 반면 ReentrantLock은 직접 lock()으로 락을 걸고 unlock()으로 해제해야 하며, try-finally 블록을 사용하는 것이 필수적입니다.
lock.lock();
try {
// 보호할 코드
} finally {
lock.unlock();
}
2. 공정성(Fairness) 설정 지원
- ReentrantLock은 생성자에서 공정성 여부를 설정할 수 있습니다 (new ReentrantLock(true)).
- synchronized는 공정성 설정이 불가능하며, JVM이 스케줄링 정책에 따라 스레드에게 락을 부여합니다.
3. 인터럽트 대응(lockInterruptibly)
- ReentrantLock은 락 대기 중인 스레드가 인터럽트를 받을 수 있도록 설정할 수 있습니다.
- synchronized는 락을 대기 중인 스레드가 인터럽트에 반응하지 않습니다.
4. tryLock 등 다양한 제어 방식
- ReentrantLock은 즉시 락 시도 (tryLock()), 타임아웃 대기 (tryLock(timeout, unit)) 등 다양한 락 획득 방법을 제공합니다.
- synchronized는 무조건 대기이며, 락 획득 여부를 선택적으로 처리할 수 없습니다.
5. Condition 지원 (wait/notify 대체)
- ReentrantLock은 여러 개의 조건 변수를 newCondition()으로 생성할 수 있으며, 이를 통해 세분화된 wait-notify 제어가 가능합니다.
- synchronized는 객체 모니터에서 제공하는 wait(), notify(), notifyAll()만 사용할 수 있습니다.
6. 모니터링 및 상태 확인 API 제공
- ReentrantLock은 isLocked(), getHoldCount(), isHeldByCurrentThread() 등 락의 상태를 확인할 수 있는 API들을 제공합니다.
- synchronized는 이런 정보를 직접 조회할 수 있는 수단이 없습니다.
결론
- 간단한 경우에는 synchronized가 간결하고 안전하게 사용할 수 있습니다.
- 보다 정교한 락 제어, 공정성 보장, 조건 제어, 모니터링이 필요한 경우에는 ReentrantLock이 유리합니다.
- 단, ReentrantLock은 해제를 누락하면 데드락이 발생할 수 있기 때문에, 반드시 try-finally로 사용해야 합니다.
이는 스레드의 생명주기와 관련된 내용이 많기에 추후에 상세하게 차이점을 정리해보겠습니다.
메서드 지원
기본 락 관련 메서드
lock()
- 다른 스레드가 락을 보유하고 있지 않다면 즉시 락을 획득하며, hold count를 1로 설정합니다.
- 현재 스레드가 이미 보유 중이라면 hold count만 증가시키고 바로 반환합니다.
- 락이 다른 스레드에 의해 점유 중이라면, 현재 스레드는 스케줄링 대상에서 제외되고 대기합니다.
lockInterruptibly()
- InterruptedException에 반응하는 락 획득 메서드입니다.
- 대기 중 인터럽트가 발생하면 예외를 던지고, 인터럽트 상태를 초기화합니다.
- 공정성 여부와 상관없이 인터럽트 응답이 우선시됩니다.
tryLock()
- 락이 비어 있다면 즉시 락을 획득하고 true를 반환합니다.
- 공정성 여부에 상관없이 락이 가능하면 먼저 들어옵니다(선입선출 무시).
- 락이 점유 중이면 즉시 false를 반환합니다.
tryLock(long timeout, TimeUnit unit)
- 주어진 시간 동안 락을 기다리며, 그 시간 내에 락을 획득하면 true를 반환합니다.
- 공정성 정책이 설정된 경우, 락 대기 중인 스레드가 있다면 먼저 기다립니다.
- 대기 중 인터럽트가 발생하면 InterruptedException이 발생합니다.
- 제한 시간 경과 시 false를 반환합니다.
unlock()
- 현재 스레드가 락을 보유 중이라면 hold count를 1 감소시킵니다.
- hold count가 0이 되면 락을 해제합니다.
- 소유자가 아닌 스레드가 호출하면 IllegalMonitorStateException이 발생합니다.
조건 변수 관련
newCondition()
- Condition 객체를 생성합니다.
- Java의 Object.wait()/notify()와 비슷한 역할을 하며, 락과 함께 사용할 수 있는 await()/signal() 기능을 제공합니다.
- 락을 보유하지 않은 상태에서 호출하면 IllegalMonitorStateException이 발생합니다.
상태 조회 및 모니터링
getHoldCount()
- 현재 스레드가 이 락을 몇 번 보유하고 있는지를 반환합니다.
- 디버깅이나 테스트 시 유용합니다.
isHeldByCurrentThread()
- 현재 스레드가 이 락을 보유 중인지 확인합니다.
isLocked()
- 어떤 스레드든 이 락을 보유 중인지 여부를 반환합니다.
isFair()
- 이 락이 공정성 설정(true)을 사용 중인지 반환합니다.
getOwner()
- 이 락을 소유 중인 스레드를 반환합니다. 없으면 null을 반환합니다.
대기열 상태 확인
hasQueuedThreads()
- 현재 락을 기다리는 스레드가 있는지 여부를 반환합니다.
hasQueuedThread(Thread thread)
- 특정 스레드가 락을 기다리고 있는지 여부를 반환합니다.
getQueueLength()
- 현재 락을 대기 중인 스레드 수를 추정치로 반환합니다.
getQueuedThreads()
- 락을 기다리는 스레드들을 포함한 컬렉션을 반환합니다. 순서는 보장되지 않습니다.
Condition 대기열 상태 확인
hasWaiters(Condition condition)
- 주어진 조건 변수에서 기다리고 있는 스레드가 있는지 반환합니다.
getWaitQueueLength(Condition condition)
- 주어진 조건 변수에서 대기 중인 스레드의 수를 추정치로 반환합니다.
getWaitingThreads(Condition condition)
- 조건 변수에서 대기 중인 스레드들을 컬렉션으로 반환합니다.
문자열 표현
toString()
- 락의 상태(잠김 여부, 소유 스레드 정보 포함)를 문자열로 반환합니다. 예: "java.util.concurrent.locks.ReentrantLock[Locked by Thread-1]"
'Dev Lang > Java' 카테고리의 다른 글
[Java] 람다 완전 정리 – 함수형 프로그래밍의 시작 (1) | 2025.05.19 |
---|---|
[Java] 자바 스레드에서 run()은 왜 체크 예외를 던질 수 없을까? (0) | 2025.05.12 |
[Java] 프록시 패턴을 통한 멀티 스레드 환경 조성하기 (0) | 2025.04.04 |
[Java] 제네릭 클래스의 이해와 활용 (0) | 2025.01.06 |
[JAVA] 자바의 기본형과 참조형의 값 공유 특성 (1) | 2024.12.28 |