Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Mingadinga opened this issue Sep 12, 2023 · 0 comments
Open

Comments

@Mingadinga
Copy link
Member

동기화 장치

동기화 장치란

  • 스레드가 다른 스레드를 기다릴 수 있게 하여, 서로 작업을 조율할 수 있게 해줌
  • 자주 사용 : CountDownLatch, Semaphore
  • 가장 강력 : Phaser

CountdownLatch

CountdownLatch

  • 일회성 장벽
  • 하나 이상의 스레드가 다른 스레드 작업이 끝날 때까지 기다리게 한다
  • 정해진 횟수만큼 countDown()이 호출되면 대기 중인 스레드들을 깨운다

예제 : 동시성을 필요로 하는 프레임워크 구축

  • 어떤 동작을 동시에 시작해 모두 완료하기까지의 시간을 잰다
  • 유일한 메소드의 매개변수 : 동작을 실행할 실행자, 동작을 몇개나 동시에 수행할 수 있는지를 뜻하는 동시성 수준(concurrency)

시나리오

  • 모든 작업자 스레드는 동작을 수행할 준비를 한다
  • 작업자 스레드가 준비를 마치면 타이머 스레드가 시작 방아쇠를 당겨 작업자 스레드들이 일을 시작한다.
  • 마지막 작업자 스레드가 동작을 마치자마자 타이머 스레드는 시계를 멈춘다.
package effectivejava.chapter11.item81;
import java.util.concurrent.*;

// Simple framework for timing concurrent execution 327
public class ConcurrentTimer {
    private ConcurrentTimer() { } // Noninstantiable

    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(); // Tell timer we're ready
                try {
                    start.await(); // Wait till peers are ready
                    action.run();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown();  // Tell timer we're done
                }
            });
        }

        ready.await();     // Wait for all workers to be ready
        long startNanos = System.nanoTime();
        start.countDown(); // And they're off!
        done.await();      // Wait for all workers to finish
        return System.nanoTime() - startNanos;
    }
}
  • 준비 : 각 스레드에서 ready.countDown 호출, start.await에서 걸려 start가 깨어나기를 기다림
  • 실행 : 마지막 스레드가 ready.countDown 호출하면 타이머 스레드가 시간 재기를 시작하고 start.countDown를 호출해서 기다리던 스레드를 깨운다.
  • 종료 : 타이머 스레드는 세번째 래치 done이 열리기를 기다리다 done.countDown가 호출되면 열린다. 타이머 스레드는 done 래치가 열리자마자 깨어나 종료 시간을 기록한다.

추가적인 사항

  • 이때 동시성 수준 (concurrency)만큼의 스레드를 생성하지 않는다면, 스레드 기아 교착 상태가 발생한다.
  • 시간 간격을 잴 때는 더 정밀하고 시간 보정에 영향을 받지 않는 System.nanoTime()을 사용하는 편이 좋다.

wait와 notify 사용 시 주의사항

레거시 코드라면 어쩔 수 없이 wait와 notify를 다뤄야한다. 새로 작성하는 코드라면 concurrent 패키지의 요소들을 사용하는 것이 좋다.

wait의 표준 사용법

  • wait : 스레드가 어떤 조건이 충족되기를 기다린다.
  • 동기화 영역 안에서 호출해야한다.
  • while문과 함께 사용해 악의적인 notify()에 의해 스레드가 깨어나는 것을 방지한다.
synchronized (obj) {
	while(<조건이 충족되지 않았다>) obj.wait();
	// 조건이 충족됐을 때의 동작 수행
}

더 안전하게 사용하기

  • 대기 전에 조건을 검사해 충족되었다면 wait를 건너뛴다. 조건이 충족되었는데 스레드가 notify를 먼저 호출해서 대기 상태에 빠지면, 그 스레드를 다시 깨울 수 있다는 보장이 없다.
  • 대기 후에 조건을 검사해 조건이 충족되지 않았다면, 다시 대기하게 한다. 조건이 충족되지 않았는데 스레드가 동작을 이어가면 락이 보호하는 불변식을 깨뜨릴 위험이 있다.

조건이 만족되지 않아도 스레드가 깨어날 수 있는 상황

  • 다른 스레드가 실수로 혹은 악의적으로 notify를 호출
  • notify로 스레드가 깨어나는 사이에 다른 스레드가 락을 얻어 그 락이 보호하는 상태 변경
  • 깨우는 스레드가 지나치게 관대해서, 대기 중인 스레드 중 일부만 조건이 충족되어도 notifyAll로 모든 스레드를 깨울 수도 있다
  • 대기 중인 스레드가 드물게 notify 없이도 깨어나는 경우가 있다. 허위 각성이라고 부른다.

notify vs notifyAll

둘의 차이

  • notify : 특정 스레드를 깨움
  • notifyAll : 대기 중인 모든 스레드를 깨움

notifyAll를 사용하는 것이 좋다

  • 모든 스레드가 깨어나니 항상 정확한 결과를 얻을 것이다.
  • 관련 없는 스레드가 실수로 혹은 악의적으로 wait를 호출하는 공격으로부터 보호할 수 있다.
  • notify를 사용하면 스레드가 wait 공격을 받아 응답 불가 상태에 빠질 수도 있다.

notify 최적화

  • 모든 스레드가 같은 조건을 기다리고
  • 조건이 한번 충족될 때마다 단 하나의 스레드만 혜택을 받음
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant