volatile

Tags:

이 이슈도 이젠 그만.
말투가 절대 나같지 않게 닭살스러운건 j2eestudy.co.kr 여기 올리다보니
어쩔 수 없음..

JDK 1.4 또는 그 이전의 버젼까지 제대로 지원이 안되던 키워드중의 하나가 volatile입니다. 이 키워드가 JDK 1.5에서는 메모리 모델을 좀더 형식화하여 재정의하면서, 그 의미가 새로이 주목받게 되었습니다.

volatile 키워드가 자바에서 갖는 의미를 잘 이해하려면 몇가지 용어에 대해서 알아야합니다.

첫번째는 가시성(visibility) 입니다. 가시성이란, 어떤 변수의 값을 내가 볼 때 그 변수의 어떤 값을 보는가 하는 문제입니다. 이 문제가 발생되는 이유는 CPU에게는 레지스터라는 멋진 캐시가 있기 때문입니다.

예를들어 두개의 CPU가 있는 머신에서 쓰레드 두개가 각각의 CPU에서 돌아갈 때 다음과 같은 코드를 생각해보죠.

public class MyClass {

public static int a =3;
}

이 때 첫번째 쓰레드가

MyClass.a=4;

라고 했다고 하고

두번째 쓰레드가 연이어 System.out.println(MyClass.a)를 했다고 합시다.

그렇다면 두번째 쓰레드에서 반드시 4를 출력한다는 것은 보장되지 않습니다. 그 이유는 첫번째 쓰레드가 갱신한값이 반드시 메모리에 저장되지 않을 가능성(즉 레지스터에만 업데이트 한다)이 있기 때문이며, 또한 두번째 쓰레드가 메모리의 값을 읽어오지 않을 가능성(즉 레지스터에서 읽어왔다)이 있기 때문입니다.

두번째는 재배치(reordering)의 문제입니다. 자세하게 설명을 하려면 복잡하니까, 간단하게 그 의미만 말씀드리겠습니다. 재배치는 일단 (1) CPU, (2) MEMORY내의 MMU, (3) 컴파일러에 의해서 일어나는 동작이며 성능을 극대화 하기 위해서 발생합니다.

이들 세가지 개체는 W(쓰기), R(읽기)의 동작이 다음과 같이 있다고 할 때

WWRWWRWR

이를 다음과 같이 수정할 수 있습니다.

WWWWWRRR

이는 우리가 통상적으로 이야기하는 지역성(locality. 시간/공간적으로 인접한 개체가 같이 접근될 가능성이 높다. 따라서 그들을 같이 접근하면 성능이 향상된다)을 극대화 하기 위한 최적화 기법입니다. 기본적으로 동기화 되지 않은(synchronized나 volatile을 사용하지않은)변수는 항상 재배치의 대상이됩니다.

세번째는 원자성의 개념입니다.
대부분의 변수는 자바에서 32비트 안으로 처리되지만, 64비트인 long, double, 그리고 객체에 대한 참조 변수는 두번 이상의 메모리 접근으로 이루어 질 수 있습니다. 예를들어,

double a = 12;
a += 1;

이라는 명령은 상위 32비트의 수정 + 하위 32비트의 수정이라는 두개의 명령으로 이루어집니다. 따라서 멀티 CPU, 멀티 쓰레드 환경에서는 반만 변경된 결과를 다른 쓰레드에서 볼 수 있다는 문제가 있습니다.

더 이상의 자세한내용(예를들어, 재배치로 인해 완전히 초기화 되지 않은 객체가 싱글톤 디자인 패턴에서 반환되는 가능성)은 자바스터디에 강좌를 올려뒀었기 때문에 나중에 링크를 하도록 하고, JDK 1.5에서의 volatile의 의미를 살펴보겠습니다.

이를 위해 먼저 C/C++언어에서의 volatile의 의미를 이야기하자면, C/C++ 에서의 volatile은 단일 CPU내에서만 reordering문제를 해결합니다. 다시 말해, 컴파일러 수준에서의 reordering을 막을 뿐이며 MMU에 의한 reordering을 막지 않습니다. 따라서 멀티 CPU, 멀티 쓰레드 환경에서의 volatile은 그야말로 ‘무용지물’이며 심지어는
C/C++에서의 volatile이란 죽은 거나 마찬가지이다(braindead)라고 말하기도합니다.

반면 JDK 1.5에서의 volatile은 컴파일러/CPU/MMU상에서의 재배치를 막으므로, 앞서 말씀드린 원자성/가시성/재배치의 문제를 모두 해결합니다.

그러나, volatile이 멀티 쓰레드 환경에서의 동기화된 변수선언을 위한 완전한 구성요소는 아닙니다.

예를들어,

volatile int a=3;
System.out.println(a);
a=4;

와 같은 동작은 멀티쓰레드/멀티 CPU상에서 thread-safe합니다.

하지만,

volatile int a=3;
System.out.println(a);
a+=4;

는 멀티쓰레드/멀티 CPU상에서 thread-safe하지 않습니다.

그 이유는 a+=4라는 명령은 다음과 같이 분할되기 때문입니다.

1) a를 읽어 지역변수 temp에 저장 // thread-safe함
2) temp+=4 // 이 시점에서 다른 쓰레드가 끼여들 수 있음
3) a에 temp를 저장 // thread-safe함

이 순서에서 2번단계에서 다른 쓰레드가 끼어들어, 이쪽에서는 분명히 4를 더했다고 생각했는데 다른쪽에서는 4가 더해지지 않은 값을 볼 수 있습니다. 다시 말해, volatile 변수를 읽거나(read), 쓰는(write) 동작은 멀티쓰레드/멀티 CPU상에서 안전합니다.

하지만, volatile 변수를 읽어서 수정한 뒤 쓰는(read-modify-write) 동작은 멀티쓰레드/멀티 CPU상에서 안전하지 않습니다. 그리고 이 정도의 안정성을 보장하는 것이 자신들로서는 최선이라고 Doug Lea가 이야기한 바 있고요..

안타까운점은, volatile은 거의 synchronized 와 같은 수준의 성능 저하를 보이며 (JSR 133 FAQ참조), 따라서 성능 향상을 위한 잇점이 그다지 크지는 않아 보인단 것입니다.

요약하자면, C/C++의 volatile은 단일 CPU에서만 통하는 멀티쓰레드 안전한 변수 선언기법이며, JAVA에서의 volatile은 멀티 CPU에서 read/write에 안전한 멀티쓰레드 변수 선언기법이고, read-modify-write 용도로는 부적합하다(이런 목적으로는 java.util.concurrent가 준비중이니 기대하자구요)는 것입니다.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *