BlockingQueue — 블로킹 큐 인터페이스
BlockingQueue — 블로킹 큐 인터페이스
BlockingQueue<E>는 Queue에 블로킹 연산을 더한 인터페이스예요. 요소를 꺼낼 때는 큐가 비어 있지 않게 될 때까지 기다리고, 요소를 넣을 때는 큐에 공간이 생길 때까지 기다려요. 자바 동시성 프로그래밍에서 생산자-소비자(producer-consumer) 패턴의 핵심 재료예요.
본문
4가지 형태의 메서드
BlockingQueue의 메서드는 즉시 만족될 수 없지만 나중에는 만족될 수 있는 연산을 다루는 방식에 따라 4가지 형태가 있어요:
- 예외를 던지는 형태
- 특수 값을 반환하는 형태(예외가 아니라 실패를 알림)
- 무기한 블로킹하는 형태
- 주어진 최대 시간까지만 블로킹하다 포기하는 형태
| 동작 | 예외 | 특수 값 | 블로킹 | 타임아웃 |
|---|---|---|---|---|
| 삽입 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
| 제거 | remove() |
poll() |
take() |
poll(time, unit) |
| 검사 | element() |
peek() |
해당 없음 | 해당 없음 |
주요 특성
null요소를 받지 않아요.add/put/offer에null을 넣으면 구현체가NullPointerException을 던져요. 여기서null은poll연산의 실패를 나타내는 센티널(sentinel) 값으로 쓰여요.- 용량 제한이 있을 수 있어요. 주어진 시점에
remainingCapacity만큼을 초과해put하면 블로킹돼요. 본질적 제한이 없는BlockingQueue는 항상Integer.MAX_VALUE의 남은 용량을 보고해요. - 스레드 안전해요. 모든 큐잉 메서드는 내부 락이나 동시성 제어로 원자적으로 동작해요. 다만
addAll,containsAll,retainAll,removeAll같은 일괄Collection연산은 구현이 달리 명시하지 않는 한 반드시 원자적인 건 아니에요. 그래서addAll(c)가c의 일부만 추가한 뒤 예외로 실패할 수도 있어요. - "close"/"shutdown" 연산은 기본 제공되지 않아요. 스트림 종료가 필요하면 생산자가 end-of-stream 또는 poison 객체를 넣고, 소비자가 해석하는 전략을 흔히 써요.
생산자-소비자 예제
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) { queue = q; }
public void run() {
try {
while (true) { queue.put(produce()); }
} catch (InterruptedException ex) { ... handle ...}
}
Object produce() { ... }
}
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
public void run() {
try {
while (true) { consume(queue.take()); }
} catch (InterruptedException ex) { ... handle ...}
}
void consume(Object x) { ... }
}
class Setup {
void main() {
BlockingQueue q = new SomeQueueImplementation();
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
메모리 일관성 효과: 다른 동시성 컬렉션처럼, 한 스레드가 BlockingQueue에 객체를 넣기 전의 동작은 다른 스레드가 그 요소를 접근·제거한 이후의 동작보다 happen-before 관계예요.
대표 구현체
ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue가 있어요.
삽입 메서드
boolean add(E e) — 용량 제한을 위반하지 않고 즉시 가능하면 삽입하고 true를 반환해요. 공간이 없으면 IllegalStateException을 던져요. 용량 제한이 있는 큐에서는 offer를 쓰는 게 보통 좋아요.
IllegalStateException,ClassCastException,NullPointerException,IllegalArgumentException
boolean offer(E e) — 즉시 가능하면 삽입하고 true, 공간이 없으면 false를 반환해요. 예외로만 실패를 알리는 add(E)보다 선호돼요.
void put(E e) throws InterruptedException — 공간이 생길 때까지 기다리며 삽입해요. 대기 중 인터럽트되면 InterruptedException.
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException — 공간이 생길 때까지 최대 timeout만큼 기다리며 삽입해요. 성공 시 true, 대기 시간이 지나면 false.
제거 메서드
E take() throws InterruptedException — 요소가 생길 때까지 기다리며 큐의 머리를 꺼내 제거해요.
InterruptedException(대기 중 인터럽트)
E poll(long timeout, TimeUnit unit) throws InterruptedException — 요소가 생길 때까지 최대 timeout만큼 기다리며 머리를 꺼내요. 시간이 지나면 null.
int remainingCapacity() — 메모리·자원 제약이 없다고 가정할 때 블로킹 없이 추가로 받을 수 있는 요소 수를 반환해요. 본질적 제한이 없으면 Integer.MAX_VALUE. 참고로 remainingCapacity를 봐도 다른 스레드가 곧 삽입·제거할 수 있어서 성공 여부를 항상 알 수는 없어요.
boolean remove(Object o) — o.equals(e)인 요소 하나를 제거해요. 큐가 변했으면 true.
ClassCastException,NullPointerException(선택)
boolean contains(Object o) — o.equals(e)인 요소를 하나 이상 담고 있으면 true.
배수(drain) 메서드
int drainTo(Collection<? super E> c) — 큐에서 사용 가능한 모든 요소를 제거해 주어진 컬렉션에 추가해요. poll을 반복하는 것보다 효율적일 수 있어요. 큐를 자기 자신에게 drain하면 IllegalArgumentException. 연산 중 컬렉션이 수정되면 동작은 정의되지 않아요.
UnsupportedOperationException,ClassCastException,NullPointerException,IllegalArgumentException
int drainTo(Collection<? super E> c, int maxElements) — 최대 maxElements개만큼 사용 가능한 요소를 제거해 추가해요. (drainTo(c)와 동일한 예외)