81. wait와 notify보다는 동시성 유틸리티를 애용하라

📌 1. 발표 전 알아야 할 개념

동시성(Concurrency)

  • 여러 작업이 논리적으로 동시에 실행되는 것

  • 멀티 스레딩, 병렬 처리 등을 통해 작업 효율성 향상

  • 시스템 자원 활용 최적화, 응답성 향상, 처리량 증가 등이 목적

다시 한 번 복습

  • 스레드(Thread) : 프로세스 내 실행되는 흐름의 단위

  • 공유 자원 : 여러 스레드가 동시에 접근할 수 있는 데이터나 리소스

  • 동기화(Synchronization) : 여러 스레드가 공유 자원에 안전하게 접근하게 하는 매커니즘

  • 데드락(Deadlock) : 두 개 이상의 스레드가 서로 자원을 기다리며 무한히 대기하는 상태

📕 2. wait와 notify

// 자바의 동시성 매커니즘 발전 과정
초기 자바 → wait()/notify() → java.util.concurrent 패키지

2-1. wait()notify() 메서드

  • 동시성 프로그래밍의 기본이 되는 메서드들

  • wait() : 스레드가 특정 조건이 충족될 때까지 대기하게 함

  • notify() : 대기 중인 스레드 중 하나를 깨움

  • notifyAll() : 대기 중인 모든 스레드를 깨움

synchronized (obj) {
    while (<조건이 충족되지 않았다>)
        obj.wait();  // 락을 놓고, 깨어나면 다시 잡는다

    // 조건이 충족됐을 때의 동작을 수행한다
}
  • 반드시 synchronized 블록 안에서 호출해야 한다

  • 객체의 내장 모니터 락과 연계하여 동작한다

2-2. wait()notify()의 한계점

  • 정확한 락 제어가 필요하다

    • 반드시 synchronized 블록 안에서만 사용 가능함

  • 오류 발생 가능성이 높다

    • spurious wakeup : 조건이 만족되지 않았는데, 스레드가 깨어나는 경우가 존재함

  • 정교한 동기화 패턴 구현이 어렵다

    • notify()는 무작위 하나의 스레드만 깨움 - 필요한 스레드가 아닐 수 있음

    • notifyAll()은 모든 대기 스레드를 깨우지만 성능 저하의 우려가 있음

🔧 3. java.util.concurrent로 대체하자

  • 자바의 고수준 동시성 유틸리티를 제공하는 패키지

  • wait()/notify()의 단점을 보완하기 위해 도입

3-1. ConcurrentMap - 스레드 안전한 Map

동시에 여러 스레드가 값을 넣거나 꺼내도, 충돌 없이 안전하게 동작함

// 문자열 중복 제거 기능
private static final ConcurrentMap<String, String> map = new ConcurrentHashMap<>();

public static String intern(String s) {
    String result = map.putIfAbsent(s, s); // key가 없을 때만 추가
    return result == null ? s : result;
}
  • putIfAbsent는 동시에 여러 스레드가 같은 키로 put해도, 단 하나만 저장되도록 보장

  • synchronized 없이도 안전하며, 코드가 간결해지고 성능도 향상됨

3-2. CountDonwLatch - 스레드 실행 및 타이밍 제어

  • 모든 스레드가 준비되면, 하나의 신호(countDown)로 동시에 출발함

N개의 작업을 동시에 시작, 모두 끝날때까지 대기
public static long time(Executor executor, int concurrency, Runnable action) throws InterruptedException {
    CountDownLatch ready = new CountDownLatch(concurrency);
    CountDownLatch start = new CountDownLatch(1);
    CountDownLatch done = new CountDownLatch(concurrency);

    for (int i = 0; i < concurrency; i++) {
        executor.execute(() -> {
            ready.countDown();        // 준비 완료
            start.await();            // 시작 신호 기다림
            action.run();             // 작업 실행
            done.countDown();         // 작업 완료
        });
    }

    ready.await();    // ready : 모든 스레드 준비될 때까지 대기
    long startTime = System.nanoTime();
    start.countDown(); // start : 일제히 시작!
    done.await();     // done : 모든 스레드 작업 끝날 때까지 대기
    return System.nanoTime() - startTime;
}
  • ready : 모든 작업자 준비 신호

  • start : 한 번에 모두 시작(출발 신호)

  • done : 작업 종료 확인

3-3. BlockingQueue - 생산자-소비자 문제의 해답

  • 생산자(Producer)가 데이터를 만들고, 소비자(Consumer)가 소비하는 구조

  • 큐가 꽉 차면 기다리고, 비면 또 기다리는 자동 신호 시스템

3-4. java.util.concurrent.atomic 패키지

  • 락 없이도 원자적 연산을 지원하는 클래스들

  • AtomicInteger, AtomicLong, AtomicReference

// 스레드 안전한 카운터
private static final AtomicInteger counter = new AtomicInteger();

public static int incrementAndGet() {
    return counter.incrementAndGet();  // 원자적 증가 연산
}

👍🏼 향후 발전 포인트

  • 또 다른 유용한 동시성 유틸리티들

    • Semapore - 리소스 접근 제한

    • CyclicBarrier - 여러 스레드가 특정 지점에서 만남

    • Pharser - 더 유연한 단계별 동기화화

🤖 최종 결론

wait, notify는 저수준 매커니즘으로 오류가능성이 높다 java.util.concurrent 는 더 높은 수준의 유틸리티를 제공한다 wait / notify 대신 동시성 유틸리티를 사용하자


😶‍🌫️ 느낀점

  • 자바의 유틸리티 메서드를 공부하자

Last updated