강의 메모 - volatile #
개요 #
- volatile 은 변수의 가시성과 연산의 순서를 제어하기 위해 사용하는 키워드로서 스레드 간의 데이터 일관성과 가시성을 보장하는 역할을 한다
- 가시성 보장 : 모든 쓰레드가 동일한 값을 보는것
CPU 캐시 메모리와 메인 메모리 #
- 현대 컴퓨터는 거의 대부분 2개 이상의 CPU가 장착되어 있으며 각 코어에는 레지스터와 캐시메모리가 존재한다
- CPU 캐시 메모리는 CPU 레지스터와 메인 메모리 사이에서 데이터 흐름을 최적화하고 성능을 향상시키기 위해 사용되는 고속 메모리이다
- CPU 는 값을 읽어올 때 우선 캐시에 해당 값이 있는지 확인하고 없는 경우에만 메인 메모리에서 읽어오는 특성을 가진다
CPU 가 데이터 처리를 위해 메인 메모리로 접근 할 때 다음과 같은 순서로 진행한다
- CPU는 메인 메모리의 데이터를 CPU 캐시로 읽어들인다
- 캐시 데이터를 CPU 레지스터로 다시 읽어들이고 읽어 온 데이터로 명령을 수행한다
- 데이터를 다시 메인 메모리에 저장 하기 위해서는 데이터를 읽어들일 때의 과정을 역순으로 진행한다
- 작업 결과를 레지스터에서 캐시로 보내고 적절한 시점에 캐시에서 메인 메모리로 보낸다
- 바로 보내는게 아닌, 특정한 시점에 수행하는것
※ CPU 는 처리 성능을 높이기 위해서 속도가 느린 메인 메모리에서 데이터를 읽고 쓰는 대신 속도가 빠른 CPU 캐시 메모리에서 데이터를 읽고 저장하는 메카니즘을 수행한다 ※ 멀티 스레드 환경에서 CPU 에 할당된 스레드가 메인 메모리가 아닌 CPU 캐시에서 공유 변수를 참조하게 되면 서로 다른 변수 값을 스레드가 참조하게 되는 되는 상황이 발생하게 된다
가시성 #
- 멀티스레드 환경에서 공유 변수의 변경 내용이 한 스레드에서 다른 스레드에게 어떻게 보이는지를 나타내는 개념을 말한다
- 멀티스레드 프로그래밍에서는 여러 스레드가 동시에 변수에 액세스하고 수정할 수 있기 때문에 모든 스레드에게 변수의 값이 일관되게 보여지도록 가시성이 확보되어야 한다
가시성 문제 #
- CPU 캐시에서 작업한 결과가 메인 메모리에 즉시 반영 되지 않을 경우 스레드 간에 결과가 다르게 보여지는 현상을 말한다
- CPU1 은 메인 메모리로부터 값을 읽어와서1을 더한 값을 캐시에 저장하고 메인 메모리에 반영하지 않는다
- T2 는 CPU1에서 변경한 데이터가 메인 메모리에 반영되지 않은 상태에서 값을 읽기 때문에 두 스레드는 서로 다른 캐시 값을 바라보는 가시성 문제가 발생한다
- 스레드 간 가시성을 확보하기 위해서는 CPU 가 작업한 결과를 즉시 메인 메모리에 반영해야 하며 스레드는 캐시가 아닌 메인 메모리에서 값을 참조해야 한다
- 메인메모리에 즉시 저장하지 않는 문제점 해결해야함
- Main Memory가 아닌 CPU cache에서 읽어오는 문제점 해결해야함
volatile 원리 #
- 캐시 메모리 현상으로 공유 변수에 대해 스레드간 가시성 문제가 발생할 경우 volatile 키워드를 사용하면 가시성 문제를 해결할 수 있다
- 참고로, 가시성 문제만 해결하고 동시성 문제는 해결하지못함
- 공유 변수에 volatile 키워드를 선언하면 CPU가 데이터 작업을 할 때 메인 메모리에서 공유변수를 직접 읽고 수정된 결과를 메모리에 즉시 반영함으로 가시성 문제를 해결한다
경쟁 조건 (Race Condition) #
- 스레드간 경쟁 조건을 벌여 CPU 의 캐시 메모리에 비정상적인 데이터가 저장되고 잘못된 결과 데이터가 메인 메모리에 업데이트 되는 현상이 발생한다
- 여러 스레드가 동일한 공유변수에 접근할때 발생
- 동시성 문제 때문 (위에 설명한 가시성과는 별개의 문제)
- 가시성도 해결해야하고, 동시성도 해결해야한다.
- CPU1, CPU2 가 동시에 메모리에서 sharedData 값을 읽어와서 1을 더한 값을 캐시에 저장한다
- CPU1, CPU2 에 할당된 스레드들은 경쟁조건으로 인해 두 캐시에 똑같은 2의 값을 저장한다
- 캐시 값을 메인 메모리에 반영 하더라도 캐시 자체가 이미 잘못된 값을 가지고 있기 때문에 프로그램은 비정상적으로 동작하게 된다
Syncrhonized #
- synchronized 블록을 사용하면 한 시점에 오직 하나의 쓰레드만이 동기화 영역에 접근할 수 있도록 보장해준다.
- 동시성 해결
- synchronized 블록 안에서 참조되는 모든 변수들은 메인 메모리로부터 읽어들여지고 블록을 벗어나면 그 동안 수정된 모든 변수들이 즉시 메인 메모리로 반영될 수 있도록 해준다
- 가시성 해결
- synchronized 는 상호배제와 함께 가시성의 문제까지 해결할 수 있는 기능을 포함하고 있다(synchronized 블록 내에서는 volatile 키워드가 없어도 된다)
- 성능 감수는 필요
volatile 한계점 #
- volatile 은 스레드 간 공유변수에 대한 가시성을 보장하지만 동시적 상호배제를 보장해 주지 않는다
- volatile 변수를 읽기작업하는 스레드와 쓰기작업하는 스레드가 N:1 의 상황에서는 동시성을 보장하지만 N:N 의 상황에서는 동시성을 보장해 주지 못한다
- 쓰기 작업을 하는 쓰레드가 1개여야 동시성 보장이 된다는 뜻
- 쓰기 작업을 하는 쓰레드가 2개 이상일 경우 동시성 보장 못하므로 synchronized 를 써야한다는 뜻
Happens-Before 보장 #
- JVM 은 프로그램의 성능을 향상시키기 위해 명령어를 재 정렬하지만 volatile 변수를 사용하면 해당 변수를 읽거나 쓰는 작업은 특별한 규칙에 따라 재정렬되지 않도록 보장한다
- 즉 volatile 변수 전과 후에 실행되는 명령들은 JVM 컴파일러에 의해 재 정렬 될 수 있으나 volatile 변수에 대한 명령 이전/이후에 존재한다는 규칙은 반드시 지켜진다