책에 마지막 장에 와서 내용을 정리한다. 개인적으로 자바와 관련된 깊은 지식을 배운거 같다. 이해하지 못한 내용이 더 많았지만 컴파일러의 내부 구조, GC 와 관련된 세부 동작, 그리고 덤프와 같은 내용을 학습해서 JVM 기반의 서버 개발자로서 성장한 느낌이다.
자바 메모리 모델은 멀티스레드 환경에서 데이터가 어떻게 공유되고 동기화되는지에 대한 규칙을 정의하는 모델이다.
즉, 자바 메모리 모델은 쓰레드 간 변수 읽기/쓰기 동작을 관리하여 일관성과 동기화를 보장하는 역할을 한다.
책에서는 메인 메모리와 작업 메모리 관련해서 정의한 내용이 좀 복잡했지만 아래와 같이 요약한다.
"메인 메모리(Main Memory)와 작업 메모리(Working Memory) 개념이 있으며, 스레드는 메인 메모리에서 데이터를 읽고 작업 메모리에 저장하여 사용"
각 스레드는 서로의 작업 메모리에 있는 변수에 직접 접근할 수 없으며 반드시 메인 메모리를 거쳐 값을 전송해야 한다.
volatile 키워드
가장 가벼운 동기화 메커니즘이다. volatile 키워드를 사용하게 되면 두 가지의 특성을 가진다.
모든 스레드에서 이 변수를 투명하게 볼 수 있다. 이를 "가시성을 보장한다" 라고 말할 수 있다. 세부적인 내용으로는, 일반적인 변수는 값이 메인 메모리를 거쳐 전달한다. 스레드 A가 수정한 공유 변수의 값이 스레드 B에 보일려면 A가 수정된 값을 메인 메모리에 기록한 다음 B가 다시 메인 메모리에서 읽어야한다. 하지만 volatile 변수는 한 스레드가 값을 수정하면 다른 스레드들도 새로운 값을 즉시 알게된다.
명령어 재정렬 최적화를 막아준다. 이 부분은 어셈블리 레벨까지 확인했을 때 볼 수 있다. CPU 및 JIT 컴파일러가 최적화를 위해 실행 순서를 바꾸는 것을 방지해준다.
하지만 volatile 은 절대적인 동기화는 보장하지 않는다. 가시성에서는 즉시 볼 수 있도록 보장해주지만 Write - Write 하는 쓰레드가 있을 때 count++ 와 같은 작업을 원자적으로 수행하지 않고 순서가 보장되지 않기때문이다. 가시성 확보용 키워드는 volatile 외에도 synchronized 나 final 이 있다.
synchronized 블록을 사용하면, 진입할 때 다른 스레드의 변경 사항을 강제로 읽어오고(가시성), 블록을 나갈 때 모든 변경 사항을 메인 메모리에 반영(동기화)함.
final 변수는 객체가 생성될 때 한 번만 초기화되므로,객체가 다른 스레드에 공유되더라도 final 필드는 항상 최신 값을 보장함.
스레드 구현
스레드는 기본적으로 프로세서보다 가벼운 스케줄링 단위다. 프로세스 자원을 공유하며 고유한 스택을 가지고 있다. start() 메서드가 호출된 후 아직 종료되기 전인 모든 java.lang.Thread 클래스의 인스턴스가 하나의 스레드다.
Thread 클래스는 다른 자바 클래스 라이브러리 API 와 다르게 네이티브 메서드로 구현되어있는데 운영체제(OS)의 스레드 관리 기능을 직접 활용해야 하기 때문에 이렇게 작성되었다.
커널 스레드와 유저 스레드
커널 스레드는 운영체제 커널에서 직접 관리하는 스레드이며, 커널 스레드를 이용하는 구현을 1:1 구현이라고도 한다. 커널 스레드 각각은 커널의 복제본이라고 할 수 있기에 운영체제는 여러가지 일을 동시 처리 가능하다.
사용자 스레드는 온전히 사용자 공간에서 구현되는 스레드 라이브러리를 가리킨다. 하지만 커널 스레드는 사용자 스레드의 존재와 구현 방법을 알지 못한다. 그렇기 때문에 커널의 스레드 스케줄링 같은 지원을 받지 못한다.
자바 스레드 구현
JDK 1.2 이전 클래식 VM 에서는 자바 스레드를 그린 스레드 (green thread) 라는 이름의 사용자 스레드로 구현했다. 하지만 JDK 1.3 부터는 운영 체제의 기본 스레드 모델, 1:1 모델을 선택하였다.
JVM 자체는 스레드 스케줄링에 관여하지 않고 온전히 밑단의 운영 체제가 관리한다.
자바 스레드 스케줄링
스레드 스케줄링이란? 프로세서 사용 권한을 스레드에 할당하는 것이다. 주요 스케줄링 방법으로는 선점형 스케줄링, 그리고 비선점형 스케줄링이 있다.
비선점형 스케줄링 (cooperative scheduling)
선점형 스케줄링 (preemptive scheduling)
현재 자바는 선점형 스케줄링을 이용한다. 우선순위 스케줄링을 사용하기도 하지만 스레드의 우선순위는 정확하게 판단하기는 불가능하다.
오늘 날 웹 애플리케이션의 서비스 규모가 점점 올라가면서 커널 스레드와의 1:1 매핑 구조로는 감당하기 어려울 수 있다. 커널 스레드가 많아지면서 그만큼 전환과 스케줄링 비용이 커지기 때문에 스레드 생성에 제한이 있기 때문이다. 그리고 이런 한계를 극복하고자 가상 스레드가 등장했는데 이건 조금 더 자세한 설명을 아래에서 들을 수 있다.
여러 스레드가 이용할 수 있는 공통 기능을 제공하려면 코드가 스레드 안전해야 한다. 스레드 안정성을 확보하기 위한 대표적인 단계는 아래와 같다.
대표적인 동기화 방법 중에는 synchronized 와 같은 방법이 있지만 이것은 매우 주의해서 사용해야한다.
현제 자바는 플랫폼 스레드와 운영체제의 커널 스ㅔ드와 매핑한다. 따라서 플랫폼 스레드를 정지하거나 깨우려면 운영체제의 도움을 얻어야한다 (사용자모드 <-> 커널모드 사이의 전환이 필수) 이 모드 전환은 CPU 시간을 많이 사용한다.
간단한 코드에 적용한다면 오히려 전환 작업이 더 긴 시간을 소요 시킨다. 그렇기 때문에 제한적으로 사용하는 것이 중요하다.
ReentrantLock 은 클래스 라이브러리 수준에서 동기화를 구현하는 Lock 인터페이스의 구현체이다. ReentranctLock 은 스레드를 블로킹이 아니라 waiting 상태에 두기 때문에 상대적으로 synchronized 보다 안정적이다.