[아이템81] wait 와 notify 보다는 동시성 유틸리티를 애용해라

2024. 12. 18. 00:43·1️⃣ 백앤드/이펙티브 자바

1. wait 과 notify

wait 과 notify 는 멀티쓰레드 환경에서 동기화된 작업을 위해 사용된다. 이 메서드들은 특정 조건이 충족될 때까지 스레드를 대기 상태로 만들거나, 대기 중인 스레드를 깨우는 데 사용된다.

쓰레드 라이프싸이클

wait() 메서드는 현재 쓰레드를 대기 상태로 만들며, 객체의 모니터 락을 걸다가 다른 쓰레드가 notify() 메서드로 깨우면 락이 해제된다.
notify() 메서드는 대기 상태에 있는 쓰레드를 깨우는 역할을 한다.
class SharedResource {
    synchronized void waitMethod() {
        try {
            System.out.println("Thread is waiting...");
            wait(); // 현재 스레드 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread resumed!");
    }

    synchronized void notifyMethod() {
        System.out.println("Thread is being notified...");
        notify(); // 대기 중인 스레드 하나를 깨움
    }
}

public class WaitNotifyExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();

        Thread thread1 = new Thread(() -> resource.waitMethod());
        Thread thread2 = new Thread(() -> resource.notifyMethod());

        thread1.start();
        try { Thread.sleep(1000); } catch (InterruptedException e) { }
        thread2.start();
    }
}

Thread is waiting...
Thread is being notified...
Thread resumed!

 

하지만 wait 과 notify 메서드를 올바르게 사용하기가 아주 까다로워, 고수준 동시성 유틸리티를 사용하는 편이 낫다.

 

2. java.util.concurrent 고수준 유틸리티

2-1. 동시성 컬렉션

동시성 컬렉션은 List, Queue, Map 같은 표준 컬렉션 인터페이스에 동시성을 가미해 구현한 고성능 컬렉션이다. 동기화를 각자 내부에서 수행한다. 따라서 동시성 컬렉션에서 동시성을 무력화하는 것은 불가하고, 외부에서 락을 추가로 사용하면 오히려 속도가 느려진다. 아래 ConcurrentMap 으로 구현한 예시를 살펴보자.

private static final ConcurrentMap<String, String> map = new ConcurrentHashMap<>();

public static String intern(String s) {
    String previousValue = map.putIfAbsent(s, s);
    return previousValue == null ? s : previousValue;
}

pubIfAbsent 메서드는 주어진 키에 매핑된 값이 아직 없을 때만 새 값을 집어넣는다. 기존에 값이 있다면 값을 반환하고, 없다면 null 을 반환한다. 이 메서드 덕에 스레드 안전하게 구현할 수 있다. ConcurrentHashMap 을 통해 더 개선해보자.

public static String intern(String s) {
    String result = map.get(s);
    if (result == null) {
        result = map.putIfAbsent(s, s);
        if (result == null)
            result = s;
    }
    return result;
}

ConcurrentHashMap 은 get 같은 검색 기능에 최적화되어있기 때문에 get 을 먼저 호출하고, 필요한 때만 putIfAbsent 를 호출하도록 수정할 수 있다. ConcurrentHashMap 은 동시성에도 뛰어나고, 속도도 무척 빠르다.

2-2. 동기화 장치

동기화 장치는 쓰레드가 다른 쓰레드를 기다릴 수 있게 하여서, 서로의 작업을 조율할 수 있게 도와준다. 이 때, 가장 많이 사용되는 장치는 CountDownLatch 와 Semaphore 다. 그 중, CountDownLatch 를 살펴보자.

CountDownLatch 는 하나 이상의 쓰레드가 또 다른 하나 이상의 쓰레드 작업이 끝날 때 까지 기다리게 한다.

