티스토리 뷰

Java

volatile 키워드

jvnlee 2022. 3. 27. 04:26

캐시와 메인 메모리

volatile에 대한 설명에 앞서, CPU의 메모리 참조에 대해서 간단하게 짚고 넘어가자. CPU는 코어당 한번에 하나의 쓰레드를 수행시키는데, 이 때 쓰레드의 작업에서 필요로 하는 데이터는 근본적으로는 메인 메모리인 RAM으로부터 온다. 그러나 CPU의 연산 속도에 비해 RAM은 한참 느리기 때문에 반복 사용되는 데이터를 CPU가 보다 빠르게 얻기 위해 캐시 메모리를 활용한다.

 

 

도식을 보면 CPU와 RAM 사이에 캐시 메모리가 자리하고 있는 것을 확인할 수 있다. CPU는 우선적으로 캐시에 사용하고자 하는 데이터가 있는지 확인하는데, L1, L2, L3 캐시 순서대로 확인해보고 캐시에서 데이터를 얻지 못하면 RAM에서 얻어온다. 이 글은 캐시에 대해 깊게 다루는 글은 아니므로 자세한 설명은 배제했다.

 

캐시를 사용하면 분명히 속도의 이점을 얻을 수 있다. 그런데 멀티 쓰레드 환경에서 모든 쓰레드가 공통적으로 참조하는 데이터를 각 코어와 연결된 캐시에 저장해두고 사용한다면, 개별 쓰레드에서 데이터를 변경시키는 과정에서 각 캐시에 저장된 데이터가 서로 달라지면서 동기화가 깨지게 된다. 이와 같이 캐시 메모리를 사용할 때 문제가 생길 소지가 있다면 volatile 키워드를 사용해볼 수 있다.

캐시 메모리 간의 동기화를 다른 말로 캐시 일관성(Cache Coherence)이라고 표현한다.

 

 

volatile 키워드의 사용

volatile은 변수 선언 시 앞에 붙여 다음과 같이 사용한다.

volatile boolean isOn = false;

사용 시 효과는 2가지 정도가 있다.

 

1. 해당 데이터에 접근하는 쓰레드가 캐시가 아닌 메인 메모리로부터 데이터를 참조하도록 강제한다.

멀티 코어, 멀티 쓰레드 환경에서는 RAM에 있던 데이터를 읽어간 후에 각 코어 마다의 캐시에 저장해두고 캐시를 참조하게 된다. 그래서 위에서 언급했던 대로 데이터 접근 속도는 빨라지더라도 동기화에 문제가 생길 수 있다. volatile을 사용하면 무조건 이 데이터는 메인 메모리에 저장해놓고 메인 메모리로부터 읽거나 쓰겠다고 선언하는 것과 같기 때문에 언급한 문제 상황을 방지할 수 있다.

 

2. 해당 데이터에 대한 읽기/쓰기 작업의 원자성을 보장한다.

JVM의 Runtime Data Area 중에서 개별 쓰레드마다 할당되는 영역인 Stack Area는 각 Stack Frame에 Operand Stack 이라는 피연산자 스택을 가지고 있다. 이 피연산자 스택은 4 byte 단위로 적재되기 때문에 byte, short, int 같은 4 byte 이하의 타입은 원자성을 보장 받아 데이터를 읽거나 쓸 때 처리 과정이 끊기지 않는다. 그러나 long이나 double과 같은 8 byte 크기의 타입은 4 byte를 초과하기 때문에 작업 도중에 다른 쓰레드에 의해 작업이 끊어질 수 있다. 이 경우 변수 선언 시 volatile을 사용하면, long이나 double 처럼 크기가 큰 데이터도 원자성을 보장 받아 작업이 도중에 끊어지지 않도록 할 수 있다.

 

 

synchronized와 volatile

volatile과 항상 같이 언급되는 것이 동기화 구현을 위한 키워드인 synchronized 이다. 이 둘에 대한 이야기 이전에 락(lock)과 동기화에 관한 2가지 성질에 대해 짚고갈 필요가 있다.

 

1. 상호 배제 (Mutual Exclusion, Mut-Ex)

오직 하나의 쓰레드만이 락을 얻어 임계 영역 내의 작업을 수행할 수 있다는 의미

 

2. 가시성 (Visibility)

공유 데이터에 대해 하나의 쓰레드에서 발생시킨 변경이 나머지 쓰레드에게 모두 보일 수 있게 공개된다는 의미

 

Java의 synchronized 키워드는 이 2가지 성질을 모두 보장한다. 따라서 여러 쓰레드가 동시에 어떤 임계 영역 내에 진입하려고 해도 하나만이 들어갈 수 있고, 변경이 이루어졌다면 그것이 메인 메모리에 저장된 원본에 반영되어 나머지 쓰레드가 캐시 업데이트 등을 통해 새로운 값을 참조할 수 있게 된다.

 

