BlockingQueue — 블로킹 큐 인터페이스

BlockingQueue — 블로킹 큐 인터페이스

BlockingQueue<E>Queue블로킹 연산을 더한 인터페이스예요. 요소를 꺼낼 때는 큐가 비어 있지 않게 될 때까지 기다리고, 요소를 넣을 때는 큐에 공간이 생길 때까지 기다려요. 자바 동시성 프로그래밍에서 생산자-소비자(producer-consumer) 패턴의 핵심 재료예요.

출처: Java API Reference

본문

4가지 형태의 메서드

BlockingQueue의 메서드는 즉시 만족될 수 없지만 나중에는 만족될 수 있는 연산을 다루는 방식에 따라 4가지 형태가 있어요:

  1. 예외를 던지는 형태
  2. 특수 값을 반환하는 형태(예외가 아니라 실패를 알림)
  3. 무기한 블로킹하는 형태
  4. 주어진 최대 시간까지만 블로킹하다 포기하는 형태
동작 예외 특수 값 블로킹 타임아웃
삽입 add(e) offer(e) put(e) offer(e, time, unit)
제거 remove() poll() take() poll(time, unit)
검사 element() peek() 해당 없음 해당 없음

주요 특성

  • null 요소를 받지 않아요. add/put/offernull을 넣으면 구현체가 NullPointerException을 던져요. 여기서 nullpoll 연산의 실패를 나타내는 센티널(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)와 동일한 예외)

더 알아보기 (Learn more)