Java/JVM

[JVM 밑바닥까지 파헤치기] 12-1. 자바 메모리 모델과 스레드: 동시성

noahkim_ 2024. 12. 25. 23:35

저우즈밍 님의 "JVM 밑바닥까지 파헤치기" 책을 정리한 포스팅 입니다

 

1. 컴퓨터가 여러 작업을 동시에 수행하는 이유

  • 연산 성능과 저장 및 통신 성능의 격차가 크기 떄문
  • ✅ 프로세서가 요청한 자원의 대기 시간을 활용
  • ✅ 서버는 여러 클라이언트 요청을 동시에 처리하므로 동시 처리가 필수적

 

동시성 프로그래밍의 어려움

  • 스케줄링 비결정성: 스레드 실행 순서는 매번 달라짐 (재현이 어려움)
  • 공유 메모리 문제: 가시성, 원자성, 순서 등으로 인해 예상치 못한 결과가 발생함

 

스케줄링 비결정성 문제) 실행 순서

더보기
Thread t1 = new Thread(() -> {
    for (int i = 0; i < 5; i++) System.out.println("Thread-1 : " + i);
});

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 5; i++) System.out.println("Thread-2 : " + i);
});

t1.start();
t2.start();
  • 실행할 때마다 순서가 바뀜
  • 어떤 스레드가 먼저 CPU를 잡을지는 OS 스케줄러에 의해 결정됨

 

공유 메모리 문제) 가시성

더보기
  • 한 스레드가 값을 써도, 다른 스레드가 즉시 그 값을 보지 못할 수 있음
  • ⚠️ 각 CPU 코어가 가진 자기 캐시값이 즉시 반영되지 않은 경우
  • ⚠️  다른 코어와의 동기화가 늦을 경우

 

공유 메모리 문제) 원자성 

더보기
int sum = 0;

List<Integer> list = IntStream.range(1, 1001).boxed().toList();
list.parallelStream().forEach(i -> sum += i);

System.out.println(sum);
  • java의 명령어 하나는 여러 기계어로 구성되어 있음
  • ✅ CPU 스케줄러는 기계어 단위로 원자성을 보장함
  • ⚠️ 한 스레드의 연산 중 중단 상태에서 컨텍스트 스위치가 발생하여 다른 스레드에 의해 연산될 경우 값이 꼬일 수 있음
  • ⚠️ 원자성을 보장하지 못하므로 레이스 컨디션이 발생함

 

AtomicInteger sum = new AtomicInteger(0);

list.parallelStream().forEach(i -> sum.addAndGet(i));
  • ➡️ 중간 상태가 끼어들지 못하도록 CAS 기반의 원자 연산을 해야 함

 

공유 메모리 문제) 순서

더보기
  • 코드에 적힌대로 작업하지 않는 경우가 발생할 수 있음
  • ⚠️ CPU/JIT 연산의 성능을 위해 명령 실행 순서가 변경될 수 있음

 

동시성 프로그래밍 지원

구분 제공 기능 설명 해결하는 문제
하드웨어 추상화 OS 스레드 래핑 JVM이 OS 네이티브 스레드를 감싸서 사용 OS 의존성 감소
  Thread API Thread, Runnable, Callable 제공 스레드 생성/실행 단순화
  스케줄링 지원 JVM이 OS 스케줄러와 연동 개발자가 직접 관리 불필요
스레드 관리 자동화 ThreadPool 스레드 재사용 및 작업 큐 관리 과도한 스레드 생성 비용 방지
  ExecutorService 작업 제출 중심 API 스레드 직접 관리 제거
  ThreadPoolExecutor 세밀한 풀 설정 가능 서버 환경 최적화
메모리 모델 제공 (JMM) 가시성 보장 volatile 키워드 캐시 불일치 문제 해결
  원자성 보장 synchronized, Atomic* 연산 중간 개입 방지
  순서 보장 Happens-Before 규칙 명령 재정렬 문제 해결
고수준 동시성 API 스레드 세이프 컬렉션 ConcurrentHashMap, CopyOnWriteArrayList 직접 동기화 실수 방지
  원자 변수 AtomicInteger, AtomicLong 등 CAS 기반 락 없는 연산
  Lock API ReentrantLock 등 synchronized보다 유연한 제어
  동기화 도구 CountDownLatch, Semaphore, CyclicBarrier 작업 간 협업 제어
  비동기 처리 CompletableFuture 논블로킹, 비동기 흐름 제어

 

2. 하드웨어의 효율과 일관성

캐시의 필요성

  • 메모리 I/O는 연산작업에 비해 매우 느림
  • 이를 보완하고자 메모리와 CPU 사이의 계층을 둠
  • 필요한 데이터를 캐시에 복사해 두어 작업을 빠르게 수행
  • 작업이 완료되면 결과 데이터를 캐시에서 메모리로 동기화

 

공유 메모리 멀티프로세스 시스템

  • 프로세서별 캐시는 각각 존재하므로 자신의 캐시를 기준으로 연산할 경우, 데이터 불일치가 발생할 수 있음
  • ➡️ 캐시 일관성 프로토콜로 공유 공간인 메인 메모리와 동기화를 수행함
  • ✅ MSI, MOSI, MESI, Synapse, Firefly

 

비순차 실행 최적화

  • 프로세서가 명령어를 실행하는 순서가 입력 코드에 기술된 명령어 순서와 다를 수 있음
  • 프로세서의 컴퓨팅 능력을 끌어냄