032 Volatile

강의 메모 - volatile #

개요 #

  • volatile 은 변수의 가시성과 연산의 순서를 제어하기 위해 사용하는 키워드로서 스레드 간의 데이터 일관성과 가시성을 보장하는 역할을 한다
    • 가시성 보장 : 모든 쓰레드가 동일한 값을 보는것

CPU 캐시 메모리와 메인 메모리 #

  • 현대 컴퓨터는 거의 대부분 2개 이상의 CPU가 장착되어 있으며 각 코어에는 레지스터와 캐시메모리가 존재한다
  • CPU 캐시 메모리는 CPU 레지스터와 메인 메모리 사이에서 데이터 흐름을 최적화하고 성능을 향상시키기 위해 사용되는 고속 메모리이다
  • CPU 는 값을 읽어올 때 우선 캐시에 해당 값이 있는지 확인하고 없는 경우에만 메인 메모리에서 읽어오는 특성을 가진다

img.png

CPU 가 데이터 처리를 위해 메인 메모리로 접근 할 때 다음과 같은 순서로 진행한다

  1. CPU는 메인 메모리의 데이터를 CPU 캐시로 읽어들인다
  2. 캐시 데이터를 CPU 레지스터로 다시 읽어들이고 읽어 온 데이터로 명령을 수행한다
  3. 데이터를 다시 메인 메모리에 저장 하기 위해서는 데이터를 읽어들일 때의 과정을 역순으로 진행한다
  4. 작업 결과를 레지스터에서 캐시로 보내고 적절한 시점에 캐시에서 메인 메모리로 보낸다
  5. 바로 보내는게 아닌, 특정한 시점에 수행하는것

※ CPU 는 처리 성능을 높이기 위해서 속도가 느린 메인 메모리에서 데이터를 읽고 쓰는 대신 속도가 빠른 CPU 캐시 메모리에서 데이터를 읽고 저장하는 메카니즘을 수행한다 ※ 멀티 스레드 환경에서 CPU 에 할당된 스레드가 메인 메모리가 아닌 CPU 캐시에서 공유 변수를 참조하게 되면 서로 다른 변수 값을 스레드가 참조하게 되는 되는 상황이 발생하게 된다

가시성 #

  • 멀티스레드 환경에서 공유 변수의 변경 내용이 한 스레드에서 다른 스레드에게 어떻게 보이는지를 나타내는 개념을 말한다
  • 멀티스레드 프로그래밍에서는 여러 스레드가 동시에 변수에 액세스하고 수정할 수 있기 때문에 모든 스레드에게 변수의 값이 일관되게 보여지도록 가시성이 확보되어야 한다

가시성 문제 #

  • CPU 캐시에서 작업한 결과가 메인 메모리에 즉시 반영 되지 않을 경우 스레드 간에 결과가 다르게 보여지는 현상을 말한다

img_2.png

  1. CPU1 은 메인 메모리로부터 값을 읽어와서1을 더한 값을 캐시에 저장하고 메인 메모리에 반영하지 않는다
  2. T2 는 CPU1에서 변경한 데이터가 메인 메모리에 반영되지 않은 상태에서 값을 읽기 때문에 두 스레드는 서로 다른 캐시 값을 바라보는 가시성 문제가 발생한다
  3. 스레드 간 가시성을 확보하기 위해서는 CPU 가 작업한 결과를 즉시 메인 메모리에 반영해야 하며 스레드는 캐시가 아닌 메인 메모리에서 값을 참조해야 한다
  4. 메인메모리에 즉시 저장하지 않는 문제점 해결해야함
  5. Main Memory가 아닌 CPU cache에서 읽어오는 문제점 해결해야함

volatile 원리 #

  • 캐시 메모리 현상으로 공유 변수에 대해 스레드간 가시성 문제가 발생할 경우 volatile 키워드를 사용하면 가시성 문제를 해결할 수 있다
    • 참고로, 가시성 문제만 해결하고 동시성 문제는 해결하지못함
  • 공유 변수에 volatile 키워드를 선언하면 CPU가 데이터 작업을 할 때 메인 메모리에서 공유변수를 직접 읽고 수정된 결과를 메모리에 즉시 반영함으로 가시성 문제를 해결한다 img_3.png

경쟁 조건 (Race Condition) #

  • 스레드간 경쟁 조건을 벌여 CPU 의 캐시 메모리에 비정상적인 데이터가 저장되고 잘못된 결과 데이터가 메인 메모리에 업데이트 되는 현상이 발생한다
  • 여러 스레드가 동일한 공유변수에 접근할때 발생
  • 동시성 문제 때문 (위에 설명한 가시성과는 별개의 문제)
    • 가시성도 해결해야하고, 동시성도 해결해야한다.

img_4.png

  1. CPU1, CPU2 가 동시에 메모리에서 sharedData 값을 읽어와서 1을 더한 값을 캐시에 저장한다
  2. CPU1, CPU2 에 할당된 스레드들은 경쟁조건으로 인해 두 캐시에 똑같은 2의 값을 저장한다
  3. 캐시 값을 메인 메모리에 반영 하더라도 캐시 자체가 이미 잘못된 값을 가지고 있기 때문에 프로그램은 비정상적으로 동작하게 된다

Syncrhonized #

  • synchronized 블록을 사용하면 한 시점에 오직 하나의 쓰레드만이 동기화 영역에 접근할 수 있도록 보장해준다.
    • 동시성 해결
  • synchronized 블록 안에서 참조되는 모든 변수들은 메인 메모리로부터 읽어들여지고 블록을 벗어나면 그 동안 수정된 모든 변수들이 즉시 메인 메모리로 반영될 수 있도록 해준다
    • 가시성 해결
  • synchronized 는 상호배제와 함께 가시성의 문제까지 해결할 수 있는 기능을 포함하고 있다(synchronized 블록 내에서는 volatile 키워드가 없어도 된다)
  • 성능 감수는 필요 img_5.png

volatile 한계점 #

  • volatile 은 스레드 간 공유변수에 대한 가시성을 보장하지만 동시적 상호배제를 보장해 주지 않는다
  • volatile 변수를 읽기작업하는 스레드와 쓰기작업하는 스레드가 N:1 의 상황에서는 동시성을 보장하지만 N:N 의 상황에서는 동시성을 보장해 주지 못한다
    • 쓰기 작업을 하는 쓰레드가 1개여야 동시성 보장이 된다는 뜻
    • 쓰기 작업을 하는 쓰레드가 2개 이상일 경우 동시성 보장 못하므로 synchronized 를 써야한다는 뜻 img_6.png

Happens-Before 보장 #

  • JVM 은 프로그램의 성능을 향상시키기 위해 명령어를 재 정렬하지만 volatile 변수를 사용하면 해당 변수를 읽거나 쓰는 작업은 특별한 규칙에 따라 재정렬되지 않도록 보장한다
  • 즉 volatile 변수 전과 후에 실행되는 명령들은 JVM 컴파일러에 의해 재 정렬 될 수 있으나 volatile 변수에 대한 명령 이전/이후에 존재한다는 규칙은 반드시 지켜진다 img_7.png