AbstractQueue<E> Thread safe하게 큐 관리를 해보자

말하는 감자·2025년 5월 21일

내일배움캠프

목록 보기
63/73

참고 공식문서 https://docs.oracle.com/javase/8/docs/api/java/util/AbstractQueue.html


티.꾸(TIL 꾸미기) 참 어렵다.. 이모지 고르는게 세상에서 제일 어려움

이번게시글은 동시성 2탄급 게시글이다..




AbstractQueue란?

공식 문서 요약:
1. FIFO(First-In-First-Out) 방식의 큐를 기반으로 한다.
2. add, remove, element 메서드는 fasle나 null 대신에 예외를 던지는 형태고, 내부적으로 offer, poll, peek 메서드에 위임한다.
3. 큐는 null 값을 저장하지 않는다. (넣으면 NullPointerException)
4. 이 클래스를 상속하는 클래스는 최소한 offer(E e), poll(), peek() 메서드를 구현해야 한다. (null 삽입 불가능XX)

※ 즉, 이 추상 클래스는 큐의 기본 골격만 제공하고, 실제 저장 방식이나 동시성 여부는 서브 클래스에서 정해야한다.




자주 사용되는 큐 클래스들 및 차이점


헐 사진 엄청 작네

ArrayBlockingQueue, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, PriorityQueue, SynchronousQueue
가 있음

공통 메서드

메서드설명
put(E)큐/덱이 가득 차면 대기 (blocking)
take()큐/덱이 비어있으면 대기 (blocking)
offer(E)큐/덱이 가득 차면 false 반환
poll()큐/덱이 비어있으면 null 반환
peek()큐/덱의 첫 번째 요소 확인 (삭제X)
size()현재 요소 수 반환

여튼 자주 사용되는 AbstractQueue 구현체에 대해서 ARABOZA

먼저, 이름부터 카테고리로 나눌 수 있음.

색깔에 비슷한거끼리 묶어봤다.
BlockingQueue 랑 Non-BlockingQueue로 나뉠 수 있고
Array/Linked/Priority 3개의 자료구조?? 로 나뉠 수 잇는거같아보인다.

실제 자료구조?는 이런식으로 확장되고 구현된다.






🧩큐 분류 기준

1. 동기화 방식에 따른 분류

🟥 BlockingQueue (블로킹 큐)

ㅊㅊ https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/BlockingQueue.html

  • 정의: 큐가 비어있거나 가득 찼을 때, 해당 상태가 해소될 때까지 스레드를 대기시킴.
  • 용도: 생산자-소비자 패턴에서 유용.
  • 특징:
    • put(): 큐가 가득 차면 대기.
    • take(): 큐가 비어있으면 대기.

공통적으로 ReentrantLock 라는 동기화 방식을 사용한다. (아래 설명 추가)


🟦 Non-BlockingQueue (논블로킹 큐)

https://www.baeldung.com/java-queue-linkedblocking-concurrentlinked

  • 정의: 락 없이 동시성을 제어하며, 스레드를 대기시키지 않음.
  • 용도: 고성능이 요구되는 환경에서 유용.
  • 특징:
    • offer(): 큐가 가득 차면 false 반환.
    • poll(): 큐가 비어있으면 null 반환.

❓ 어떻게 락 없이 동시성 제어를 하지요??

=> CAS (Compare-And-Swap) 연산 사용
CPU 수준에서 지원하는 CAS 연산 (비원자적 비교-수정) 을 사용 << "값이 여전히 이전 상태라면 내가 원하는 값으로 바꿔줘!!"

📌 CAS

// 의사코드
if (현재값 == 예상값) {
    현재값 = 새값
    return 성공
} else {
    return 실패
}
  • 이 연산은 동시에 여러 스레드가 접근해도 경합을 일으키지 않고 하나만 성공함.
  • 실패한 스레드는 다시 시도(재시도 루프)함 → 그래서 lock-free라고 해도 바보처럼 무한 루프 돌리는 건 아님.
    ConcurrentLinkedQueue 구조 같은 경우 headtail 노드를 AtomicReference 로 감싸고 있음

요소 삽입 (offer())시 : 마지막 노드에 새 노드를 연결하고, CAS로 tail 포인터를 새 노드로 바꿈
-> 다른 스레드가 동시에 삽입해도 CAS가 실패하면 재시도

요소 제거 (poll())시 : head의 다음 노드를 읽고 CAS로 head를 이동시킴 (데이터는 next에 있음)



2. 내부 자료구조에 따른 분류

🟥 Array 기반

  • 예시: ArrayBlockingQueue
  • 특징:
    • 고정 크기의 배열 사용.
    • 삽입/삭제 시 인덱스 기반으로 처리.
    • 예측 가능한 성능.

