DelayQueue는 자바의 동시성 유틸리티 패키지(java.util.concurrent)에 포함된 특별한 형태의 블로킹 큐 구현체입니다. 이 큐의 핵심 기능은 요소들이 정해진 지연 시간 이후에만 소비될 수 있도록 보장한다는 점입니다. 즉, 큐에 삽입된 요소는 특정 만료 시점이 되기 전까지는 소비자 스레드에 의해 가져갈 수 없습니다.
1. DelayQueue의 주요 활용 사례
DelayQueue의 독특한 특성은 다양한 시나리오에서 강력한 도구로 활용될 수 있습니다:
- 캐시 만료 관리: 유효 기간이 설정된 캐시 항목을 자동으로 제거하여 메모리를 효율적으로 관리합니다.
- 작업 예약 시스템: 특정 시점에 실행되어야 하는 작업을 스케줄링하거나 알림을 보낼 때 사용됩니다.
- 세션 만료 처리: 사용자 세션에 시간 제한을 두어 비활성 세션을 자동으로 종료합니다.
- 게임 로직 구현: 스킬 쿨타임, 아이템 재생성 등 시간 기반 게임 메커니즘에 적용됩니다.
- 주문 유효성 검사: 제한 시간 내에 처리되지 않은 주문이나 거래를 자동으로 취소하는 데 사용될 수 있습니다.
2. 핵심 기능 및 디자인 원칙
DelayQueue는 다음과 같은 중요한 특성들을 지닙니다:
- 무한 용량: 이론적으로는 메모리가 허용하는 한 무제한의 요소를 저장할 수 있는 언바운드 큐입니다.
- 스레드 안전성: 내부적으로
ReentrantLock을 사용하여 동시성 환경에서의 안전한 접근을 보장합니다. - 블로킹/논블로킹 연산: 요소 추가 및 가져오기 작업 시 블로킹 또는 논블로킹 방식을 선택할 수 있습니다.
- 지연된 요소 소비: 요소의 만료 시간이 도달해야만 큐에서 꺼낼 수 있습니다.
- 우선순위 기반 정렬: 내부적으로 우선순위 큐를 사용하여, 만료 시간이 가장 임박한 요소가 항상 큐의 맨 앞에 위치하도록 유지합니다.
2.1. 클래스 구조
DelayQueue는 다음과 같은 핵심 필드를 포함하며, AbstractQueue를 상속받고 BlockingQueue 인터페이스를 구현합니다.
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
// 동시성 접근 제어를 위한 재진입 락
private final transient ReentrantLock accessGuard = new ReentrantLock();
// 지연 요소를 저장하고 정렬하는 우선순위 큐
private final PriorityQueue<E> internalPriorityQueue = new PriorityQueue<E>();
// 리더-팔로워 패턴을 위한 리더 스레드 참조
private Thread currentLeader = null;
// 요소 가용성을 기다리는 스레드를 위한 조건 변수
private final Condition elementAvailable = accessGuard.newCondition();
// ... 추가적인 메서드 구현 ...
}
2.2. Delayed 인터페이스의 역할
DelayQueue에 저장될 모든 객체는 반드시 Delayed 인터페이스를 구현해야 합니다. 이 인터페이스는 Comparable<Delayed>를 상속받으므로, 다음 두 가지 메서드를 구현해야 합니다:
long getDelay(TimeUnit unit): 남은 지연 시간을 지정된TimeUnit으로 반환합니다. 이 시간이 0보다 작거나 같으면 요소가 만료되었음을 의미합니다.int compareTo(Delayed other): 두Delayed객체의 우선순위를 비교하는 데 사용됩니다. 일반적으로 남은 지연 시간을 기준으로 정렬합니다.
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
2.3. 내부 데이터 구조
DelayQueue는 내부적으로 PriorityQueue를 사용하여 Delayed 요소들을 저장합니다. 이 PriorityQueue는 compareTo 메서드에 따라 요소들을 항상 정렬된 상태로 유지하므로, 만료 시간이 가장 가까운 요소가 큐의 맨 앞에 위치하게 됩니다.
3. 핵심 메서드 분석
3.1. 요소 추가: offer() 및 put()
offer(E element) 메서드는 큐에 요소를 추가합니다. 내부 PriorityQueue에 요소를 넣은 후, 만약 새로 추가된 요소가 큐의 헤드가 되었다면(가장 빠른 만료 시간), 기다리고 있는 스레드를 깨울 필요가 있습니다. put(E element) 메서드는 DelayQueue가 무한 용량이므로 offer()를 단순히 호출합니다.
public boolean offer(E element) {
final ReentrantLock guard = this.accessGuard;
guard.lock(); // 락 획득
try {
internalPriorityQueue.offer(element); // 우선순위 큐에 요소 추가
// 새로 추가된 요소가 큐의 헤드(가장 빠른 만료)인 경우
if (internalPriorityQueue.peek() == element) {
currentLeader = null; // 리더 스레드 초기화
elementAvailable.signal(); // 기다리는 스레드 중 하나를 깨움
}
return true;
} finally {
guard.unlock(); // 락 해제
}
}
public void put(E element) {
offer(element); // 무한 용량이므로 offer와 동일하게 동작
}
3.2. 요소 가져오기: poll(), take(), poll(long timeout, TimeUnit unit)
poll() - 비블로킹 방식: 큐의 헤드 요소를 확인하고, 만료되지 않았다면 즉시 null을 반환합니다.
public E poll() {
final ReentrantLock guard = this.accessGuard;
guard.lock();
try {
E headElement = internalPriorityQueue.peek(); // 헤드 요소 확인
// 큐가 비어있거나 헤드 요소가 아직 만료되지 않았다면 null 반환
if (headElement == null || headElement.getDelay(TimeUnit.NANOSECONDS) > 0)
return null;
else
return internalPriorityQueue.poll(); // 만료된 요소 반환
} finally {
guard.unlock();
}
}
take() - 블로킹 방식: 만료된 요소가 나타날 때까지 현재 스레드를 블록시킵니다. 내부적으로 리더-팔로워 패턴을 사용하여 효율적인 대기를 구현합니다.
public E take() throws InterruptedException {
final ReentrantLock guard = this.accessGuard;
guard.lockInterruptibly(); // 인터럽트 가능한 락 획득
try {
for (;;) {
E headElement = internalPriorityQueue.peek();
if (headElement == null) { // 큐가 비어있다면
elementAvailable.await(); // 요소가 추가될 때까지 무기한 대기
} else {
long remainingDelayNanos = headElement.getDelay(TimeUnit.NANOSECONDS);
if (remainingDelayNanos <= 0) // 만료되었다면
return internalPriorityQueue.poll(); // 요소 반환
headElement = null; // 메모리 누수 방지를 위해 참조 해제
// 리더-팔로워 패턴 적용
if (currentLeader != null) { // 이미 리더 스레드가 있다면
elementAvailable.await(); // 팔로워 스레드로 무기한 대기
} else {
Thread currentThread = Thread.currentThread();
currentLeader = currentThread; // 현재 스레드를 리더로 설정
try {
// 남은 지연 시간만큼 대기 (리더 스레드만 정확한 시간을 기다림)
elementAvailable.awaitNanos(remainingDelayNanos);
} finally {
if (currentLeader == currentThread)
currentLeader = null; // 리더 역할 종료
}
}
}
}
} finally {
// 락 해제 전, 큐가 비어있지 않고 리더가 없다면 다른 스레드를 깨움
if (currentLeader == null && internalPriorityQueue.peek() != null)
elementAvailable.signal();
guard.unlock();
}
}
poll(long timeout, TimeUnit unit) - 타임아웃 블로킹 방식: 지정된 시간 동안만 만료된 요소를 기다립니다.
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long remainingNanos = unit.toNanos(timeout);
final ReentrantLock guard = this.accessGuard;
guard.lockInterruptibly();
try {
for (;;) {
E headElement = internalPriorityQueue.peek();
if (headElement == null) {
if (remainingNanos <= 0) return null;
remainingNanos = elementAvailable.awaitNanos(remainingNanos); // 요소 추가 대기
} else {
long elementDelayNanos = headElement.getDelay(TimeUnit.NANOSECONDS);
if (elementDelayNanos <= 0) return internalPriorityQueue.poll(); // 만료됨
if (remainingNanos <= 0) return null; // 타임아웃
headElement = null; // 참조 해제
// 리더-팔로워 패턴 및 타임아웃 처리
if (remainingNanos < elementDelayNanos || currentLeader != null) {
remainingNanos = elementAvailable.awaitNanos(remainingNanos);
} else {
Thread currentThread = Thread.currentThread();
currentLeader = currentThread;
try {
long waitedNanos = elementAvailable.awaitNanos(elementDelayNanos);
remainingNanos -= (elementDelayNanos - waitedNanos); // 실제 대기 시간 반영
} finally {
if (currentLeader == currentThread)
currentLeader = null;
}
}
}
}
} finally {
if (currentLeader == null && internalPriorityQueue.peek() != null)
elementAvailable.signal();
guard.unlock();
}
}
3.3. 리더-팔로워 패턴
DelayQueue는 내부적으로 "리더-팔로워" 패턴을 사용하여 불필요한 스레드 깨우기를 최소화하고 성능을 최적화합니다.
- 리더 스레드:
take()나poll(timeout)을 호출한 스레드 중 가장 먼저 대기 상태가 되는 스레드입니다. 이 스레드는 큐의 헤드 요소가 만료될 때까지 정확한 시간만큼 대기합니다. - 팔로워 스레드: 리더 스레드가 이미 존재할 때 대기 상태가 되는 스레드들입니다. 이들은 무기한으로 대기하며, 리더 스레드가 요소를 가져가거나 새로운 요소가 추가되어 깨워지기를 기다립니다.
이 패턴은 "thundering herd" 문제(많은 스레드가 동시에 깨어나 경쟁하는 현상)를 방지하고, 컨텍스트 스위칭 오버헤드를 줄여 시스템 효율성을 높입니다.
4. DelayQueue 활용 예제
4.1. 기본 사용
Delayed 인터페이스를 구현한 간단한 요소를 DelayQueue에 넣고 꺼내는 예제입니다.
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.Objects;
// 지연 가능한 요소 클래스
class SimpleDelayedItem implements Delayed {
private final String content;
private final long activationTimeMs; // 활성화(만료) 시간 밀리초
public SimpleDelayedItem(String content, long delayMilliseconds) {
this.content = Objects.requireNonNull(content);
this.activationTimeMs = System.currentTimeMillis() + delayMilliseconds;
}
@Override
public long getDelay(TimeUnit unit) {
long remainingTimeMs = activationTimeMs - System.currentTimeMillis();
return unit.convert(remainingTimeMs, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed other) {
if (other == this) return 0;
// 남은 지연 시간을 기준으로 정렬
long diff = this.getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS);
return Long.compare(diff, 0); // diff < 0이면 -1, diff == 0이면 0, diff > 0이면 1
}
@Override
public String toString() {
return "SimpleDelayedItem{content='" + content + "', activatesAt=" + activationTimeMs + "}";
}
}
public class BasicDelayQueueUsage {
public static void main(String[] args) throws InterruptedException {
DelayQueue<SimpleDelayedItem> queue = new DelayQueue<>();
// 다른 지연 시간을 가진 요소 추가
queue.put(new SimpleDelayedItem("Event Alpha", 3000)); // 3초 후 만료
queue.put(new SimpleDelayedItem("Event Beta", 1000)); // 1초 후 만료
queue.put(new SimpleDelayedItem("Event Gamma", 5000)); // 5초 후 만료
System.out.println("Processing delayed items...");
// 큐가 빌 때까지 요소 소비
while (!queue.isEmpty()) {
SimpleDelayedItem item = queue.take(); // 만료될 때까지 블록
System.out.println("Consumed: " + item + " at " + System.currentTimeMillis());
}
System.out.println("All items processed.");
}
}
4.2. 만료성 캐시 시스템
DelayQueue를 사용하여 자동으로 만료되는 캐시를 구현하는 예제입니다.
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.Objects;
// 캐시 항목
class CacheEntry<K, V> implements Delayed {
private final K cacheKey;
private final V cacheValue;
private final long expiryTimeMs; // 만료 시각 밀리초
public CacheEntry(K key, V value, long timeToLiveMs) {
this.cacheKey = Objects.requireNonNull(key);
this.cacheValue = Objects.requireNonNull(value);
this.expiryTimeMs = System.currentTimeMillis() + timeToLiveMs;
}
public K getKey() { return cacheKey; }
public V getValue() { return cacheValue; }
@Override
public long getDelay(TimeUnit unit) {
long remaining = expiryTimeMs - System.currentTimeMillis();
return unit.convert(remaining, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed other) {
long diff = this.getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS);
return Long.compare(diff, 0);
}
}
// 만료성 캐시 관리자
class ExpiryCacheManager<K, V> {
private final ConcurrentHashMap<K, V> dataStore = new ConcurrentHashMap<>();
private final DelayQueue<CacheEntry<K, V>> expiryQueue = new DelayQueue<>();
private final Thread cleanerThread;
public ExpiryCacheManager() {
// 백그라운드에서 만료된 항목을 정리하는 스레드 시작
cleanerThread = new Thread(this::runCacheCleaner);
cleanerThread.setDaemon(true); // 데몬 스레드로 설정
cleanerThread.start();
}
public void put(K key, V value, long timeToLiveMs) {
dataStore.put(key, value);
expiryQueue.put(new CacheEntry<>(key, value, timeToLiveMs));
}
public V get(K key) {
return dataStore.get(key);
}
public void remove(K key) {
dataStore.remove(key);
}
private void runCacheCleaner() {
while (!Thread.currentThread().isInterrupted()) {
try {
CacheEntry<K, V> expiredItem = expiryQueue.take(); // 만료될 때까지 대기
if (expiredItem != null) {
// ConcurrentHashMap의 remove(key, value)는 특정 value일 때만 제거
if (dataStore.remove(expiredItem.getKey(), expiredItem.getValue())) {
System.out.println("Removed expired key: " + expiredItem.getKey());
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 인터럽트 상태 복원
break;
}
}
System.out.println("Cache cleaner thread stopped.");
}
public void shutdown() {
cleanerThread.interrupt();
}
}
public class CacheSystemExample {
public static void main(String[] args) throws InterruptedException {
ExpiryCacheManager<String, String> myCache = new ExpiryCacheManager<>();
myCache.put("user:1", "Alice", 2000); // 2초 후 만료
myCache.put("product:a", "Laptop", 5000); // 5초 후 만료
System.out.println("Initial get user:1: " + myCache.get("user:1")); // 존재해야 함
Thread.sleep(3000); // 3초 대기
System.out.println("After 3s, get user:1: " + myCache.get("user:1")); // null이어야 함
Thread.sleep(3000); // 추가 3초 대기 (총 6초)
System.out.println("After 6s, get product:a: " + myCache.get("product:a")); // null이어야 함
myCache.shutdown();
}
}
4.3. 작업 스케줄러
DelayQueue를 사용하여 특정 시간에 작업을 실행하는 간단한 스케줄러를 구현할 수 있습니다.
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.Objects;
// 예약된 작업 객체
class ScheduledAction implements Delayed {
private final Runnable actionToPerform;
private final long executionTimeMs; // 작업 실행 시각 밀리초
private final long creationOrder; // 동일 시간 작업 시 순서 보장용
private static final AtomicLong sequencer = new AtomicLong();
public ScheduledAction(Runnable task, long delayMilliseconds) {
this.actionToPerform = Objects.requireNonNull(task);
this.executionTimeMs = System.currentTimeMillis() + delayMilliseconds;
this.creationOrder = sequencer.getAndIncrement();
}
public void run() {
actionToPerform.run();
}
@Override
public long getDelay(TimeUnit unit) {
long remaining = executionTimeMs - System.currentTimeMillis();
return unit.convert(remaining, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed other) {
if (other == this) return 0;
if (other instanceof ScheduledAction) {
ScheduledAction otherAction = (ScheduledAction) other;
long timeDiff = executionTimeMs - otherAction.executionTimeMs;
if (timeDiff < 0) return -1;
else if (timeDiff > 0) return 1;
else if (creationOrder < otherAction.creationOrder) return -1; // 동일 시간 시 생성 순서 기준
else return 1;
}
// 다른 Delayed 객체와 비교 시 일반적인 지연 시간 기준
long delayDiff = getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS);
return Long.compare(delayDiff, 0);
}
}
// 간단한 작업 스케줄러
class SimpleTaskScheduler {
private final DelayQueue<ScheduledAction> taskQueue = new DelayQueue<>();
private final Thread workerExecutionThread;
private volatile boolean keepRunning = true;
public SimpleTaskScheduler() {
workerExecutionThread = new Thread(this::processScheduledTasks);
workerExecutionThread.start();
}
public void schedule(Runnable task, long delayMilliseconds) {
taskQueue.offer(new ScheduledAction(task, delayMilliseconds));
}
private void processScheduledTasks() {
while (keepRunning) {
try {
ScheduledAction taskToExecute = taskQueue.take(); // 만료될 때까지 대기
System.out.println("Executing task at " + System.currentTimeMillis());
taskToExecute.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Scheduler worker thread stopped.");
}
public void shutdown() {
keepRunning = false;
workerExecutionThread.interrupt();
}
}
public class SchedulerDemo {
public static void main(String[] args) throws InterruptedException {
SimpleTaskScheduler scheduler = new SimpleTaskScheduler();
scheduler.schedule(() -> System.out.println("Task A completed (3s delay)"), 3000);
scheduler.schedule(() -> System.out.println("Task B completed (1s delay)"), 1000);
scheduler.schedule(() -> System.out.println("Task C completed (5s delay)"), 5000);
Thread.sleep(6000); // 스케줄러가 작업을 처리할 시간을 줌
scheduler.shutdown();
}
}
5. 성능 고려사항 및 최적화
5.1. 시간 복잡도
DelayQueue의 주요 연산은 내부 PriorityQueue에 기반합니다.
offer():PriorityQueue에 요소를 삽입하는 데O(log n)시간이 소요됩니다.poll(): 큐 헤드를 확인하고 제거하는 데O(log n)시간이 소요됩니다.take():poll()과 유사하게O(log n)시간이 소요되지만, 대기 시간이 추가될 수 있습니다.peek(): 큐 헤드를 단순히 확인하는 데O(1)시간이 소요됩니다.
5.2. 메모리 사용
DelayQueue의 메모리 사용량은 다음과 같은 요소에 의해 결정됩니다:
- 내부
PriorityQueue의 배열 구조. - 저장된 각
Delayed객체 자체의 크기. - 동시성 제어를 위한 락 및 조건 변수의 오버헤드.
5.3. 성능 최적화 권장 사항
- 적절한 지연 시간 단위 선택: 비즈니스 로직에 맞는 가장 적절한
TimeUnit을 사용하여 오버헤드를 줄입니다. - 단기 객체 생성 최소화: 불필요하게 많은
Delayed객체를 생성하고 버리는 것을 피하고, 가능하다면 객체 풀링을 고려합니다. - 큐 크기 모니터링: 무한 용량 큐이지만, 과도한 요소 적재는 메모리 부족을 야기할 수 있으므로 큐 크기를 주기적으로 모니터링해야 합니다.
- 리더-팔로워 패턴 활용:
DelayQueue에 내장된 이 패턴을 통해 대기 스레드 간의 효율성을 높입니다.
6. 다른 블로킹 큐와의 비교
6.1. PriorityBlockingQueue와의 차이점
| 특성 | DelayQueue | PriorityBlockingQueue |
|---|---|---|
| 요소 요구사항 | Delayed 인터페이스 구현 필수 |
Comparable 구현 또는 Comparator 제공 필수 |
| 정렬 기준 | 남은 지연 시간 (가장 빨리 만료되는 요소가 먼저) | 지정된 우선순위 (가장 높은 우선순위 요소가 먼저) |
| 요소 가져오기 | 만료 시간 도달 후 가져올 수 있음 | 가장 높은 우선순위 요소를 즉시 가져올 수 있음 |
| 주요 활용 | 작업 예약, 캐시 만료, 세션 관리 | 우선순위 기반 작업 처리, 이벤트 큐 |
6.2. 다른 블로킹 큐 유형과의 비교
| 큐 유형 | 유계성 (Boundedness) | 정렬 방식 | 주요 특징 |
|---|---|---|---|
ArrayBlockingQueue |
유계 (고정 용량) | FIFO (선입선출) | 배열 기반, 고정 크기 큐 |
LinkedBlockingQueue |
선택적 유계 | FIFO (선입선출) | 링크드 리스트 기반, 유연한 용량 설정 가능 |
PriorityBlockingQueue |
무계 | 우선순위 기반 | 힙(Heap) 기반, 우선순위 정렬 |
DelayQueue |
무계 | 지연 시간 기반 | 지연 시간 만료 후 요소 추출 가능 |
SynchronousQueue |
유계 (0) | 없음 | 요소를 저장하지 않고 직접 전달 (생산자-소비자 간 핸드오프) |
7. 일반적인 문제와 해결책
7.1. 메모리 누수 위험
문제: 지연 시간이 매우 길게 설정된 요소들이 DelayQueue에 계속 쌓이면, 실제로는 사용되지 않는 객체들이 오랫동안 메모리를 점유하여 메모리 누수와 유사한 상황이 발생할 수 있습니다.
해결책: DelayQueue 자체는 무한 용량이지만, 애플리케이션 수준에서 최대 용량을 제한하거나, 오래된 요소를 강제로 제거하는 메커니즘을 추가할 수 있습니다.
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.atomic.AtomicInteger;
// 제한된 용량을 가진 DelayQueue 래퍼
class BoundedDelayQueueWrapper<E extends Delayed> {
private final DelayQueue<E> internalQueue = new DelayQueue<>();
private final int capacityLimit;
private final ReentrantLock sizeLock = new ReentrantLock();
private final AtomicInteger currentSize = new AtomicInteger(0);
public BoundedDelayQueueWrapper(int maxCapacity) {
if (maxCapacity <= 0) throw new IllegalArgumentException("Capacity must be positive");
this.capacityLimit = maxCapacity;
}
public boolean offer(E item) {
sizeLock.lock();
try {
if (currentSize.get() >= capacityLimit) {
System.out.println("Queue is full, cannot add: " + item);
return false;
}
if (internalQueue.offer(item)) {
currentSize.incrementAndGet();
return true;
}
return false;
} finally {
sizeLock.unlock();
}
}
public E take() throws InterruptedException {
E item = internalQueue.take();
if (item != null) {
currentSize.decrementAndGet();
}
return item;
}
public int size() {
return currentSize.get();
}
// ... 다른 메서드들은 internalQueue에 위임
}
7.2. 지연 시간 정확성 문제
문제: System.currentTimeMillis()나 System.nanoTime()은 시스템 시계의 변경에 영향을 받을 수 있으며, 이는 특히 네트워크 타임 프로토콜(NTP) 동기화 등으로 인해 시계가 조정될 때 지연 시간의 정확성에 영향을 줄 수 있습니다.
해결책: getDelay를 구현할 때 System.nanoTime()을 기반으로 하는 단조 시계(monotonic clock)를 사용하여 절대적인 시간 대신 경과 시간을 측정함으로써 외부 시계 변화에 덜 민감하게 만들 수 있습니다. System.nanoTime()은 JVM 시작 이후의 경과 시간을 나노초 단위로 반환하며, 시스템 시계 변화에 영향을 받지 않습니다.
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.Objects;
// 단조 시계를 사용하는 지연 요소
class MonotonicDelayedItem implements Delayed {
private final String name;
private final long startNanos; // 생성 시점의 System.nanoTime()
private final long delayDurationNanos; // 지연될 총 나노초
public MonotonicDelayedItem(String name, long delayMilliseconds) {
this.name = Objects.requireNonNull(name);
this.startNanos = System.nanoTime();
this.delayDurationNanos = TimeUnit.MILLISECONDS.toNanos(delayMilliseconds);
}
@Override
public long getDelay(TimeUnit unit) {
long elapsedNanos = System.nanoTime() - startNanos;
long remainingNanos = delayDurationNanos - elapsedNanos;
return unit.convert(remainingNanos, TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed other) {
long currentDelay = this.getDelay(TimeUnit.NANOSECONDS);
long otherDelay = other.getDelay(TimeUnit.NANOSECONDS);
return Long.compare(currentDelay, otherDelay);
}
@Override
public String toString() {
return "MonotonicDelayedItem{name='" + name + "', remaining=" + getDelay(TimeUnit.MILLISECONDS) + "ms}";
}
}
7.3. 작업 실행 중 예외 처리
문제: 스케줄러와 같은 시스템에서 DelayQueue를 사용하여 작업을 실행할 때, 개별 작업에서 발생하는 예외가 전체 스케줄러의 동작을 방해하거나 중단시킬 수 있습니다.
해결책: 예약된 작업(Runnable)을 래핑하여 예외 처리 로직을 포함시키고, 예외 발생 시 적절하게 로깅하거나 특정 핸들러로 전달합니다.
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.Objects;
// 예외 처리가 가능한 지연 작업
class ExceptionSafeScheduledAction implements Delayed {
private final Runnable actualTask;
private final long triggerTimeMs;
private final Consumer<Exception> errorHandler;
public ExceptionSafeScheduledAction(Runnable task, long delayMs,
Consumer<Exception> handler) {
this.actualTask = Objects.requireNonNull(task);
this.triggerTimeMs = System.currentTimeMillis() + delayMs;
this.errorHandler = handler; // 예외 핸들러는 선택 사항
}
public void execute() {
try {
actualTask.run();
} catch (Exception e) {
if (errorHandler != null) {
errorHandler.accept(e);
} else {
System.err.println("Unhandled exception in scheduled task: " + e.getMessage());
e.printStackTrace();
}
}
}
@Override
public long getDelay(TimeUnit unit) {
long remaining = triggerTimeMs - System.currentTimeMillis();
return unit.convert(remaining, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed other) {
return Long.compare(this.getDelay(TimeUnit.MILLISECONDS),
other.getDelay(TimeUnit.MILLISECONDS));
}
}
8. 모범 사례
8.1. Delayed 요소 설계
compareTo메서드 구현 정확성:getDelay와compareTo는 일관성 있게 동작해야 합니다.Long.compare(this.getDelay(...), other.getDelay(...))를 사용하는 것이 좋습니다.- 불변성 유지:
Delayed요소는 일단 큐에 추가된 후에는 변경되지 않도록 불변 객체로 설계하는 것이 좋습니다. equals및hashCode구현: 컬렉션에서 요소의 동등성을 검사하거나 해시 기반 컬렉션에 사용될 경우, 이들을 올바르게 구현해야 합니다.
import java.io.Serializable;
import java.util.Objects;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public final class RobustDelayedItem implements Delayed, Serializable {
private static final long serialVersionUID = 1L; // 직렬화 ID
private final String identifier;
private final long expirationEpochMilli; // 절대 만료 시점 (Epoch Millis)
public RobustDelayedItem(String id, long delayMilliseconds) {
this.identifier = Objects.requireNonNull(id, "Identifier cannot be null");
this.expirationEpochMilli = System.currentTimeMillis() + delayMilliseconds;
}
public String getIdentifier() {
return identifier;
}
@Override
public long getDelay(TimeUnit unit) {
long remaining = expirationEpochMilli - System.currentTimeMillis();
return unit.convert(remaining, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed other) {
// 남은 지연 시간을 기준으로 비교
return Long.compare(this.getDelay(TimeUnit.MILLISECONDS),
other.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
RobustDelayedItem that = (RobustDelayedItem) obj;
return expirationEpochMilli == that.expirationEpochMilli &&
Objects.equals(identifier, that.identifier);
}
@Override
public int hashCode() {
return Objects.hash(identifier, expirationEpochMilli);
}
@Override
public String toString() {
return "RobustDelayedItem{id='" + identifier + "', expiresAt=" + expirationEpochMilli + "}";
}
}
8.2. 사용 시나리오 적용
- 큐 크기 지속적 모니터링:
DelayQueue의 크기를 모니터링하여 예상치 못한 메모리 사용량 증가를 감지합니다. - 소비자 스레드 수 조절: 애플리케이션의 부하와 처리량 요구 사항에 맞춰 소비자 스레드 수를 적절히 설정합니다.
- 우아한 종료 로직 구현: 애플리케이션 종료 시
DelayQueue의 모든 스레드를 안전하게 중단하고, 처리되지 않은 요소들을 마저 처리하거나 저장하는 로직을 포함합니다.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
class GracefulDelayQueueProcessor<E extends Delayed> {
private final DelayQueue<E> processingQueue = new DelayQueue<>();
private final List<Thread> workerThreads = new ArrayList<>();
private volatile boolean runningState = true;
private final Consumer<E> elementProcessor;
public GracefulDelayQueueProcessor(Consumer<E> processor) {
this.elementProcessor = processor;
}
public void startWorkers(int numberOfWorkers) {
for (int i = 0; i < numberOfWorkers; i++) {
Thread worker = new Thread(this::workerLoop, "DelayQueue-Worker-" + i);
workerThreads.add(worker);
worker.start();
}
}
public void stopWorkers() {
runningState = false; // 종료 플래그 설정
workerThreads.forEach(Thread::interrupt); // 모든 작업 스레드에 인터럽트 요청
for (Thread worker : workerThreads) {
try {
worker.join(5000); // 각 스레드가 5초 내에 종료되기를 기다림
if (worker.isAlive()) {
System.err.println("Worker thread " + worker.getName() + " did not terminate in time.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Main thread interrupted while waiting for workers to terminate.");
break;
}
}
processRemainingElements(); // 스레드 종료 후 남은 요소 처리
System.out.println("DelayQueue processor stopped gracefully. " + processingQueue.size() + " elements remaining.");
}
private void workerLoop() {
while (runningState || !processingQueue.isEmpty()) { // 실행 중이거나 큐에 요소가 남아있을 경우
try {
// 짧은 타임아웃으로 poll하여 종료 플래그를 주기적으로 확인
E element = processingQueue.poll(100, TimeUnit.MILLISECONDS);
if (element != null) {
elementProcessor.accept(element); // 요소 처리
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName() + " interrupted, stopping.");
break; // 인터럽트 발생 시 루프 종료
} catch (Exception e) {
System.err.println(Thread.currentThread().getName() + " encountered an error: " + e.getMessage());
e.printStackTrace();
}
}
}
private void processRemainingElements() {
E element;
int count = 0;
while ((element = processingQueue.poll()) != null) {
try {
elementProcessor.accept(element);
count++;
} catch (Exception e) {
System.err.println("Error processing remaining element: " + e.getMessage());
}
}
if (count > 0) {
System.out.println("Processed " + count + " remaining elements during shutdown.");
}
}
public boolean addElement(E element) {
return processingQueue.offer(element);
}
public int size() {
return processingQueue.size();
}
}