메모리 가시성
- 메모리 가시성 : 멀티스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 문제를 의미한다.
public class VolatileFlagMain {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread t = new Thread(task, "work");
t.start();
sleep(1000);
task.runFlag = false;
log("runFlag = " + task.runFlag);
}
static class MyTask implements Runnable {
boolean runFlag = true;
@Override
public void run() {
log("task 시작");
while (runFlag) {
//runFlag가 false로 변하면 탈출
}
log("task 종료");
}
}
}
- 위 코드는 runFlag가 false가 될 때까지 스레드가 무한 루프를 도는 코드이다.
- 예상대로라면 sleep(1000) 이후 runFlag가 false가 되면서 무한 루프가 종료되어야 한다.
하지만 main()에서 정상적으로 runFlag가 false로 변경되었음에도 불구하고 MyTask 스레드에서는 무한 루프를 돌게 되는 현상이 발생한다. 이런 문제가 메모리 가시성(memory visibility) 문제이다.
- 예상대로라면 sleep(1000) 이후 runFlag가 false가 되면서 무한 루프가 종료되어야 한다.
- 일반적으로는 메모리 접근 방식을 좌측 그림과 같이 생각하지만 실제 메모리 접근 방식은 우측 그림과 같다.
- 메인 메모리는 CPU 입장에서 보면 거리도 멀고 상대적으로 속도도 느리지만 가격이 저렴해서 큰 용량으로 쉽게 구성할 수 있다.
- 매우 빠른 CPU 연산 성능을 따라가려면 CPU 가까이에 빠른 메모리가 필요한데 이것이 캐시 메모리이다.
- 캐시 메모리는 CPU와 가깝고 속도도 빠른 메모리이며 가격이 상대적으로 비싸기에 큰 용량을 구성하기는 어렵다.
- 예시 코드에서 runFlag의 값을 각 스레드에서 사용하려면 CPU에서 효율적으로 값을 처리하기 위해서 캐시 메모리에 불러오며 이후 캐시 메모리에 있는 runFlag 값을 사용하게 된다.
- 이후 main()에서 runFlag 값을 false로 바꾸면 메인 메모리의 runFlag가 아닌 main 스레드 캐시 메모리의 runFlag 값만 false로 바뀌게 된다.
- 메인 메모리에 값의 변경이 즉시 반영되지 않는다.
- CPU 설계 방식, 종류마다 다르기에 캐시 메모리의 값이 메인 메모리에 반영되는 시점은 알 수 없다.
- 메인 메모리의 값이 변경되더라도 work 스레드 캐시 메모리에서 메인 메모리의 값을 불러와야하는데 그 시점도 알 수 없다.
- 주로 컨텍스트 스위칭이 일어날 때 캐시 메모리의 변경 내용이 메인 메모리로 전달된다.
- Thread.sleep()이나 System.out 등이 호출될 때 스레드가 잠시 쉬는데 이럴 때 컨텍스트 스위칭이 되면서 주로 갱신이 되지만 갱신이 보장되지는 않는다.
- 메모리 가시성 문제를 해결하기 위해서 여러 스레드에서 같은 시점에 정확히 같은 데이터를 보는 것이 중요하다.
- 해결하기 위해서 성능을 약간 포기하는 대신 값을 읽고 쓸 때, 메인 메모리에 직접 접근하도록 자바에서 제공하는 것이 volatile 키워드이다.
- volatile로 선언한 변수에 대해서는 값을 읽고 쓸 때 항상 메인 메모리에 직접 접근하도록 한다.
- 약 5배 정도의 성능 차이가 발생하기에 메인 메모리에 직접 접근해야할 때만 사용하는 것이 좋다.
자바 메모리 모델 (Java Memory Model)
- Java Memory Model(JMM) : 자바 프로그램이 어떻게 메모리에 접근하고 수정할 수 있는지 규정하며 여러 스레드들의 작업 순서를 보장하는 happens-before 관계에 대한 정의다.
- happens-before : 자바 메모리 모델에서 스레드 간의 작업 순서를 정의하는 개념이다.
- ex) A 작업이 B 작업보다 happens-before 관계에 있다면 A 작업에서의 모든 메모리 변경 사항은 B 작업에서 볼 수 있다. A 작업에서 변경된 내용은 B 작업이 시작되기 전에 모두 메모리에 반영한다.
- 관계는 한 동작이 다른 동작보다 먼저 발생함을 보장한다.
- 관계는 스레드 간의 메모리 가시성을 보장하는 규칙이다.
- 관계가 성립하면 한 스레드의 작업을 다른 스레드에서 볼 수 있게 된다.
- 한 스레드에서 수행한 작업을 다른 스레드가 참조할 때 최신 상태가 보장되는 것이다.
- volatile 키워드도 happens-before 중 하나이다.
- 규칙을 따르면 프로그래머가 멀티스레드 프로그램을 작성할 때 예상치 못한 동작을 피할 수 있다.
- happens-before 관계가 발생하는 경우
- 프로그램 순서 규칙 - 단일 스레드 내에서 프로그램 순서대로 작성된 모든 명령문은 happens-before 순서로 실행된다.
- volatile 변수 규칙 - 한 스레드에서 volatile 변수에 대한 쓰기 작업은 해당 변수를 읽는 모든 스레드에 보이도록 하며 쓰는 작업은 읽는 작업보다 happens-before 관계를 형성한다.
- 스레드 시작 규칙 - 한 스레드에서 Thread.start()를 호출하면 해당 스레드 내의 모든 작업은 start() 호출 이후에 실행된 작업보다 happens-before 관계가 성립한다.
- 스레드 종료 규칙 - Thread.join()을 호출하면 join 대상 스레드의 모든 작업은 join()이 반환된 후의 작업보다 happens-before 관계를 가진다.
- 인터럽트 규칙 - Thread.interrupt()를 호출하는 작업이 인터럽트된 스레드가 인터럽트를 감지하는 시점의 작업보다 happens-before 관계가 성립한다.
- 객체 생성 규칙 - 객체의 생성자는 객체가 완전히 생성된 후에만 다른 스레드에 의해 참조될 수 있도록 보장한다.
- 모니터 락 규칙 - 한 스레드에서 synchronized 블록을 종료 후 그 모니터 락을 얻는 모든 스레드는 해당 블록 내의 모든 작업을 볼 수 있다.
- 전이 규칙 - A가 B보다 happens-before 관계에 있고 B가 C보다 happens-before 관계에 있다면 A는 C보다 happens-before 관계에 있다.
- volatile / 스레드 동기화 기법(synchronized, ReentrantLock)을 사용하면 메모리 가시성 문제가 발생하지 않는다.
출처 : [인프런 김영한 실전 자바 - 고급편]
김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 강의 | 김영한 - 인프런
김영한 | 멀티스레드와 동시성을 기초부터 실무 레벨까지 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을 안다?
www.inflearn.com
'Java > [인프런 김영한 실전 자바 - 고급편]' 카테고리의 다른 글
[인프런 김영한 실전 자바 - 고급편] concurrent.Lock (0) | 2024.08.02 |
---|---|
[인프런 김영한 실전 자바 - 고급편] 동기화 - synchronized (0) | 2024.08.02 |
[인프런 김영한 실전 자바 - 고급편] 스레드 제어와 생명 주기 1, 2 (1) | 2024.08.02 |
[인프런 김영한 실전 자바 - 고급편] 스레드 생성과 실행 (1) | 2024.07.30 |
[인프런 김영한 실전 자바 - 고급편] 프로세스와 스레드 소개 (2) | 2024.07.30 |