ReentrantLock은 스레드가 재귀적으로 같은 락을 획득할 수 있는 락입니다. 여기서 락은 공정모드(fair mode)와 비공정모드(unfair mode) 두 가지로 나뉩니다.
공정모드(Fair Mode): 락을 요청한 순서대로 스레드가 락을 획득하게 하는 방식입니다. 즉, 먼저 락을 요청한 스레드가 먼저 락을 획득합니다. 공정모드는 ReentrantLock 생성 시 new ReentrantLock(true)로 설정할 수 있습니다. 하지만 이 모드는 성능이 상대적으로 떨어질 수 있습니다. 왜냐하면, 스레드가 순서를 기다려야 하기 때문에 경쟁이 심화될 수 있기 때문입니다.
비공정모드(Unfair Mode): 비공정모드는 락을 요청한 스레드들이 락을 획득하는 데 있어서 순서를 보장하지 않는 방식입니다. 락을 요청한 스레드가 락을 바로 획득할 수 있으면 락을 바로 잡고 실행을 시작합니다. 비공정모드는 기본 모드(new ReentrantLock(false) 또는 아무 인자 없이 생성)로 설정되며, 성능 면에서 공정모드보다 유리할 수 있습니다.
ReentrantLock은 synchronized와 비슷한 역할을 하지만, 더 유연하게 사용될 수 있습니다. 특히, Condition 객체를 이용해서 락과 대기 큐를 보다 세밀하게 제어할 수 있습니다.
Condition 객체 생성: ReentrantLock은 내부적으로 여러 Condition을 가질 수 있습니다. Condition은 lock.newCondition()을 통해 생성됩니다. 하나의 락에 여러 대기 조건을 가질 수 있다는 점에서 Object.wait()와 Object.notify()보다 더 유연합니다.
await(): Condition.await()는 현재 스레드가 대기하도록 만듭니다. 이때 스레드는 락을 해제한 상태에서 대기하게 되며, 다른 스레드가 signal()을 호출할 때까지 기다립니다. 이는 Object.wait()와 유사한 역할을 합니다.
signal(): Condition.signal()은 대기 중인 스레드 중 하나를 깨워 실행 상태로 전환합니다. 이 메서드는 Object.notify()와 비슷합니다. 대기 중인 모든 스레드를 깨우고 싶다면 signalAll()을 사용하면 됩니다.
notify (Object 클래스):
signal (Condition 인터페이스):
synchronized를 사용할 때 스레드가 두 가지 주요 대기 상태로 나뉩니다.
BlockingQueue는 여러 스레드가 안전하게 데이터를 추가하거나 제거할 수 있는 큐입니다. 이때 스레드가 큐에 대해 다양한 방식으로 대기할 수 있습니다.
공정모드(Fair Mode)는 락을 요청한 순서대로 스레드가 락을 획득할 수 있도록 보장합니다. 이로 인해 락을 요청한 순서대로 락을 스레드가 차례로 받게 되므로 컨텍스트 스위칭이 더 자주 발생할 수 있습니다. 여러 스레드가 동일한 락을 기다리고 있을 때, 스레드 스케줄러가 공정하게 락을 배분하기 위해 추가적인 작업을 수행하게 되면서 성능이 떨어질 수 있습니다.
비공정모드(Unfair Mode)는 락을 바로 사용할 수 있는 스레드가 즉시 락을 획득하게 됩니다. 이를 통해 불필요한 컨텍스트 스위칭이 줄어들고, 락을 빠르게 잡고 작업을 수행할 수 있어 성능이 상대적으로 향상될 수 있습니다. 이 방식은 시스템이 최적화되어 락 경쟁이 적거나 특정 순서가 중요하지 않은 경우 유리합니다.
따라서, 공정모드는 스레드 간의 공평성을 보장하지만, 성능 손실이 있을 수 있고, 비공정모드는 성능 최적화 측면에서 더 유리하지만, 스레드 간의 순서 보장은 하지 못합니다.
ReentrantLock의 여러 Condition을 사용할 수 있는 기능은 다음과 같은 장점을 제공합니다:
세분화된 대기 조건 관리: 여러 개의 Condition을 통해 서로 다른 조건에 대해 스레드들이 대기할 수 있습니다. 예를 들어, 생산자-소비자 패턴에서 생산자가 특정 조건에 따라 대기하고, 소비자는 다른 조건에 따라 대기할 수 있습니다. 이처럼 대기 조건을 세분화하면, 한 조건이 만족되었을 때 다른 대기 중인 스레드를 깨우지 않게 되어 불필요한 깨어남을 방지할 수 있습니다.
성능 최적화: 모든 스레드가 하나의 모니터 대기 큐에서 관리되는 notify() 방식과 달리, Condition은 다양한 대기 큐를 사용할 수 있습니다. 필요에 따라 해당 조건을 만족하는 대기 중인 스레드만 선택적으로 깨울 수 있기 때문에, 전체 성능을 향상시킬 수 있습니다.
직관적 코드 구조: 서로 다른 대기 조건을 가진 Condition을 사용하면 코드의 가독성과 유지보수성이 높아집니다. 각각의 Condition을 명확하게 구분해 사용할 수 있기 때문에, 대기 중인 스레드가 어떤 조건에서 대기하고 있는지 더 명확하게 이해할 수 있습니다.
BlockingQueue에서 대기하지 않고 작업을 처리해야 할 때 사용할 수 있는 주요 전략은 다음과 같습니다:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
boolean success = queue.offer(5);
if (!success) {
// 큐가 꽉 찬 경우의 예외 처리
System.out.println("큐가 가득 차서 작업을 추가할 수 없습니다.");
}
try {
queue.add(5); // 큐가 꽉 찬 경우 예외 발생
} catch (IllegalStateException e) {
// 예외 처리
System.out.println("큐가 가득 찼습니다: " + e.getMessage());
}
Integer item = queue.poll();
if (item == null) {
// 큐가 비어 있을 때 처리
System.out.println("큐에 데이터가 없습니다.");
}
이러한 방식들은 모두 대기하지 않고 즉시 결과를 처리할 수 있는 방법들로, 각 방법을 상황에 맞게 선택할 수 있습니다.