Java 기초부터 마스터까지 — 8화
스레드와 비동기 작업을 안전하게 실행하기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
동시에 여러 작업을 실행하면 처리량을 높일 수 있지만 공유 상태도 함께 복잡해진다. 스레드 개수보다 누가 상태를 변경하고 완료와 실패를 어떻게 전달할지 먼저 정해야 한다.
ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
Future<Integer> result = executor.submit(() -> loadScore());
int score = result.get();
System.out.println(score);
} finally {
executor.shutdown();
}
작업을 ExecutorService에 제출하면 스레드 생성과 재사용을 실행기가 관리한다. Future의 get은 완료까지 현재 스레드를 기다리게 하고 작업 예외도 전달한다.
공유 상태 보호하기
private final Object lock = new Object();
private int gold;
public void addGold(int amount) {
synchronized (lock) {
gold += amount;
}
}
같은 값을 여러 스레드가 동시에 읽고 쓰면 연산이 유실되거나 중간 상태가 보일 수 있다. synchronized는 같은 잠금을 사용하는 구간을 한 번에 하나의 스레드만 실행하게 한다.
잠금 범위가 너무 넓으면 기다림이 늘고, 여러 잠금을 다른 순서로 얻으면 교착 상태가 생길 수 있다. 가능하면 변경 가능한 공유 상태 자체를 줄이는 편이 좋다.
CompletableFuture
CompletableFuture<Player> playerFuture =
CompletableFuture.supplyAsync(this::loadPlayer, executor);
CompletableFuture<List<Item>> itemFuture =
CompletableFuture.supplyAsync(this::loadItems, executor);
CompletableFuture<Void> all =
CompletableFuture.allOf(playerFuture, itemFuture);
all.join();
독립된 작업을 먼저 시작하고 완료 뒤의 단계를 조합할 수 있다. 어떤 실행기에서 코드가 동작하는지와 예외를 어디서 처리하는지 분명하게 해야 한다.
동시성은 코드를 자동으로 빠르게 만들지 않는다. 작업 크기, 대기 시간, 공유 상태, 실행기 종료를 측정하고 관리해야 한다.