🟦 Linked 기반

  • 예시: LinkedBlockingQueue, LinkedBlockingDeque
  • 특징:
    • 노드 기반의 연결 리스트 사용.
    • 동적 크기 조절 가능.
    • 삽입/삭제 시 노드 연결/해제.

출처: https://www.geeksforgeeks.org/priorityblockingqueue-class-in-java/


🟩 Priority 기반

  • 예시: PriorityBlockingQueue
  • 특징:
    • 요소의 우선순위에 따라 정렬.
    • 기본적으로 자연 순서 또는 Comparator 사용.
    • 우선순위 기반 작업 처리에 유용.

출처: https://www.geeksforgeeks.org/priorityblockingqueue-class-in-java/



🟡 특별한 구현체 SynchronousQueue

  • 정의: 내부에 저장 공간이 없는 큐로, 하나의 스레드가 put()을 호출하면 다른 스레드가 take()를 호출할 때까지 대기함.

  • 특징:

    • 0 용량: 요소를 저장하지 않음.
    • 직접 전달: 생산자와 소비자가 동시에 만나야 데이터 전달이 이루어짐.
    • 공정성 설정 가능: FIFO 순서를 보장하는 공정성 모드 설정 가능.
  • 용도:

    • 스레드 간 직접적인 데이터 전달이 필요한 경우.
    • ExecutorService에서 작업을 즉시 처리하거나 거부할 때 사용.

출처 : https://www.baeldung.com/java-synchronous-queue




요약은 지선생이

요약




💟 ArrayBlockingQueue

https://java-latte.blogspot.com/2013/10/arrayblockingqueue-in-java.html


ArrayBlockingQueue는 배열 기반의 고정 크기 FIFO(선입선출) 큐

이름 답게
Array("배열로 만들어진" + "크기가 고정된")
Blocking("스레드 안전한")
Queue("순서 지키는 큐")



사용방법

ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

  1. put("A") → A 들어감

  2. put("B") → B 들어감
    ...

  3. 5개 다 차면 → 다음 put()은 블로킹 (멈춰서 기다림)

  4. take() → 가장 앞의 A 나옴

  5. 비어 있으면 → take()는 대기함

생성자 옵션

ArrayBlockingQueue(int capacity)
ArrayBlockingQueue(int capacity, boolean fair)

fair = true → 공정성: 먼저 기다린 스레드가 먼저 락 획득 (FIFO 기반 락 큐)
fair = false → 비공정: 빠른 스레드가 락을 먼저 가져갈 수 있음 → 성능은 더 좋음



핵심 특징

1️⃣ 배열 기반이므로 공간이 연속적이고 성능이 예측 가능하다.

  • Linked 방식보다 메모리 캐시 효율이 좋음

2️⃣ 크기가 고정된다.

  • 한 번 만들면 큐의 길이 못 바꿈 (Resizable 아님)

3️⃣ 블로킹 기능이 내장되어 있다..

  • 생산자-소비자 패턴에 적합 (put()과 take()가 자동으로 대기와 재개 처리)
  • 즉, 내부적으로 명시적인 락을 사용함.



언제 쓸까?

✅ Blocking(대기) 메커니즘이 필요한 경우
✅ 락 기반 동기화로 예측 가능한 동작을 원할 때 => 락 써야할 때!
❌ 비차단 고성능 처리가 필요할 때는 부적합
❌ 큐 크기를 유동적으로 늘려야 할 때는 부적합







💟 ConcurrentLinkedQueue

https://www.prepbytes.com/blog/java/concurrentlinkedqueue-in-java-with-examples/
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html

ConcurrentLinkedQueue는 락 없이 빠르고 안전하게 여러 스레드에서 동시에 접근 가능한 FIFO 큐로, 대용량/고빈도 작업에서 스레드 간 안전한 데이터 전달이 필요한 경우에 유용함.



사용방법

ConcurrentLinkedQueue<E> queue = new ConcurrentLinkedQueue<>();

  1. queue.add("A"); // 요소 추가
  2. queue.offer("B"); // 요소 추가 (add와 동일, 큐가 가득 차있을 경우만 차이. 이 큐는 무제한이라 동일)
  3. queue.peek(); // 첫 번째 요소 확인 (비어있으면 null)
  4. queue.poll(); // 첫 번째 요소 꺼내고 제거 (비어있으면 null)
  5. queue.isEmpty(); // 큐가 비어있는지 확인
  6. queue.size(); // 큐의 크기 반환



핵심 특징

1️⃣ 무한 크기

  • 큐의 크기를 제한하지 않음 → 큐가 가득 차는 상황 없음
  • 메모리가 허용하는 한 얼마든지 삽입 가능

2️⃣ Lock-Free 기반의 비차단(Non-blocking) 알고리즘

  • 내부적으로 CAS(Compare-And-Swap) 연산을 사용해 동시성을 보장
  • synchronized나 Lock을 사용하지 않음
  • 여러 스레드가 동시에 접근해도 기다림 없이 작업 수행 가능 → 지연 없이 빠름

