public class VolatilleFlagMain {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread t = new Thread(task, "work");
log("runFlag = " + task.runFlag);
t.start();
sleep(1000);
log("runFlag를 false로 변경 시도");
task.runFlag = false;
log("runFlag = " + task.runFlag);
log("main 종료");
}
static class MyTask implements Runnable {
boolean runFlag = true;
// volatile boolean runFlag = true; // 캐시 메모리 사용하지 않음.
@Override
public void run() {
log("task 시작");
while (runFlag) {
// runFlag가 false로 변하면 탈출
}
log("task 종료");
}
}
}
위의 코드는 두 쓰레드의 실행 과정으로 이해할 수 있다. 간단하게 설명하자면, work스레드가 runFlag라는 불리언 변수에 의존한 while문을 돌리고 있을 때 main스레드가 어느 시점에 runFlag를 false로 바꾼다면 우리가 아는 상식으로는 work스레드의 while 로직은 종료되어야 한다.
16:36:02.180 [ main] runFlag = true
16:36:02.183 [ work] task 시작
16:36:03.187 [ main] runFlag를 false로 변경 시도
16:36:03.188 [ main] runFlag = false
16:36:03.188 [ main] main 종료
하지만 결과는 그렇지 않다. main스레드가 runFlag를 false로 만들었지만 work스레드의 종료는 일어나지 않는다.

runFlag는 힙 영역에서 생성된 task 인스턴스에 존재할 것이다. main스레드는 분명 이에 접근하여 runFlag를 바꾸었고 work스레드는 이 runFlag에 의존한 while을 돌리고 있었기에 영향을 받아야하지만 그렇지 않은 이유는 캐시메모리의 존재 때문이다.
메인 메모리는 하드디스크보단 빠르겠지만 실제로 CPU입장에서는 캐시 메모리의 존재보다 거리도 멀고 속도도 상대적으로 느리다. CPU의 빠른 연산을 가능하게 하기 위한 훌륭한 보조 수단은 캐시 메모리이다.
캐시 메모리는 Runnable상태의 CPU가 가장 우선적으로 접근하는 저장장치가 될 것이다. 굳이 티어로 나누자면 캐시메모리-메모리-ssd,하드와 같은 순서일 것이다.
캐시 메모리는 높은 성능과 CPU와 가까이 존재하기 위해 대신 용량이 작다.
각 스레드는 CPU의 자원을 받아 자신을 실행한다. runFlag의 값을 사용하기 위해 runFlag를 캐시 메모리에 불러온다. 그리고 캐시 메모리에 있는 runFlag를 사용하는 것이다.
main 스레드가 실행되기 이전 이미 캐시메모리에는 runFlag값이 초기값 true로 안착되어있고 이후에 main스레드가 runFlag=false라는 명령을 내리면 캐시 메모리의 runFlag를 변경하게 된다.
메인 메모리의 runFlag의 값은 즉시 반영되지 않으며 이 시점은 CPU마다, 운영체제마다 다를 수 있지만 대개 컨텍스트 스위칭 시점에 재정립된다.
이처럼 멀티스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 문제를 메모리 가시성 (memory visibility)이라 한다. 이름 그대로 메모리에 변경한 값이 보이는가, 보이지 않는가의 문제이다.
이를 해결하기 위해서는 캐시메모리에 runFlag를 적재하지 않고 두 스레드 모두 runFlag를 메인 메모리로부터 사용하면 해결할 수 있다.
자바의 volatile은 이를 반영해주는 선언이다. 변수 앞에 volatile을 작성한다면 이는 캐시 메모리에 적재하지 않고 메모리에 존재하며 필요한 스레드들은 이를 가져다 쓴다.
happens-before는 스레드 간의 작업 순서를 정의하는 개념적인 용어이다. 만약 A 작업이 B 작업보다 happens-before 관계에 있다면 A 작업에서의 모든 메모리 변경사항은 B 작업에서 볼 수 있다.
즉 한 동작이 다른 동작보다 먼저 발생함을 보장하는 관계이며 스레드 간의 메모리 가시성을 보장하는 규칙이다.
단일 스레드에서
int a = 1;
int b = 2;
라는 코딩은 항상 a = 1이 b = 2보다 먼저 실행된다. 만약 a = 1이 메인 스레드에서 일어나며 b = 2가 work스레드에서 일어난다고 한다면 무엇이 더 빨리 실행될 지는 알 수 없다.
volatile 변수에 대한 쓰기 작업은 항상 해당 변수를 읽는 모든 스레드에 보여야 한다.
이 개념이 동기화 개념을 이해하기 전에는 굉장히 와닿지 않는 것 같다. volatile 변수에 변경이 시작되었다면 만약 다른 쓰레드에서 이를 아주 빠르게 조회하고있었다면 이는 멈추고 변수가 변경되자마자 다시 이를 조회한다는 뜻이다.
우리가 이미 배운 thread.join또한 이전과 이후의 작업에서 happens-before관계를 가지며 인터럽트또한 그러하다.