Volatile
- 'Java 변수를 main memory에 저장하겠다' 라는 것을 명시하는 것
- 매번 변수의 값을 read할 때 마다 cpu cache에 저장된 값이 아닌 메인 메모리에서 읽는 것
- 변수의 값을 write할 때 마다 main memory에 까지 작성하는 것
volatile 변수를 사용하고 있지 않는 멀티스레드 어플리케이션에서는 task를 수행하는 동안 성능 향상을 위해 메인 메모리에서 읽은 변수 값을 cpu cache에 저장하게 된다.
Example
sharedObject를 공유하는 두 개의 스레드가 있다.
- 스레드 1은 카운터 값을 더하고 읽는 연산을 한다. (read & write)
- 스레드 2는 카운터 값을 읽기만 한다. (only read)
public class SharedObject {
public int counter = 0;
}
스레드 1은 카운터 값을 증가시키고 있지만, cpu cache에 반영되어 있고 실제로 메인 메모리에는 반영이 되지 않는다. 그렇기 때문에 스레드2는 카운터 값을 계속 읽어오지만 0을 가져오게 된다.
그러면 어떻게 해결할 수 있을까?
Volidate 키워드를 추가하게 되면 main memory에 저장하고 읽어오기 때문에 변수 값 불일치 문제를 해결할 수 있다.
public class SharedObject {
public volatile int counter = 0;
}
언제 volatile이 적합할까?
멀티 스레드 환경에서 하나의 스레드만 read & write하고 나머지 스레드가 read하는 상황에서 가장 최신의 값을 보장한다.
항상 volatile이 옳을까?
스레드-1이 값을 읽어 1을 추가하는 연산을 진행한다. (아직 메인 메모리 반영 전)
스레드-2이 값을 읽어 1을 추가하는 연산을 진행한다. (아직 메인 메모리 반영 전)
두개의 스레드가 1을 추가하는 연산을 하여 최종결과가 2여야하지만 각각 결과를 메인 메모리에 반영하게 된다면 1만 남은 상황이 발생한다.
하나의 스레드가 아닌 여러 스레드가 write하는 상황에선 적합하지 않다. 여러 스레드가 write하는 상황이라면 synchronized를 통해 변수 read & write의 원자성을 보장해야 한다.
volatile 성능에 어떤 영향이 있나요?
volatile는 변수의 read와 write를 메인 메모리에서 진행하게 된다. cpu cache보다 메인 메모리가 비용이 더 크기 때문에 변수 값 일치를 보장해야 하는 경우에만 사용하는 것이 좋다.
실습
public class Main {
private static volatile boolean flag = false;
private static int counter = 0;
public static void main(String[] args) {
// 데이터를 쓰는 스레드
Thread writerThread = new Thread(() -> {
try {
Thread.sleep(1000); // 1초 대기
System.out.println("Writer thread is setting flag to true");
flag = true;
System.out.println("Flag has been set to true");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 데이터를 읽는 스레드
Thread readerThread = new Thread(() -> {
System.out.println("Reader thread is waiting for flag");
while (!flag) {
counter++; // flag가 false인 동안 계속 반복
}
System.out.println("Reader thread detected flag change!");
System.out.println("Loop executed " + counter + " times");
});
// 스레드 시작
readerThread.start();
writerThread.start();
// 메인 스레드에서 두 스레드가 종료될 때까지 대기
try {
readerThread.join();
writerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
결과
Reader thread is waiting for flag
Writer thread is setting flag to true
Flag has been set to true
Reader thread detected flag change!
Loop executed 556426940 times
만약 volatile을 사용하지 않았다면 readerThread가 flag의 변경을 감지하지 못했을 것이며, 루프가 무한히 계속되었을 것이다. 이는 각 스레드가 CPU 캐시에서 값을 읽기 때문이다. 위와 같은 결과는 volatile이 멀티스레드 환경에서 변수의 가시성을 보장한다는 것을 보여주는 예시다. 하지만, 위에서 언급했듯이 멀티 스레드 환경에서 항상 정답은 아니다. 원자성을 보장하지 못하므로 AtomicInteger나 synchronized 키워드 등 다른 대안을 선택해야 한다. Spring Boot에서 동기화 메커니즘이 다양한 것으로 알고 있다. 앞으로 다양한 동기화 방식을 알아보겠다 !!
정리
volatile
- Main Memory에 read & write를 보장하는 키워드
- 하나의 Thread가 write하고 나머지 Thread가 읽는 상황인 경우
- 변수의 값이 최신의 값으로 읽어와야 하는 경우
주의할 점?
- 성능에 영향이 어느 정도 영향을 줄 수 있는 Point라는 점
참고 자료