홈으로 돌아가기

Java 스레드: 아키텍처와 동기화

이 글은 하드웨어 프로세서 수준부터 JVM까지 Java 스레드의 구조를 분해합니다. 동기화 메커니즘, Race Condition, Deadlock, Livelock 문제와 효과적인 개발을 위한 비동기 CompletableFuture를 설명합니다.

Java 멀티스레딩: CPU 코어부터 Deadlock까지
Advertisement 728x90

Java 스레드 아키텍처와 동기화: CPU부터 JVM까지

Java 코드는 바이트코드로 컴파일되며, JVM이 이를 프로세서의 기계어 명령어로 변환합니다. 각 CPU 코어는 명령어를 순차적으로 실행합니다: 레지스터에서 비트 시프트 같은 간단한 연산은 한 클럭 사이클에 처리됩니다. 레지스터는 숫자, 주소, 데이터를 저장하는 초고속 메모리 셀입니다.

현대 프로세서는 슈퍼스칼라 아키텍처를 사용합니다. 하이퍼스레딩은 단일 하드웨어 스레드가 한 사이클에 2~6개의 명령어를 실행하게 해, 빠른 컨텍스트 스위칭으로 한 코어당 여러 논리 스레드가 있는 듯한 착각을 줍니다.

동시에 실행되는 스레드 수는 코어 수에 제한됩니다. 스레드들은 객체를 위한 힙 메모리를 공유하지만, 각자 로컬 변수와 객체 참조를 위한 스택을 가집니다.

Google AdInline article slot

레이스 컨디션과 메모리 구조

레이스 컨디션은 여러 스레드가 동시에 같은 힙 객체를 읽고 수정할 때 발생합니다. 결과는 읽기-수정-쓰기 작업 순서에 따라 예측 불가능합니다.

코드는 로컬에서 부하 없이 잘 동작하지만, 프로덕션 환경의 멀티스레딩에서 불규칙적으로 충돌이 발생합니다. OS 프로세스는 공유 메모리를 할당하며, 그 안의 스레드들은 자원을 놓고 경쟁하는 가벼운 실행 단위입니다.

동기화 메커니즘: wait/notify와 모니터

모든 Java 객체는 내장 모니터(락)를 가집니다. synchronized 블록은 객체의 모니터를 획득해 다음을 보장합니다:

Google AdInline article slot
  • 상호 배제: 블록 안 코드는 한 스레드만 실행.
  • 가시성: 블록 종료 후 변경 사항이 다른 스레드에 보임 (volatile 지원 필요).

모니터는 다음을 포함합니다:

  • Entry Set: 획득 대기 스레드 큐.
  • Wait Set: 대기 상태 스레드.

wait(), notify(), notifyAll()같은 객체의 synchronized 블록 안에서만 동작—아니면 IllegalMonitorStateException 발생.

  • wait(): 스레드를 Wait Set으로 이동.
  • notify(): 랜덤 스레드 깨움 (신호 손실 위험).
  • notifyAll(): 모든 스레드 깨움 (권장).

정적 synchronized 메서드는 클래스 모니터(Class)를 락하며, 인스턴스 메서드(this)와 충돌 없음.

Google AdInline article slot

고급 락킹: ReentrantLock과 Semaphore

더 세밀한 제어를 위해 java.util.concurrent.locks.ReentrantLock 사용:

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 크리티컬 섹션
} finally {
    lock.unlock();
}

주요 메서드:

  • tryLock(): 대기 없이 획득 (실패 시 false 반환).
  • tryLock(timeout, unit): 타임아웃 대기.
  • lockInterruptibly(): 인터럽트 가능 획득.
  • new ReentrantLock(true): 공정 락 (FIFO 순서, 느림).

Semaphore는 접근 제한:

Semaphore sem = new Semaphore(3); // 3개 허가
sem.acquire(); // 획득
sem.release(); // 해제

멀티스레딩 함정: 데드락과 라이브락

데드락: 순환 락 의존. 스레드 A가 lock1 보유하고 lock2 대기; 스레드 B가 반대. 시스템 멈춤.

피하는 법:

  • 고정 순서로 락.
  • 타임아웃 사용.
  • 중첩 락 최소화.

라이브락: 스레드 바쁘지만 진척 없음. 예: 두 스레드가 서로 양보하며 루프 돌며 CPU 폭증.

스레드의 동기 vs 비동기

동기 메서드는 I/O(예: DB 쿼리)에서 스레드 블로킹: 응답까지 스레드 유휴.

비동기는 CompletableFuture 사용—지연 결과 컨테이너(대기/완료/실패).

연산 체인:

  • thenApply(): 결과 변환.
  • thenCompose(): 다른 Future와 조합.
  • thenAccept(): 반환 없이 소비.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "result")
    .thenApply(s -> s.toUpperCase())
    .thenAccept(System.out::println);

이로 스레드가 다른 작업에 자유로워 멀티스레드 환경 처리량 증가.

주요 요약

  • 코어당 하드웨어 스레드 하나; 하이퍼스레딩으로 다수 시뮬레이션.
  • 레이스 컨디션은 공유 힙에서; 동기화로 상호 배제와 가시성 보장.
  • 신호 손실 피하려 notifyAll() 선호.
  • ReentrantLock은 타임아웃, 인터럽트, 공정성으로 synchronized 확장.
  • CompletableFuture로 블로킹 없이 비동기 파이프라인.

— Editorial Team

Advertisement 728x90

다음 읽기