
자바에서 동기화를 위해 synchronized 키워드를 많이 사용하지만, 몇 가지 단점이 있습니다. 이를 해결하기 위해 Lock 인터페이스와 ReentrantLock이 등장했습니다. 이번 글에서는 synchronized의 단점과 Lock의 동작 방식에 대해 쉽게 설명하겠습니다.
synchronized를 사용하면, 스레드가 락을 획득하지 못할 경우 무한 대기하게 됩니다. 즉, 락이 풀릴 때까지 계속 기다려야 하며 다음과 같은 기능이 없습니다.
이러한 문제 때문에 스레드가 영원히 실행되지 못하는 상황이 발생할 수도 있습니다.
락이 풀렸을 때 어떤 스레드가 락을 획득할지 보장되지 않습니다. 여러 개의 스레드가 대기 중이라면 특정 스레드가 너무 오랫동안 락을 얻지 못할 수도 있음 (기아 현상 발생 가능).
➡️ 이러한 문제를 해결하기 위해 Lock 인터페이스와 ReentrantLock이 등장했습니다.
Lock 인터페이스는 synchronized와 다르게 객체 내부에 있는 모니터 락을 사용하지 않고, 별도로 락을 관리할 수 있습니다. 대표적인 구현체는 ReentrantLock입니다.
private final Lock lock = new ReentrantLock();
synchronized(this) 대신 lock.lock()을 사용하여 락을 획득합니다.
lock.lock();
try {
// 임계 영역
} finally {
lock.unlock(); // 반드시 unlock() 호출해야 함!
}
💡주의!
lock.unlock()은 반드시finally블록에서 호출해야 합니다.
그래야 중간에 예외가 발생해도 락이 정상적으로 해제됨.
ReentrantLock 내부에는 대기 큐가 존재합니다. 락을 얻지 못한 스레드는 대기 큐에 저장되고 이후 락이 해제되면 차례로 깨워집니다.
자바에서 스레드를 대기 상태로 만들고 깨우는 기능은 LockSupport 클래스를 활용합니다.
LockSupport.park();
위 코드를 실행하면 스레드는 WAITING 상태가 됩니다.
LockSupport.unpark(thread);
외부에서 스레드를 깨울 수 있습니다.
Blocked vs Waiting vs Timed_Waiting| 상태 | 특징 | 예시 |
|---|---|---|
Blocked | 락을 기다리는 상태, 인터럽트해도 빠져나올 수 없음 | synchronized 사용 시 발생 |
Waiting | 외부에서 깨울 때까지 대기 | LockSupport.park(), Object.wait() |
Timed_Waiting | 일정 시간만 기다림 | Thread.sleep(ms), LockSupport.parkNanos(ns) |
💡
synchronized에서는Blocked상태만 존재하지만,Lock을 사용하면 더 유연한 대기 방식이 가능함.
1️⃣ 스레드 t2가 락을 획득하지 못하면 Waiting 상태가 되고 대기 큐에 등록됨 (LockSupport.park() 호출됨).
2️⃣ t1이 락을 사용한 후 unlock()을 호출하면 대기 큐에서 대기 중인 스레드를 깨움 (LockSupport.unpark(thread) 호출됨).
3️⃣ t2는 Runnable 상태로 전환되고, 다시 락을 획득하려고 시도함.
➡️ 이렇게 하면 synchronized보다 더 정교하게 동기화를 제어할 수 있음.
| 비교 | synchronized | Lock (ReentrantLock) |
|---|---|---|
| 대기 방식 | 무한 대기 (Blocked) | 타임아웃, 인터럽트 가능 |
| 공정성 | 보장 X | fair 설정 가능 (공정한 락 분배) |
| 사용 방식 | JVM 모니터 락 사용 | 직접 락 관리 (더 유연함) |
| 대기 상태 | Blocked | Waiting, Timed_Waiting 지원 |
👉 synchronized는 간단하지만 제한적이고, Lock은 유연하지만 직접 관리해야 한다는 점이 다름!
synchronized는 기본적인 동기화에는 적합하지만 무한 대기, 공정성 문제가 있음.Lock (ReentrantLock)을 사용하면 더 유연한 락 제어가 가능하고 공정성도 보장할 수 있음.lock.lock()을 사용한 경우 반드시 unlock()을 명시적으로 호출해야 함 (안 하면 교착 상태 발생 가능!).LockSupport를 활용하면 특정 스레드를 직접 깨울 수도 있음.➡️ 따라서 복잡한 동기화가 필요한 경우 Lock을 적극적으로 활용하는 것이 좋음! 🚀