멀티스레딩을 구현할 때 한 스레드에서 변경한 값이 다른 스레드에는 보이지 않는 상황이 발생할 수 있습니다. 이는 각 스레드가 동일한 메모리에 대해 서로 일관되지 않은 관점을 가진다는 의미이며, 이를 메모리 일관성 오류(memory consistency error)라고 부릅니다.
메모리 일관성 오류가 발생하는 원인
CPU는 성능 최적화를 위해 메인 메모리에 접근하는 순서를 재배열할 수 있으며, 실제 스레드가 메모리에 접근하는 순서와 달라질 수 있습니다. 이러한 현상은 특히 쓰기(write) 작업이 수행될 때 두드러지는데, 이는 CPU의 대기 시간을 줄이기 위함입니다.
쓰기 연산은 원자적(atomic)으로 처리됩니다. 즉, 한 스레드가 쓰기 작업을 수행하는 동안 다른 스레드가 해당 메모리에 대해 다른 연산을 동시에 수행할 수 없습니다.
또한 쓰기 작업이 실행되는 순서 자체는 관련된 모든 CPU에서 일관되게 유지됩니다. 하지만 문제는 각 CPU가 다른 CPU의 쓰기 시점을 서로 다르게 인식할 수 있다는 점입니다. 이러한 인식 차이가 누적되면 메모리 불일치, 즉 메모리 일관성 오류로 이어질 수 있습니다.
메모리 일관성 오류를 피하는 방법
메모리 일관성 오류를 방지하는 핵심은 happens-before 관계를 설정하는 것입니다. happens-before 관계가 성립하면 한 스레드가 수행한 메모리 쓰기 결과가 다른 스레드의 읽기 작업에 반드시 보이도록 보장됩니다.
Java에서 대표적인 happens-before 관계는 다음과 같습니다.
- start() 함수: 새로 생성된 스레드가 시작될 때, start() 호출 이전에 수행된 변경 사항이 새 스레드에 보이도록 보장합니다.
- join() 함수: 한 스레드가 다른 스레드의 join()을 성공적으로 호출해 반환하면, 해당 스레드가 수행한 모든 작업 결과가 join()을 호출한 스레드에 보이도록 보장합니다.
이 외에도 synchronized 블록이나 volatile 변수 역시 happens-before 관계를 형성하여 메모리 가시성을 보장하는 데 활용됩니다.
예제 코드
import java.io.*;
class class_shared{
static int m=2;
void inc(){
for(int j=0;j<5;j++){
m = m+1;
System.out.println("After its increment is "+m);
}
}
void dec(){
for(int j=0;j<5;j++){
m = m-1;
System.out.println("After its decrement is "+m);
}
}
}
public class Demo{
public static void main(String[] args){
final class_shared my_inst = new class_shared();
Thread my_t_1 = new Thread(){
@Override
public void run(){
my_inst.inc();
}
};
Thread my_t_2 = new Thread(){
@Override
public void run(){
my_inst.dec();
}
};
my_t_1.start();
my_t_2.start();
}
}
실행 결과
After its increment is 3 After its decrement is 2 After its decrement is 2 After its decrement is 1 After its increment is 3 After its decrement is 0 After its increment is 1 After its increment is 1 After its decrement is 0 After its increment is 2
코드 설명
'class_shared'라는 클래스는 static 변수 m을 정의하고, inc() 메서드는 숫자를 5번 반복해 1씩 증가시키며 콘솔에 출력합니다. dec() 메서드는 같은 방식으로 값을 1씩 감소시키며 출력합니다.
Demo 클래스의 main 함수에서는 class_shared의 인스턴스를 생성한 뒤, 두 개의 스레드 객체를 만들고 run() 메서드를 오버라이드하여 각각 inc()와 dec()를 호출하도록 설정합니다. 마지막으로 두 스레드를 start() 함수로 실행합니다.
실행 결과를 보면 증가와 감소 출력이 예측한 순서대로 나타나지 않고 뒤섞여 있는 것을 확인할 수 있습니다. 이는 두 스레드가 동일한 static 변수 m에 동시에 접근하면서 발생하는 현상으로, 적절한 동기화 없이는 메모리 일관성이 보장되지 않음을 잘 보여주는 예시입니다.