이 장치를 활용하면, 예를 들어 어떤 동작 여러개를 동시에 시작해 모두 완료하기까지의 시간을 재는 프로그램을 만들 수 있다. 메서드 하나면 된다. 매개변수로, 동작들을 실행할 실행자와 동작을 몇 개나 동시에 수행할 수 있는지를 뜻하는 동시성 수준 (concurrency) 를 받는다. 타이머 쓰레드가 시계를 시작하기 전 모든 작업자 쓰레드는 동작할 준비를 마치고, 마지막 작업자 쓰레드가 준비를 마치면 타이머 쓰레드가 '시작 방아쇠'를 당겨 일을 시작시키고, 마지막 작업이 끝나면 시계를 멈춘다. 일련의 과정을 wait, notify 로 구현하려면 굉장히 난잡해진다.

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(); 
            try {
                // 모든 작업자 스레드가 준비될 때까지 기다린다.
                start.await();
                action.run();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                // 타이머에게 작업을 마쳤음을 알린다.
                done.countDown();  
            }
        });
    }

    ready.await();     // 모든 작업자가 준비될 때까지 기다린다.
    long startNanos = System.nanoTime();
    start.countDown(); // 작업자들을 깨운다.
    done.await();      // 모든 작업자가 일을 끝마치기를 기다린다.
    return System.nanoTime() - startNanos;
}

CountDownLatch 3개를 사용한다. ready 는 준비 완료, start 는 시작시간 기록, done 은 마지막 작업자 쓰레드가 동작을 마친 후 사용된다. 마지막에 타이머 쓰레드는 done 래치가 열리자마자 깨어나 종료 시각을 기록하게 된다. 

 

3. 정리

코드를 새로 작성한다면 wait 과 notify 쓸 이유가 없고, java.util.concurrent 의 고수준 동시성 유틸리티를 사용하자.

'1️⃣ 백앤드 > 이펙티브 자바' 카테고리의 다른 글

[아이템83] 지연 초기화는 신중히 사용하라  (0) 2024.12.19
[아이템82] 스레드 안전성 수준을 문서화해라  (0) 2024.12.19
[아이템80] 스레드보다는 실행자, 태스크, 스트림을 애용하라  (0) 2024.12.16
[아이템79] 과도한 동기화는 피하라  (0) 2024.12.12
[아이템78] 공유 중인 가변 데이터는 동기화해 사용하라  (0) 2024.12.11
'1️⃣ 백앤드/이펙티브 자바' 카테고리의 다른 글
  • [아이템83] 지연 초기화는 신중히 사용하라
  • [아이템82] 스레드 안전성 수준을 문서화해라
  • [아이템80] 스레드보다는 실행자, 태스크, 스트림을 애용하라
  • [아이템79] 과도한 동기화는 피하라
HOZINU
HOZINU
주니어 백앤드 개발자의 세상만사 이모저모. 주로 개발 이야기를 다룸.
  • HOZINU
    백엔드 탐험 일지
    HOZINU
  • 전체
    오늘
    어제
  • 블로그 메뉴

    • ⛪ HOME
    • 🌍 GITHUB
    • 카테고리 (73)
      • 1️⃣ 백앤드 (72)
        • 이펙티브 자바 (72)
      • 2️⃣ CS (0)
        • 운영체제 (0)
        • 네트워크 기초 (0)
        • 네트워크 응용 (0)
        • SSL & PKI (0)
        • 기타 (0)
      • 3️⃣ 코딩테스트 (0)
      • 4️⃣ 개인공부 (0)
        • MSA (0)
        • REDIS (0)
      • 5️⃣ 일상이야기 (1)
  • 인기 글

  • 태그

    캡슐화
    정보은닉
    컴포지션
    멤버클래스
    try-with-resources
    finalizer
    싱글턴
    hashcode
    optional
    Cleaner
    빌더
    계층구조
    CLONE
    Comparable
    로타입
    맥북
    표준예외
    의존객체
    equals
    정적 팩터리 메서드
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
HOZINU
[아이템81] wait 와 notify 보다는 동시성 유틸리티를 애용해라
상단으로

티스토리툴바