메모리 가시성
메모리 가시성
메모리 가시성(memory visibility)이란?
- 멀티 스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 것
스레드간 변수 변경 시 적용 안되는 상황
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class VolatileFlagMain {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
log("runFlag = " + task.runFlag);
thread.start();
sleep(1000);
log("runFlag를 false로 변경 시도");
task.runFlag = false;
log("runFlag = " + task.runFlag);
log("main 종료");
}
static class MyTask implements Runnable {
boolean runFlag = true;
@Override
public void run() {
log("task 시작");
while (runFlag) {
// runFlag가 false로 변하면 탈출
}
log("task 종료");
}
}
}
위 코드를 실행해보면 task.runFlag 값이 false로 출력 됨에도 MyTask는 계속 실행되고 있어 프로그램이 종료하지 않습니다.
그 이유는 스레드 별로 변수를 캐시 메모리에 저장하기 때문입니다.
메모리 접근 방식
일반적으로 생각하는 메모리 접근 방식
실제 메모리 접근 방식
💡 캐시 메모리 <-> 메인 메모리
서로 값을 반영하는 타이밍은 일반적으로 컨텍스트 스위칭이 일어날 때이지만 이것도 보장되는 것은 아닙니다.
메인 메모리에 바로 접근 - volatile
변수 앞에 volatile 키워드를 쓰면 메인 메모리에 바로 접근 하는 변수가 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class VolatileFlagMain {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
log("runFlag = " + task.runFlag);
thread.start();
sleep(1000);
log("runFlag를 false로 변경 시도");
task.runFlag = false;
log("runFlag = " + task.runFlag);
log("main 종료");
}
static class MyTask implements Runnable {
// volatile 추가
volatile boolean runFlag = true;
@Override
public void run() {
log("task 시작");
while (runFlag) {
// runFlag가 false로 변하면 탈출
}
log("task 종료");
}
}
}
이제 두 스레드 모두 메인 메모리에 있는 runFlag 값을 참조하기 때문에 정상적으로 로직이 실행되는 것을 볼 수 있습니다.
volatile는 메인 메모리에 바로 접근 하기 때문에 성능 저하가 있습니다.
자바 메모리 모델(Java Memory Model)
- 자바 프로그램이 어떻게 메모리에 접근하고 수정할 수 있는지를 규정
happens-before
- JMM에서 스레드 간의 작업 순서를 정의하는 개념
규칙
- 한 동작이 다른 동작보다 먼저 발생함을 보장
- 스레드 간의 메모리 가시성을 보장하는 규칙
- 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 관계가 성립
- 한 스레드에서
- 객체 생성 규칙
- 객체의 생성자에서 초기화된 필드는 생성자가 완료된 후 다른 스레드에서 참조될 때 happens-before 관계가 성립
- 모니터 락 규칙
- 한 스레드에서
synchronized블록을 종료한 후, 그 모니터락을 얻는 모든 스레드는 해당 블록 내의 모든 작업을 볼 수 있음 ReentrantLock과 같이 락을 사용하는 경우에도 happens-before 관계가 성립
- 한 스레드에서
- 전이 규칙
- A가 B보다 happens-before 관계에 있고, B가 A보다 happens-before 관계에 있다면, A는 C보다 happens-before 관계에 있음
- 즉,
(A > B, B > C = A > C)
💡 한줄 요약
volatile또는스레드 동기화 기법(synchronized, ReentrantLock)을 사용하면 메모리 가시성 문제가 발생하지 않는다.
참고
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

