이들은 여러 스레드가 동시에 한정된 공유 자원을 사용할 때 발생하는, 소위 생산자-소비자 문제를 해결하기 위한 메서드들이다.
생산자-소비자 문제에 대해서 간단히 설명하지면 데이터를 생산하는 생산자와 소비하는 소비자, 그리고 그 데이터를 일시적으로 저장하는 버퍼가 있다. 만약 한정된 크기의 버퍼가 꽉 찼을 경우 생산자는 데이터를 더는 넣지 못하고 그대로 버리게 된다. 반대로 비어 있을 경우에는 소비자가 데이터를 소비하지 못하게 된다. 이런 문제를 해결하려면 데이터를 생산할 때까지 소비자가 기다리거나 소비자가 데이터를 소비할 때까지 생산자가 기다려야 했는데 이를 실현시켜주는 것이 이 메서드들이다.
사용 방법은 간단한다. 임계 영역 안에서 스레드가 기다리게 하고 싶은 지점에다가 wait();를 작성해주면 된다. 예를 들면 if문을 사용하여 버퍼가 꽉 찼을 때 wait를 호출해 기다리게 하면 된다.
wait를 당한 스레드는 자신이 갖고 있던 락을 반납하고 모니터 락처럼 인스턴스마다 갖고 있는 '스레드 대기 집합'이라는 곳에서 waiting 상태로 대기하게 된다. waiting 상태는 notify()로 깨워줄 때까지 계속된다. 만약 깨어나면 wait()이후의 코드부터 실행되면 스레드는 락을 다시 얻고 자신이 하려던 작업을 실행시킨다. 물론 여전히 조건이 충족이 안 됐으면 다시 waiting 상태로 돌아갈 수도 있다.
wait와 마찬가지로 작업이 끝난 뒤에다가 코드를 써주면 된다. 그러면 스레드 대기 집합에 있는 스레드를 notify()이면 무작위로 하나만 꺠우고, notifyAll()이면 다 꺠워 버린다. 물론 깨워졌다고 해서 바로 runnable 상태가 되는 건 아니다. 아직 락을 못 받았기 때문이다. 꺠워나면 락 대기 장소에 가서 blocked 상태로 락을 얻을 때까지 기다렸다가 락을 얻으면 그때 작업을 수행한다.
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
// 공유 버퍼 클래스
class Buffer {
private Queue<Integer> queue;
private int capacity; // 버퍼의 최대 용량
public Buffer(int capacity) {
this.queue = new LinkedList<>();
this.capacity = capacity;
}
// 생산자가 데이터를 버퍼에 추가하는 메서드
public synchronized void put(int value) throws InterruptedException {
// 버퍼가 가득 찼으면 생산자는 대기 (wait)
while (queue.size() == capacity) {
System.out.println("버퍼 가득 참: 생산자 대기 중...");
wait(); // 이 객체의 락을 풀고 대기 상태로 진입
}
// 버퍼에 데이터 추가
queue.offer(value);
System.out.println("생산: " + value + " (현재 크기: " + queue.size() + ")");
// 데이터를 추가했으니, 대기 중인 소비자에게 알림 (notify)
// 버퍼에 데이터가 생겼으니 소비자가 가져갈 수 있음을 알림
notifyAll(); // 모든 대기 중인 스레드에게 알릴 수도 있음 (notify()는 하나만 깨움)
}
// 소비자가 데이터를 버퍼에서 가져가는 메서드
public synchronized int get() throws InterruptedException {
// 버퍼가 비어있으면 소비자는 대기 (wait)
while (queue.isEmpty()) {
System.out.println("버퍼 비어있음: 소비자 대기 중...");
wait(); // 이 객체의 락을 풀고 대기 상태로 진입
}
// 버퍼에서 데이터 가져오기
int value = queue.poll();
System.out.println("소비: " + value + " (현재 크기: " + queue.size() + ")");
// 데이터를 가져갔으니, 대기 중인 생산자에게 알림 (notify)
// 버퍼에 공간이 생겼으니 생산자가 데이터를 넣을 수 있음을 알림
notifyAll(); // 모든 대기 중인 스레드에게 알림
return value;
}
}
// 생산자 스레드 클래스
class Producer extends Thread {
private Buffer buffer;
private Random random = new Random();
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) { // 10개의 데이터를 생산
int value = random.nextInt(100); // 0-99 사이의 랜덤 값
buffer.put(value);
Thread.sleep(random.nextInt(500)); // 생산 후 잠시 대기 (0~499ms)
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 인터럽트 상태 복원
System.out.println("생산자 스레드 인터럽트됨.");
}
}
}
// 소비자 스레드 클래스
class Consumer extends Thread {
private Buffer buffer;
private Random random = new Random();
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) { // 10개의 데이터를 소비
buffer.get();
Thread.sleep(random.nextInt(500)); // 소비 후 잠시 대기 (0~499ms)
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 인터럽트 상태 복원
System.out.println("소비자 스레드 인터럽트됨.");
}
}
}
public class WaitNotifyExample {
public static void main(String[] args) {
Buffer buffer = new Buffer(5); // 용량이 5인 버퍼 생성
Producer producer = new Producer(buffer);
Consumer consumer = new Consumer(buffer);
producer.start();
consumer.start();
try {
producer.join(); // 생산자 스레드가 종료될 때까지 기다림
consumer.join(); // 소비자 스레드가 종료될 때까지 기다림
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("메인 스레드 인터럽트됨.");
}
System.out.println("\n--- 생산자-소비자 작업 완료 ---");
}
}
다만 이들에게는 한계가 있다. 바로 원하는 스레드를 못 꺠운다는 점이다. 물론 notifyAll()을 사용하면 전부 꺠울 수 있지만 원하지 않는 스레드도 꺠어난다는 비효율이 발생한다. 어쩌면 원하는 스레드는 못 깨울 수도 있다. 게다가 스레드 대기 집합도 한 개뿐이다. 이러한 문제를 해결하기 위한 것이 Condition 객체다.