가시성(Visibility) 보장
멀티쓰레드 환경에서 공유 변수의 값 변경 시 다른 쓰레드들에게 변경 사항이 즉시 반영되는 것을 보장합니다. 쓰레드는 작업 시 자신의 로컬 캐시에 데이터를 복사해 사용하는데, volatile 키워드가 선언된 변수는 작업 완료 시 메인 메모리에 즉시 갱신됩니다.
public class VolatileVisibilityTest {
public static void main(String[] args) {
SharedData data = new SharedData();
new Thread(() -> {
try { Thread.sleep(2000); } catch (InterruptedException e) {}
data.initializeValue();
System.out.println("갱신 완료: " + data.value);
}, "데이터 쓰레드").start();
while (data.value == 0) {
// 대기 루프
}
System.out.println("최종 값 확인: " + data.value);
}
}
class SharedData {
int value = 0; // volatile로 선언 시 가시성 보장
public void initializeValue() {
this.value = 100;
}
}
원자성(Atomicity) 미보장
volatile은 단일 연산만 보장할 뿐 복합 연산에 대한 원자성은 제공하지 않습니다. 여러 쓰레드가 동시에 증감 연산을 수행할 경우 계산 결과가 누락되는 문제가 발생할 수 있습니다.
public class AtomicityIssueTest {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
for(int i=0; i<20; i++) {
new Thread(() -> {
for(int j=0; j<1000; j++) {
counter.incrementValue();
}
}).start();
}
Thread.sleep(3000); // 대기
System.out.println("최종 카운트: " + counter.count);
}
}
class Counter {
int count = 0;
public void incrementValue() {
count++; // volatile 적용 시에도 원자성 문제 발생
}
}
명령어 재정렬 방지
CPU의 실행 최적화로 인한 명령어 순서 변경을 막아줍니다. volatile 변수는 메모리 배리어(Memory Barrier)를 생성해 코드의 실행 순서를 보장합니다.
public class InstructionReorderingTest {
private boolean status = false;
private int operand = 0;
public void configureSettings() {
operand = 5; // 연산 준비
status = true; // 상태 변경
}
public void validateOperation() {
if(status) {
operand += 10; // operand가 0일 가능성 존재
}
}
}
메모리 모델 설명
- 로드(load) 명령: 메인 메모리 → 레지스터
- 스토어(store) 명령: 레지스터 → 메인 메모리
- 메모리 배리어: CPU의 명령어 재배치 방지
동기화 대안
- synchronized 블록: 원자성과 가시성 동시 보장
- AtomicInteger: CAS(Compare and Swap) 기반 원자적 연산 제공