3️⃣ 내부적으로 싱글 연결 리스트(Singly Linked List) 구조를 사용



언제 쓸까?

✅ 락 없이 성능을 높이고 싶을 때 => 락 사용x
✅ 메모리 사용이 유연해야 할 때
❌ 스케줄링 우선순위, 지연 처리 필요할 때는 부적합




여기까지해서 ArrayBlockingQueueConcurrentLinkedQueue의 간단비교!

ArrayBlockingQueue vs ConcurrentLinkedQueue


즉, 스레드 세이프하는 알고리즘이 서로다르고 배열과 리스트의 차이정도가 있다.







💟 LinkedBlockingQueue / LinkedBlockingDeque

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/LinkedBlockingQueue.html
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/LinkedBlockingDeque.html
https://www.baeldung.com/java-blocking-queue

이름부터 Queue 와 Deque(덱) 라서 특징은 비슷할 것이라고 생각하고 같이 쓴다.



사용방법

// LinkedBlockingQueue
LinkedBlockingQueue<E> queue = new LinkedBlockingQueue<>();
LinkedBlockingQueue<E> queue2 = new LinkedBlockingQueue<>(int capacity); // 기본은 무제한

// LinkedBlockingDeque
LinkedBlockingDeque<E> deque = new LinkedBlockingDeque<>();
LinkedBlockingDeque<E> deque2 = new LinkedBlockingDeque<>(int capacity);

capacity를 지정하지 않으면 Integer.MAX_VALUE가 최대 크기로 설정됨.
capacity를 지정하면 메모리 사용 제한 및 생산-소비 속도 제어 가능.



LinkedBlockingDeque는 덱이기 때문에 양쪽 삽입/삭제가 가능하다 (따로 사용하는 메서드가 더 있음)

deque.offerFirst(e);
deque.offerLast(e);
deque.pollFirst();
deque.pollLast();

이런식으로 앞/뒤 설정가능








이쯤되면 궁금증이 생길것이다.

❓ AbstractQueue 는 모두 Thread safe한가?

정답은 ❌다.

AbstractQueue그 자체로는 Thread-safe하지 않으며, 그걸 상속한 각 구현체들이 Thread-safe 여부를 직접 구현 방식에 따라 결정 한다.


동기화 방식을 정리해봤는데 사진으로 알 수 있다 싶이
BlockingQueueReentrantLock라는 동기화 방식을, 그 외에는 구현체마다 각각의 동기화 방식을 사용한다.

즉, BlockingQueue 인터페이스를 구현한 클래스들은 멀티스레드 환경에서 안전하게 작동하도록 설계되었음.

반면에 PriorityQueue동기화 코드 자체가 없다

PriorityQueue는 java.util.AbstractQueue의 하위 클래스암

🔒 멀티스레드 환경에서는 PriorityQueue 를 어떻게 쓸까?

PriorityBlockingQueue를 사용한다.
이건 BlockingQueue 인터페이스를 구현하고 있고,
내부적으로 ReentrantLock을 사용해 Thread-safe하게 동작함.
마찬가지로 우선순위 기반 정렬을 지원함.

PriorityBlockingQueue = BlockingQueue + PriorityQueue








ReentrantLock 동기화 방식

ㅊㅊ : https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html
https://velog.io/@may_yun/JAVA-synchronized-VS-Reentrant-Lock-차이점

이야 오라클 공식문서 완전 친절해 ㅋㅋ

ReentrantLock 이란?

ReentrantLock의 동기화 방식은 Java에서 명시적인(lock-based) 방식으로 스레드 간 동기화를 제어하는 도구로써
스레드 간에 공유 자원에 대한 접근을 제어하기 위한 락(lock) 객체로 사용한다

사용방법


이거 공식문서에 잇는 예제인데
객체 호출해서 lock으로 얻고, try-finally 부분에서 unlock()을 하는게, 이전시간에 사용했던 레투스와 거의 유사한 형태다.

참고로
lock.isHeldByCurrentThread();
현재 스레드가 이 ReentrantLock을 보유하고 있는지 확인하는 메서드임
반환 값은 true/false

굳이굳이 자바에서 동기화를 관리해야해서 synchronized 같은 것을 메서드에 끼워넣을바엔 해당 도구를 사용해보자!!!

📌 synchronized는 공정성을 지원하지 않는다. 반면 ReentrantLock은 생성자의 인자를 통해서 공정/불공정 설정을 할 수 있다.






요즘에는 DB에 락 설정을 잘해놓으니깐 + redis 락이 좋으니깐 굳이 자바단에서 락 설정을 세세하게 할 필요 없는듯?

profile
대충 데굴데굴 굴러가는 개발?자

0개의 댓글