Java에서 동시성(concurrency)을 다루는 방법은 크게 세가지가 존재한다.
synchronized
volatile
Atomic
이 중 volatile
은 다른 스레드의 변경 사항에 대한 가시성(Visibility)을 확보시켜 준다.
하지만 원자성을 보장하지 않기 때문에, 동시성 문제를 완전히 해결할 수 없다.
메모리 계층 구조에서 CPU 코어와 메인메모리 사이에는 캐시가 존재한다.
프로세서의 발전속도를 메인메모리가 따라가지 못하면서,
CPU와 메인메모리 사이의 속도 간극을 줄여주는 완충제 역할을 위해 캐시를 도입한 것이다.
멀티코어 환경에서는 각 CPU 코어 별로 캐시메모리가 존재한다.
즉, 멀티스레딩 시 서로 다른 스레드가 서로 다른 코어에서 실행되면,
서로 다른 캐시를 사용하게 될 수 있다.
이로인해 메모리 가시성 문제(다른 스레드의 변경 사항이 보이지 않음)가 발생하며, 이를 해결하기 위해 volatile
을 사용할 수 있다.
volatile
은 JVM이 해당 변수의 읽기/쓰기를 항상 메인메모리 기준으로 강제한다.
즉, 어떤 스레드든 캐시된 값이 아닌, 최신 값을 공유하도록 보장한다.
트랜잭션 격리 수준 중 Read Committed
은 커밋된 다른 트랜잭션의 변경사항을 볼 수 있게 해준다.
하지만, 그것만으로 쓰기 작업에 대한 동시성 문제를 해결할 수 없다.
읽기 -> (다른 트랜잭션의 쓰기 커밋) -> 읽은 값에 기반한 쓰기
위와 같은 시나리오에서 동시성 문제가 발생하고, Read Committed
만으로는 해결이 불가능하며, 별도의 락킹 메커니즘이 필요하다.
마찬가지로, volatile
은 다른 스레드의 변경 사항에 대한 가시성은 확보할 수 있으나,
복합 연산을 불가분한 연산으로 만들지 못하므로, 동시성 문제를 완벽히 해결할 수 없다.
Read Only, Write Only 스레드를 나누어야 한다.