자바에서 동기화를 위해 synchronized 키워드를 많이 사용하지만, 몇 가지 단점이 있습니다. 이를 해결하기 위해 Lock 인터페이스와 ReentrantLock이 등장했습니다. 이번 글에서는 synchronized의 단점과 Lock의 동작 방식에 대해 쉽게 설명하겠습니다.


🔒 synchronized의 단점

1️⃣ 무한 대기 (Blocked 상태)

synchronized를 사용하면, 스레드가 락을 획득하지 못할 경우 무한 대기하게 됩니다. 즉, 락이 풀릴 때까지 계속 기다려야 하며 다음과 같은 기능이 없습니다.

  • 특정 시간까지만 대기하는 타임아웃 기능❌
  • 중간에 다른 스레드가 강제로 깨우는 인터럽트 기능❌

이러한 문제 때문에 스레드가 영원히 실행되지 못하는 상황이 발생할 수도 있습니다.

2️⃣ 공정성 문제

락이 풀렸을 때 어떤 스레드가 락을 획득할지 보장되지 않습니다. 여러 개의 스레드가 대기 중이라면 특정 스레드가 너무 오랫동안 락을 얻지 못할 수도 있음 (기아 현상 발생 가능).

➡️ 이러한 문제를 해결하기 위해 Lock 인터페이스와 ReentrantLock이 등장했습니다.


🔑 Lock과 LockSupport 활용

Lock 인터페이스는 synchronized와 다르게 객체 내부에 있는 모니터 락을 사용하지 않고, 별도로 락을 관리할 수 있습니다. 대표적인 구현체는 ReentrantLock입니다.

✅ Lock 선언 및 사용 방법

private final Lock lock = new ReentrantLock();

synchronized(this) 대신 lock.lock()을 사용하여 락을 획득합니다.

lock.lock();
try {
    // 임계 영역
} finally {
    lock.unlock(); // 반드시 unlock() 호출해야 함!
}

💡주의! lock.unlock()은 반드시 finally 블록에서 호출해야 합니다.
그래야 중간에 예외가 발생해도 락이 정상적으로 해제됨.


⚡ Lock의 대기 큐와 LockSupport

ReentrantLock 내부에는 대기 큐가 존재합니다. 락을 얻지 못한 스레드는 대기 큐에 저장되고 이후 락이 해제되면 차례로 깨워집니다.

✅ LockSupport 활용

자바에서 스레드를 대기 상태로 만들고 깨우는 기능은 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을 사용하면 더 유연한 대기 방식이 가능함.


🔄 ReentrantLock 동작 방식

1️⃣ 스레드 t2가 락을 획득하지 못하면 Waiting 상태가 되고 대기 큐에 등록됨 (LockSupport.park() 호출됨).

2️⃣ t1이 락을 사용한 후 unlock()을 호출하면 대기 큐에서 대기 중인 스레드를 깨움 (LockSupport.unpark(thread) 호출됨).

3️⃣ t2Runnable 상태로 전환되고, 다시 락을 획득하려고 시도함.

➡️ 이렇게 하면 synchronized보다 더 정교하게 동기화를 제어할 수 있음.


✅ 정리

비교synchronizedLock (ReentrantLock)
대기 방식무한 대기 (Blocked)타임아웃, 인터럽트 가능
공정성보장 Xfair 설정 가능 (공정한 락 분배)
사용 방식JVM 모니터 락 사용직접 락 관리 (더 유연함)
대기 상태BlockedWaiting, Timed_Waiting 지원

👉 synchronized는 간단하지만 제한적이고, Lock은 유연하지만 직접 관리해야 한다는 점이 다름!


🚀 결론

  • synchronized는 기본적인 동기화에는 적합하지만 무한 대기, 공정성 문제가 있음.
  • Lock (ReentrantLock)을 사용하면 더 유연한 락 제어가 가능하고 공정성도 보장할 수 있음.
  • 하지만 lock.lock()을 사용한 경우 반드시 unlock()명시적으로 호출해야 함 (안 하면 교착 상태 발생 가능!).
  • LockSupport를 활용하면 특정 스레드를 직접 깨울 수도 있음.

➡️ 따라서 복잡한 동기화가 필요한 경우 Lock을 적극적으로 활용하는 것이 좋음! 🚀

profile
배움을 추구하는 개발자

0개의 댓글