금일은 Java Thread 의 생산자와 소비자 문제에 있어 앞서 포스팅 글에서의 비효율성을 해결하는 방법에 대해 알아보겠습니다.
앞서 포스팅에 작성한 코드에 Lock
인터페이스오 ReentrantLock
구현체를 사용해서 구현해 보겠습니다.
private final Lock lock = new ReentrantLock();
private final Condition producerCond = lock.newCondition();
private final Condition consumerCond = lock.newCondition();
private final Queue<String> queue = new ArrayDeque<>();
private final int max;
public BoundedQueueV5(int max) {
this.max = max;
}
@Override
public void put(String data) {
lock.lock();
try {
while (queue.size() == max) {
log("[put] 큐가 가득 참, 생산자 대기");
try {
producerCond.await();
log("[put] 생산자 깨어남");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
queue.offer(data);
log("[put] 생산자 데이터 저장, consumerCond.signal() 호출");
consumerCond.signal();
} finally {
lock.unlock();
}
}
@Override
public String take() {
lock.lock();
try {
while (queue.isEmpty()) {
log("[take] 큐에 데이터가 없음, 소비자 대기");
try {
consumerCond.await();
log("[take] 소비자 깨어남");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
String data = queue.poll();
log("[take] 소비자 데이터 획득, producerCond.signal() 호출");
producerCond.signal();
return data;
} finally {
lock.unlock();
}
}
Condition.signal() 함수는 Object.notify() gka수와는 다르게 임의의 스레드를 깨우는 것이 아니라 fifo 순서로 깨우게 된다. 이 부분은 자바 버전과 구현에 따라 달라질 수 있지만 보통 Queue 구조를 사용하기 때문에 fifo 순서를 유지합니다.
또한 syncronized 블록 내에서 모니터 락을 가지는 스레드 호출이 아니라 ReentrantLock 을 가지고 있는 스레드가 호출해야 합니다.
사실은 BLOCKED 상태의 스레드도 자바 내부에서 따로 관리된다.
c2,c3 가 단순히 BLOCKED 상태돌 변경만 된것이 아니라 내부의 자료구조에 의해 관리 되어 지는 것이다.
스레드 대기 집합에 있는 c1 이 스레드 대기 집합을 빠져 나오게 되면 락을 얻어서 락 대기 집합까지 빠져나가야 임계 영역을 수행할 수 있는 것이다.
만약 락을 획득할 수 없으면 락 대기 집합에서 관리 되는 것이다.
자바의 모든 객체 인스턴스는 멀티스레드와 임계 영역을 다루기 위해 내부에 3가지 기본 요소를 가지게 됩니다.
이 3가지 요소는 서로 맞물려 돌아간다.
모니터 락 획득 대기
wait() 대기
ReentrantLock 락 획득 대기
await() 대기
사실 개념은 같다고 봐야된다. 단 락 대기 집합에서의 상태값만 다르다고 보면 된다. syncronized 의 BLOCKED 상태는 깨울 수 있는 방법이 없다.
반대로 ReentrantLock 은 WAITING 상태 이므로 중간에 깨울 수 있으므로 유연한 설계가 가능하다.