공공부하자개발 · 영어 학습 노트
자바
고급모던 자바와 성능0/10 완료
  • 01제네릭과 와일드카드
  • 02멀티스레드와 동기화
  • 03람다식과 함수형 인터페이스
  • 04Stream API와 병렬 처리
  • 05Optional로 NPE 방지
  • 06메서드 활용 패턴 (고급)
  • 07Java 21 모던 문법
  • 08어노테이션·리플렉션·동적 프록시
  • 09CompletableFuture 심화와 가상 스레드 실전
  • 10JVM 메모리·GC·OOM 진단
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 고급 › 02 / 10

멀티스레드와 동기화

섹션 7진행 0 / 10
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

5. 자주 하는 실수 (Tip)

❌ 실수 1: run() 을 직접 호출

java
Thread t = new Thread(() -> heavyWork());
t.run();     // 새 스레드가 아니라 현재 스레드에서 그냥 실행됨

run() 은 평범한 메서드입니다. 병렬성이 전혀 생기지 않는데 코드는 멀쩡해 보여서 오래 눈치 못 챕니다.

java
✅
Thread t = new Thread(() -> heavyWork());
t.start();   // OS 스레드 생성 → 그 스레드가 run() 호출

❌ 실수 2: volatile 로 count++ 보호

java
private volatile int count;
public void hit() { count++; }     // 여전히 갱신 손실 발생

volatile 은 가시성만 보장합니다. 읽기-더하기-쓰기 세 단계 사이에 다른 스레드가 끼어드는 것은 막지 못합니다.

java
✅
private final AtomicInteger count = new AtomicInteger();
public void hit() { count.incrementAndGet(); }
// 또는 synchronized 블록

❌ 실수 3: submit() 하고 Future.get() 을 안 함 → 예외 실종

java
pool.submit(() -> { processPayment(order); });   // 안에서 예외가 나도 아무 로그도 없음

submit 은 예외를 Future 안에 담아 두고, 아무도 get() 을 안 하면 그대로 사라집니다. 결제가 실패했는데 성공한 줄 아는 사고가 납니다.

java
✅
Future<?> f = pool.submit(() -> processPayment(order));
try { f.get(); } catch (ExecutionException e) { log.error("payment failed", e.getCause()); }
// 결과가 필요 없으면 execute() 를 쓰고 UncaughtExceptionHandler 를 설정하거나, 작업 안에서 try-catch

❌ 실수 4: shutdown() 누락 → JVM 이 안 끝남

java
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(task);
// main 이 끝나도 워커 스레드(non-daemon)가 살아 있어 프로세스가 종료되지 않음
java
✅
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {   // JDK 19+
    pool.submit(task);
}   // close() → shutdown() + awaitTermination()
// 구버전: finally { pool.shutdown(); pool.awaitTermination(30, SECONDS); }

❌ 실수 5: 락 안에서 외부 메서드 호출 / 락 순서 불일치

java
synchronized void transferTo(Account other, long amt) {
    synchronized (other) { ... }     // this → other 순서. 반대 방향 호출과 만나면 데드락
}

락을 잡은 채 다른 락을 잡으면 순환 대기 조건이 성립할 수 있습니다.

java
✅
// 전역 순서(예: id) 로 정렬해서 항상 같은 순서로 잠근다 (예제 2 참고)
// 또는 tryLock(timeout) 으로 실패 시 전부 해제 후 재시도

❌ 실수 6: 스레드 풀에서 ThreadLocal 을 remove() 안 함

java
CURRENT_USER.set(user);
handle(request);
// remove 없음 → 이 워커 스레드가 다음 요청을 처리할 때 이전 사용자가 보임
java
✅
CURRENT_USER.set(user);
try { handle(request); }
finally { CURRENT_USER.remove(); }

❌ 실수 7: 가상 스레드를 풀링하거나 CPU 작업에 사용

java
ExecutorService pool = Executors.newFixedThreadPool(200, Thread.ofVirtual().factory());   // 의미 없음

가상 스레드는 싸니까 작업마다 하나씩 만드는 것이 설계 의도입니다. 200개로 제한하면 이점이 사라집니다. 동시성 상한이 필요하면 Semaphore 로 제어합니다.

java
✅
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
    Semaphore limit = new Semaphore(50);                 // 외부 API 동시 호출 50개 제한
    for (var req : requests) ex.submit(() -> { limit.acquire(); try { call(req); } finally { limit.release(); } });
}
자주 하는 실수 (Tip)
  • ❌ 실수 1: run() 을 직접 호출
  • ❌ 실수 2: volatile 로 count++ 보호
  • ❌ 실수 3: submit() 하고 Future.get() 을 안 함 → 예외 실종
  • ❌ 실수 4: shutdown() 누락 → JVM 이 안 끝남
  • ❌ 실수 5: 락 안에서 외부 메서드 호출 / 락 순서 불일치
  • ❌ 실수 6: 스레드 풀에서 ThreadLocal 을 remove() 안 함
  • ❌ 실수 7: 가상 스레드를 풀링하거나 CPU 작업에 사용
이전 섹션4 응용 변형 예제5 / 7다음 섹션6 연습 문제