그런데 동기화가 항상 좋은 것만은 아니다. 상호 배제는 성능에 대한 분명한 비용 지불이 수반되기 때문이다. 따라서 다수의 쓰레드가 동시에 특정 데이터에 접근해도 상관없지만, 변경에 대한 가시성만 필요한 경우에는 volatile을 사용하면 된다. 하나의 쓰레드만 쓰기 작업을 하고 나머지는 읽기 작업만 하는 환경이라면 volatile을 써도 동기화 효과를 얻을 수 있기 때문이다.

 

그러나 여러 쓰레드가 동시에 쓰기 작업을 시도할 수 있는 환경에서는 volatile 만으로는 동기화 효과를 얻을 수 없다. volatile을 붙였다고 해서 상호 배제가 이루어지는 것은 아니니, 만약 여러 쓰레드의 작업 시도에 대해 상호 배제성이 필요하다면 synchronized 키워드를 활용해야 한다. 그래서 보통은 volatile과 synchronized를 함께 사용하기도 한다. 이렇게 하면 지정한 데이터를 메인 메모리로부터만 읽거나 쓰게할 수 있고(volatile), 읽기/쓰기 작업은 한번에 한 쓰레드만 하도록 할 수 있다.(synchronized)

 

 

Happens-Before Ordering

volatile로 선언된 변수의 재밌는 점은, 자신의 가시성 뿐만 아니라 자신 주변의 일반 변수들의 가시성에도 영향을 미친다는 것이다. volatile 변수의 값에 쓰기 작업을 하면, 변경 사항은 메인 메모리에 즉각 반영이 되는데, 이 때 자신의 변경 사항을 반영하는 김에 자신에 대한 쓰기 작업 이전에 발생한 모든 쓰기(volatile이 붙지 않은 변수일지라도)들도 모두 함께 메인 메모리에 반영시킨다.

 

반대로 volatile 변수의 값에 읽기 작업을 하면, 마지막으로 읽었던 시점 이후에 바뀐 모든 데이터를 함께 메인 메모리로부터 읽어온다. 따라서 volatile 변수 읽기 작업을 하면 해당 작업 이후의 읽기 작업들에는 메인 메모리에서 전달된 최신 변경사항이 반영되어 변경이 이루어진 값들을 읽을 수 있게 된다.

 

이렇게 volatile 변수가 주는 visibility 혜택을 Volatile Visibility Gurantee라고 부른다.

 

단, Volatile Visibility Gurantee는 코드의 실행 순서에 강하게 의존하기 때문에, 코드의 실행 순서가 바뀌어 버리면 효과를 누릴 수 없게 된다. 문제는 JIT 컴파일러가 컴파일 과정에서 코드 실행의 최적화를 위해 Instruction Re-ordering을 진행해 우리가 실제로 작성한 소스코드와 실제 코드의 실행 순서가 달라질 수 있다는 것이다. Java는 Instruction Re-ordering으로 Volatile Visibility Gurantee를 해치는 것을 막기 위해 Happens-Before Ordering이라는 대책을 내놓았다. 이에 따르면 일반적인 쓰기 작업들은 volatile 변수의 쓰기 작업 뒤쪽으로 re-order 될 수 없고, 읽기 작업들은 volatile 변수의 읽기 작업 앞쪽으로 re-order 될 수 없다.

Writing on a volatile variable cannot "happen before" writing on any other variables, and reading on a volatile variable must "happen before" reading on any other variables.

 

Happens-Before Ordering이 적용됨에 따라 Volatile Visibility Gurantee를 걱정 없이 누릴 수 있게 되었다. 따라서 여러 개의 변수가 있을 때, volatile 키워드를 전부 다 붙이지 않더라도 volatile 변수를 쓰거나 읽는 코드 위치에 따라 마치 volatile을 모든 변수에 다 붙인 것 같은 효과를 볼 수 있다. 이렇게 일반 변수 입장에서 volatile 변수의 덕택으로 동일한 visibility gurantee 효과를 얻는 것을 Piggybacking(의역하면 무임승차 정도..?)이라고 부른다.

 

 

정리

  volatile synchronized
대상 필드(변수) 메서드, 사용자 지정 블럭
상호 배제성 없음 (non-blocking) 있음 (blocking)
가시성 있음 있음
데이터 출처 메인 메모리 캐시 메모리, 메인 메모리

 

참고 자료

volatile Keyword in Java

https://www.geeksforgeeks.org/volatile-keyword-in-java/

 

Guide to the Volatile Keyword in Java

https://www.baeldung.com/java-volatile

 

Java Happens Before Guarantee - Java Memory Model - Part 2

https://www.youtube.com/watch?v=oY14UyP61F8 

이 영상은 글의 마지막에 등장한 Instruction Re-ordering과 Happens-Before Ordering에 관한 것인데, 설명이 정말 잘 되어있다.

 

'Java' 카테고리의 다른 글

Garbage Collector(GC)  (0) 2022.03.13
Java Virtual Machine(JVM)  (0) 2022.03.11
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday