본문 바로가기

STUDY/이펙티브자바

아이템 81: 동시성 유틸리티를 애용하라

wait와 notify

 

public class Main {
    public synchronized void waitMehod() throws InterruptedException {
        System.out.println(Thread.currentThread().getId() + " wait");
        wait();
        System.out.println(Thread.currentThread().getId() + " notified");
    }
	
    public synchronized void notifyMethod() throws InterruptedException {
        System.out.println(Thread.currentThread().getId() + " notify all");
        notifyAll();
    }
	
    public static void main(String[] args) throws InterruptedException {
        Main main = new Main();
        Thread newThread1 = new Thread(() -> {
            main.waitMehod();
        });
        Thread newThread2 = new Thread(() -> {
            main.notifyMethod();
        });
        newThread1.start();
        newThread2.start();
    }
}

 

// 실행결과
15 wait
16 notify all
15 notified

 

Java에서는 스레드의 실행 흐름을 제어하기 위한 wait(), notify() 메소드가 Object에 구현되어있다. 해당 스레드가 잠시 멈추어야 할 때는 wait()를 호출하고, 정지된 스레드를 다시 실행시키기 위해서 notify()를 호출하면 된다. 동시성 제어를 위해서는 이 두개의 함수를 잘 활용하는 것이 중요했다. 하지만 현재는 고수준의 동시성 제어를 제공하는 유틸리티들이 존재하므로 wait와 notify로 기능을 직접 구현하기 보다 존재하는 유틸리티를 활용하는 것이 좋다.

 

 


Blocking Queue

 

BlockingQueue<Integer> bq = new ArrayBlockingQueue<Integer>(10);
Thread newThread1 = new Thread(() -> {
    System.out.println(bq.take());
});
Thread newThread2 = new Thread(() -> {
    bq.add(10);
});

newThread1.start();
newThread2.start();

 

BlockingQueue의 take 메소드는 가장 최근에 삽인된 데이터를 반환한다는 점에서 기존 큐의 pop과 같은 역할을 한다. 하지만 큐에 데이터가 비어있을 경우 스레드를 정지시키고 새로운 데이터가 삽입될까지 기다린다. ThreadPoolExecutor를 포함한 대부분의 실행자 서비스 구현체에서 BlockingQueue를 활용한다.

 

...
public Integer take() {
    while (count == 0)
        wait();
    ...
}
...
public Integer add(Integer item) {
    count++;
    notifyAll();
    ...
}
...

wait와 notify를 활용하여 BlockingQueue를 구현한다면 위와 같은 모습일 것이다. 실제로는 내부적으로 더욱 복잡한 방식으로 동작하고 있고, 이를 직접 구현하려 하기보다는 BlockingQueue를 활용하는 것이 좋다.

 

 


ConcurrentHashMap

 

서비스를 개발하다보면 메소드에 원자성을 보장해야하는 경우가 많다. 하지만 비동기 환경에서 원자성을 보장하는 것을 쉽지 않다. 예를 들어 어떠한 Map 객체의 특정 key-value가 비어있는 경우에만 데이터를 넣어주는 putIfAbsent 메소드를 개발한다고 하면 아래와 같은 코드가 나올 것이다.

 

Map<Integer, Integer> syncMap = Collections.synchronizedMap(new HashMap<Integer, Integer>());
Integer item = i;
synchronized (syncMap) {
    if(syncMap.get(item) == null) {
        syncMap.put(item, item);
    }
}

이미 비동기 환경을 위해 synchronizedMap을 사용하고 있지만 원자성 보장을 위해 또 하나의 synchronized문으로 둘러쌓여져있다. synchronizedMap은 내부적으로 synchronized을 사용하고 있고 이를 다시 synchronized 문으로 감싸면 결과적으로 성능에 매우 부정적인 영향을 미치게 된다.

 

Map<Integer, Integer> syncMap2 = new ConcurrentHashMap<Integer, Integer>();
Integer item = i;
syncMap2.putIfAbsent(item, item);

이때 synchronizedMap을 ConcurrentHashMap으로 교체 하는 것 만으로 상당한 성능 향상을 이루어 낼 수 있다. ConcurrentHashMap의 putIfAbsent는 내부적으로 하나의 synchronized문을 사용한다. 이처럼 이미 존재하는 동시성 유틸리티를 적극적으로 활요하는 것이 좋다.

 

 

728x90