volatile
2024. 10. 1. 21:48ㆍBackEnd(Java)/Java
volatile 키워드는 자바에서 멀티스레드 환경에서 안전하게 값을 공유하기 위해 사용된다.
주로 변수의 값이 여러 스레드 간에 일관성 있게 공유되어야 할 때 사용되는데 volatile을 사용하면 변수의 값을 메인 메모리에서 직업 읽고 쓰도록 보장하여 스레드 간 가시성 문제를 해결한다.
volatile과 이중 검사 잠금 방식(Double Check Locking, DCL)
이중 검사 잠금 방식에서는 singletonClass 변수를 volatile로 선언하여 스레드 간에 객체의 초기화 상태를 일관성 있게 공유할 수 있다.
자바 메모리 모델에서 객체가 초기화되는 과정은 여러 단계로 이루어지며, volatile 없이 사용할 경우 객체의 완전한 초기화가 끝나지 않았음에도 다른 스레드가 그 객체를 참조하는 문제가 발생할 수 있게 된다.
다음 예시 코드를 보자
public class SingletonClass {
/**
* volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance 변수가 singleton 인스턴스로 초기화되는 과정이 올바르게 진행된다.
*/
private volatile static SingletonClass uniqueInstance;
private SingletonClass() {
}
//DCL(Double Checking Locking)을 사용하여 멀티스레딩 환경에서도 싱글턴이 돌아가도록 설정
public static SingletonClass getInstance() {
if (Objects.isNull(uniqueInstance)) {
synchronized (SingletonClass.class) {
if (Objects.isNull(uniqueInstance)) {
uniqueInstance = new SingletonClass();
}
}
}
return uniqueInstance;
}
}
왜 volatile이 중요한가?
- 메모리 가시성 문제 해결: 멀티스레드 환경에서 변수의 값이 CPU 캐시에 저장되어 다른 스레드들이 변경된 값을 보지 못하는 문제가 발생할 수 있다. volatile 키워드는 해당 변수가 항상 메인 메모리에서 읽고 쓰이도록 보장하여 이 문제를 해결한다.
- 인스턴스 초기화의 재정렬 방지: 객체가 생성되는 과정은 여러 단계로 이루어지며, 컴파일러나 JVM에 의해 코드 실행 순서가 최적화될 수 있어서 volatile 키워드를 사용하면 객체 초기화 과정에서 이런 재정렬이 방지되어, 다른 스레드에서 불완전하게 초기화된 객체를 참조하는 상황을 막을 수 있다
volatile 장점
- 메모리 가시성 보장
volatile 키워드를 사용하면 변수의 값이 모든 스레드에서 즉시 반영됩니다. 변수의 값이 항상 메인 메모리에서 읽히기 때문에, 특정 스레드에서 변경된 값을 다른 스레드가 즉시 볼 수 있습니다. 이는 스레드 간에 데이터 불일치를 방지하는 중요한 역할을 합니다. - 경량화된 동기화
volatile은 synchronized 블록보다 비용이 적게 듭니다. synchronized는 블록에 들어가는 동안 잠금을 걸어야 하므로, 잠금을 관리하는 오버헤드가 있습니다. 반면, volatile은 메모리 가시성만 보장하므로 성능 면에서 더 효율적입니다. - 재정렬 방지 (Reordering Prevention)
자바 컴파일러는 성능 최적화를 위해 명령어의 재정렬을 할 수 있습니다. 하지만 volatile로 선언된 변수는 이런 재정렬을 방지하여, 스레드들이 변수의 상태를 올바르게 확인할 수 있게 합니다. 이는 특히 객체 초기화 단계에서 중요한 역할을 합니다. - 안전한 읽기/쓰기
volatile은 읽기 및 쓰기 작업 자체는 **원자성(atomic)**을 보장합니다. 즉, 다른 스레드에서 동시에 volatile 변수를 읽거나 쓰는 동안 데이터가 손상되지 않습니다.
volatile 단점
- 원자성(atomicity) 미보장
volatile은 읽기/쓰기 자체는 원자적이지만, **복잡한 연산(예: 값 증가나 감소 등)**은 보장하지 않습니다. 예를 들어, volatile int 변수에 대해 i++와 같은 연산은 안전하지 않습니다. 이는 두 단계에 걸친 연산이기 때문에, 그 사이에 다른 스레드가 해당 값을 변경할 수 있습니다. 이런 경우에는 synchronized나 Atomic 클래스(예: AtomicInteger)가 필요합니다. - 잠금(lock) 미제공
volatile은 단순히 변수의 값을 메모리에서 읽고 쓰는 작업만을 보장하므로, 복잡한 상태 변경이나 복수의 변수를 조작해야 하는 경우에는 동기화 메커니즘이 필요합니다. 예를 들어, 객체의 여러 필드를 변경하거나 비교 후 업데이트하는 작업 등에서는 synchronized 같은 잠금 메커니즘이 필요합니다. - 복잡한 연산에 부적합
volatile은 단일 변수의 상태가 즉시 반영되기를 원할 때 적합하지만, 여러 변수 간의 상호 관계를 유지해야 할 때는 적합하지 않습니다. 예를 들어, 트랜잭션과 같은 상황에서 여러 상태를 동기화해야 할 경우 volatile은 한계가 있습니다. - 퍼포먼스 비용
volatile을 사용하면 변수의 값을 항상 메인 메모리에서 읽고 써야 하기 때문에 메모리 접근 비용이 발생합니다. CPU 캐시를 사용할 수 없기 때문에, 빈번한 읽기/쓰기 작업에서 성능이 저하될 수 있습니다. 이로 인해 캐시 일관성 프로토콜에 따라 CPU 성능이 저하될 수 있습니다. - 복잡한 객체 참조에는 한계
volatile은 기본 자료형 또는 참조형 변수에 대해서는 안전하게 사용할 수 있지만, 복잡한 객체 내부의 필드까지는 보장하지 않습니다. 예를 들어, volatile로 선언된 객체의 내부 상태를 변경하는 경우, 내부 필드 변경은 동기화되지 않기 때문에 다른 스레드에서 불일치한 상태를 볼 수 있습니다.
volatile 장단점 요약
장점 | 단점 |
메모리 가시성 보장 | 원자성 보장 부족 |
경량화된 동기화 | 복잡한 연산에 부적합 |
명령어 재정렬 방지 | 잠금(lock) 기능 없음 |
안전한 읽기/쓰기 | 메모리 접근 비용 발생 가능 |
특정 케이스에서 성능 이점 | 복잡한 객체 참조 관리 어려움 |
volatile은 메모리 가시성을 보장하고, 간단한 동기화가 필요할 때 유용하지만, 복잡한 상태 관리가 필요한 경우에는 더 강력한 동기화 기법을 사용해야 합니다.
반응형
'BackEnd(Java) > Java' 카테고리의 다른 글
Equasls, HashCode에 대해서 알아보자 (0) | 2022.12.06 |
---|---|
Hash에 대해서 알아보자 (0) | 2022.12.06 |
CompletableFuture (0) | 2022.11.22 |
추상클래스 vs 인터페이스 (0) | 2022.09.01 |
call by value? call by reference? (0) | 2022.08.22 |