Java/JVM

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

noahkim_ 2024. 12. 26. 00:08

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

 

1. 자바 메모리 모델

  • 자바 코드와 실제 메모리/CPU 사이의 추상화 규칙 집합
  • ✅ OS/하드웨어에 상관없이 동일하게 동작하기 위한 명세
  • ⚠️ 각 CPU는 캐시 구조가 다르고 메모리 일관성 모델이 다름
  • ⚠️ 같은 자바 코드이더라도 CPU/OS에 따라 다르게 동작할 수 있음
  • ➡️ 플랫폼에 상관없이 메모리를 일관된 방식으로 동작하도록 함 (가시성, 원자성, 순서 보장)

 

2. 메모리 상호작용

메모리 구조

항목 메인 메모리
작업 메모리
정의 모든 공유 변수가 저장되는 공간
각 스레드가 가지는 고유한 메모리 공간
접근 가능성 모든 스레드가 접근 가능
해당 스레드만 접근 가능
역할 프로그램 전역 상태 저장
메인 메모리의 복사본으로 작업 수행
데이터 이동 read/write 명령어로 작업 메모리와 상호작용
load/store 명령어로 메인 메모리와 동기화
특성 데이터 일관성을 위한 기준점
계산은 여기서 수행되고, 결과는 메인 메모리에 반영됨

 

프로토콜

  • 모든 공유 변수는 메인 메모리에 존재
  • 각 스레드는 자기만의 작업 메모리를 가짐
  • 직접 메인 메모리를 쓰지 않고 복사본으로 작업함
  • ➡️ 메인 메모리와 작업 메모리 간 상호작용을 위해 프로토콜이 정의됨

 

동작 순서) 프로토콜

더보기
  1. read: 메인 메모리에서 값 읽기
  2. load: 읽은 값을 작업 메모리에 올림
  3. use: 연산 수행
  4. assign: 작업 메모리에 값 변경
  5. store: 메인 메모리에 쓸 준비
  6. write: 메인 메모리에 실제 반영

 

Volatile

  • JVM이 제공하는 가장 가벼운 동기화 매커니즘
  • ✅ 가시성과 순서 보장을 제공함 (락 사용 안함)
  • ❌ 원자성 보장 안됨 (자바 명령어의 원자성 보장 안됨)

 

코드) 가시성

더보기
static boolean flag = false;

public static void main(String[] args) {
    new Thread(() -> {
        while (!flag) {
            // 무한 루프 가능
        }
        System.out.println("끝");
    }).start();

    flag = true;
}
  • 다른 스레드에 의해 flag가 true로 변경됨
  • ✅ 내 스레드는 자기 CPU 캐시값만 볼 수 있음
  • ⚠️ 무한 루프 가능
  • ➡️ 쓰기 시 다른 코어 캐시 무효화하고 읽기 시 최신 값을 읽도록 함

 

코드) 순서 보장

더보기
private static Singleton instance;

static Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}
  • CPU/JIT는 성능을 위해 명령어 순서를 변경할 수 있음
  • ✅ 원래 순서: 메모리 할당 → 생성자 호출 → instance 참조 저장
  •  정렬된 순서: 메모리 할당 → instance 참조 저장 생성자 호출
  • ⚠️ 다른 스레드가 초기화 안된 객체를 볼 수 있음
  • ➡️ 해당 변수 앞 뒤에 메모리 배리어를 삽입하여 재정렬 방지함

 

선행 발생 규칙

  • 스레드간의 동작 순서와 메모리 가시성을 정의하는 원칙
  • A가 B보다 먼저 발생하면, A의 변경은 B에게 반드시 보임
규칙 설명
프로그램 순서 규칙
하나의 스레드 안에서는 코드가 작성된 순서대로 실행됨
모니터 락 규칙
락 해제 전에 쓴 값은 다음 락 획득 스레드에게 반드시 보임
volatile 변수 규칙
volatile write 이전의 모든 작업은 read 이후에 반드시 보임
스레드 시작 규칙
Thread.start() 이전의 작업은 새 스레드 내부에서 항상 보임
스레드 종료 규칙
Thread.join() 이후 작업은 내부 모든 작업 이후에 실행됨