volatile은 변수의 값을 항상 메인 메모리에서 읽고 쓰도록 강제하는 키워드다.
멀티스레드 환경에서 각 스레드는 성능을 위해 메인 메모리의 값을 CPU 캐시나 레지스터에 복사해서 사용한다.
이로 인해 한 스레드가 값을 변경하더라도 다른 스레드는 변경 이전의 값을 계속 읽는 문제가 발생할 수 있다. (가시성 보장 X)
volatile로 선언된 변수는 읽을 때 항상 메인 메모리에서 읽고 쓸 때 즉시 메인 메모리에 반영된다.
-> 스레드 간 값의 가시성이 보장된다.
volatile은 메모리 배리어를 삽입해서 명령어 재정렬과 캐시 일관성 문제를 제어한다.
자바 프로그램은 소스 코드에 작성된 순서 그대로 실행되는 것처럼 보이지만, 실제로는 컴파일러 최적화와 CPU 명령어 재정렬로 인해서 코드 순서와 실행 순서가 달라질 수 있다.
이 과정에서 멀티스레드 환경에서는 의도하지 않은 결과가 발생할 수 있고,
volatile은 이러한 문제를 방지하기 위해 해당 변수의 읽기와 쓰기 앞뒤에 메모리 배리어를 삽입한다.
-> 메모리 배리어를 쓰기 때문에 메인 메모리 읽기 쓰기를 하는것이다.
메모리 배리어는 특정 지점을 기준으로 명령어의 재정렬을 금지하는 장치이다.
쓰기(write) 시
이전에 수행된 모든 연산이 메인 메모리에 반영된 후에 volatile 변수에 값을 기록한다.
읽기(read) 시
volatile 변수의 값을 메인 메모리에서 다시 읽은 후에 연산을 수행한다.
그래서
volatile 변수 이전의 연산은 절대 뒤로 밀리지 않고, volatile 변수 이후의 연산은 절대 앞으로 당겨지지 않는다.
volatile이 없을 때
class Worker extends Thread {
private boolean running = true;
public void stopRunning() {
running = false;
}
public void run() {
while (running) {
// 작업 수행
}
System.out.println("스레드 종료");
}
}
위 코드는 running 값이 CPU 캐시에 저장된다. 그래서 stopRunning()에서 false로 바꿔도 run() 스레드는 계속 true만 읽고 무한 루프에 빠질 수 있다.
volatile이 있을 때
class Worker extends Thread {
private volatile boolean running = true;
public void stopRunning() {
running = false;
}
public void run() {
while (running) {
// 작업 수행
}
System.out.println("스레드 종료");
}
}
running을 읽을 때마다 항상 메인 메모리에서 읽어서 한 스레드의 변경이 즉시 다른 스레드에 보인다.
public static void main(String[] args) {
Worker worker = new Worker();
worker.start(); // 새 스레드 생성
Thread.sleep(1000);
worker.stopRunning(); // ← main 스레드가 호출
}
이 코드는 서로 다른 스레드가 같은 객체를 참조한다.
일단 이 코드 위의 첫 번째 코드(volatile이 없을 때)에서 문제가 되는 부분은 아래와 같다.
while (running) {
// 작업 수행
}
Worker 스레드는 running 값을 한 번 읽은 뒤 CPU 캐시에 저장된 값을 반복해서 사용할 수 있다. (힙의 최신 값이 반영되지 않을 수 있다.)
volatile을 추가 안 했을 때의실제 동작과정
volatile을 추가했을 때의 실제 동작 과정
volatile int count = 0;
count++;
위의 코드는 읽기, 계산, 쓰기의 여러 단계로 이루어지며, 원자성은 보장되지 않는다.
-> volatile은 상태를 공유하되, 복합 연산이 없는 경우에만 적합하다.
마지막으로 volatile은 원자성을 보장하지 않기 때문에 상태신호(켜짐/꺼짐, 완료/미완료, 종료/실행 중 등)를 나타낼 때 써야한다.