volatile 을 흔히, 캐쉬를 사용하지 않고 메모리에 직접 접근하는 키워드라고 설명한다.

이는 코드의 해석을 쉽고 직관적이게 하기 위해서 훌륭한 설명이다.

하지만, 결과는 대부분 맞을지언정 틀린 설명이다.


캐쉬, 동기화 와 관련해서 하드웨어의 밑바닥까지 들여다보면 그 인생 고달파진다.

하지만 혹시나 그러고 싶은 사람들을 위해 준비했다.



[[[Volatile의 진짜 의미]]]



volatile에 대해 논하기 전에, cache coherence 라는것을 들어보았는가?

서로 다른 코어 간 캐쉬 불일치 문제는 하드웨어선에서 "언제나" 보장된다.

즉, volatile 키워드가 없어도 서로 다른 두개의 L1 캐쉬의 내용은 항상 동일하다.

같은 메모리에 A가 읽고, B가 읽고, A가 쓰고, B가 두번째 읽으면

A가 쓸때 B의 캐쉬가 인밸리데이트되며, B가 두번째 읽을때 메인메모리가 아닌 A의 캐쉬로 가서 읽는다.

즉, 언제나 B는 A가 쓴 최신의 값을 읽는다.

이게 하드웨어에 자동으로 구현되어있다.



자, 이제 volatile이 뭔가 이상하다는걸 알 수있다.

volatile은 실제로 유의미한 동기화작업을 한다.

그렇다면,

1. cache coherence에도 불구하고 발생하는 동기화 이슈는 무엇인가.

2. volatile이 어떻게 그것을 방지하는가?

두가지 측면으로 생각을 진행해보자.




가장 윗단의 캐쉬가 무엇이라고 생각하는가?
L1캐쉬?
데이터는 L1캐쉬로부터 가져오고
L1캐쉬보다 더 가까운 캐쉬는 없는가?


사실 모든 문제는 여기에서 출발한다. L1캐쉬가 가장 가까운 공간이 아니다.

캐쉬 코히어런스는 프라이빗캐쉬인 L1캐쉬들의 동기화를 진행하지만,
우리는 L1캐쉬에만 캐쉬를 의존하지 않는다.

우리는 메모리로부터 읽어온 값을, 레지스터에 저장하거나, 다른 공간(스택, 혹은 쓰레드, 프로세스 프라이빗메모리)에 저장한다.

이 또한 캐쉬다.

값이 언제 변할지 모르는 공유메모리에서, 값을 안전하고 논리적으로 더 가까운 공간으로 옮긴다는 말이다.

공유메모리의 L1캐쉬보다, 사적 메모리의 L1캐쉬가 더 가깝다.
공유메모리의 L1캐쉬보다, 레지스터가 더 가깝다.


C와 자바 둘을 예시로 들어보겠다.


C는 포인터로부터 읽은 값을 레지스터에 옮겨담고, 레지스터에서 연산을 진행하곤 한다.
캐쉬코히어런스는 레지스터까지 동기화하지 않는다.
읽은 값을 레지스터에 옮겨담은 시점에서, 레지스터의 값을 변경하는것은 다른 쓰레드에 반영되지 않는다.
우린 여전히 포인터의 주소에 값을 쓰지 않았다.
복사해온 레지스터만 변경했을 뿐이다.
아직, 공유데이터는 변하지 않았다.


하지만 논리적으로, 레지스터 값의 변경은 공유데이터의 값의 변경과 동일하게 처리되어야한다.

이에 대한 해결책은,
C가 연산할동안, 해당 포인터로부터 읽은 값을 복사해서 다른 공간에 저장하지 않고,
매번 포인터로부터 값을 받아와서 연산하는 것이다.
매번 포인터로부터 값을 쓰는것이다.

즉, volatile 은 메인메모리 상시접근이 아니다.

"L1캐쉬 상시 접근"이다.

그리고 매번 포인터로부터 값을 읽고 써서 L1캐쉬에 접근한다면,
앞서 이야기한 캐쉬 코히어런스에 의하여 동기화가 완벽하게 진행된다.


자바의 경우는 어떤가요?
자바는 레지스터에다가 변수를 저장하지 않는다.

그럼에도 자바도 문제가 터지는 연산을 한다.

자바의 메소드가 클래스의 필드를 참조한다고 해보자.

필드는 메모리위의 공간이 되며, 이 필드를 여러 쓰레드가 공유하고,
따라서 여러개의 L1캐쉬에 캐쉬되어있으며, 그 L1캐쉬들은 cache coherence에 의해 동기화가 진행된다.


JVM은 내부적으로 JVM스택이라는 공간을 쓴다.

메소드가 필드로부터 값을 읽거나 쓸때,
jvm스택 맨 위에 읽을 오브젝트를 갖다놓고
getfield {fieldname} {type} 인스트럭선을 진행해서 jvm스택 맨 위에다가 읽은 값을 저장한다.

이렇게 한번 값을 읽고나면,

이 값을 스택에서 이동해대고 지역변수슬롯에 저장해가면서 연산을 진행한다.


마찬가지로 값의 복사가 일어났다.

하드웨어는 복사된 값이 jvm 스택과 슬롯을 이동해댈동안, 그 값의 변동을 추적하지 않는다.

즉, jvm이 값을 복사해놓고 연산할 동안에는 데이터불일치가 발생한다.


마찬가지로, volatile을 써서
매번 getfield
putfield를 진행하도록 만들면 된다.


마찬가지로 매번 main memory 접근,
이 아닌
L1캐쉬 접근이다.

물론 소프트웨어적으로 main memory접근하는게
하드웨어적으로 L1캐쉬접근이므로 메인메모리접근도 틀린 소리는 아니다.


"다만, L1캐쉬를 안거치고 메인메모리 직접접근이 아니란거다"