💡 CPU와 메모리의 관계

CPU는 매우 빠르게 연산을 수행하지만, 메모리 속도는 상대적으로 느립니다.
이 문제를 해결하기 위해 캐시 메모리(Cache Memory)가 사용됩니다.

🖥️ CPU와 메모리 구조

main 스레드 -> CPU 코어1 -> 캐시 메모리 -> 메인 메모리
work 스레드 -> CPU 코어2 -> 캐시 메모리 -> 메인 메모리
  • 메인 메모리(RAM): 용량이 크지만 CPU 입장에서 거리가 멀고 속도가 느림
  • 캐시 메모리(Cache Memory): CPU 가까이에 있으며 속도가 매우 빠름, 하지만 용량이 작고 가격이 비쌈

🔥 멀티 스레드에서의 문제: 메모리 가시성

🎭 runFlag 문제 상황

volatile boolean runFlag = true;

Thread mainThread = new Thread(() -> {
    runFlag = false;
});

Thread workThread = new Thread(() -> {
    while (runFlag) {
        // do something...
    }
});

위 코드에서 mainThreadrunFlag 값을 false로 변경해도,
workThread에서는 여전히 true로 보일 수 있습니다. 왜 그럴까요?

🧠 캐시 메모리의 역할

각 스레드는 CPU 코어에서 실행되며, 코어는 캐시 메모리를 활용합니다.
runFlag 값이 캐시 메모리에 저장되면, 각 코어는 자신의 캐시 값을 계속 사용하게 됩니다.
즉, mainThreadrunFlag = false;로 변경해도, workThread가 실행되는 CPU 코어의 캐시 메모리에는 반영되지 않을 수 있습니다.

🕵️‍♂️ 언제 메인 메모리에 반영될까?

이 부분은 CPU 설계 및 캐시 동기화 정책에 따라 다릅니다. 즉, 명확한 시점을 보장할 수 없습니다.

  • 어떤 CPU에서는 즉시 반영될 수도 있고,
  • 어떤 CPU에서는 한참 뒤에 반영될 수도 있습니다.

이런 메모리 가시성 문제를 해결하지 않으면, 멀티 스레드 프로그래밍에서 의도치 않은 동작이 발생할 수 있습니다.

🏆 해결 방법

volatile 키워드 사용

volatile boolean runFlag = true;
  • volatile을 사용하면 모든 스레드가 항상 메인 메모리 값을 읽고 씁니다.
  • 즉, 캐시를 거치지 않기 때문에 변경된 값이 모든 스레드에 즉시 반영됩니다.

synchronized 블록 사용

synchronized (this) {
    runFlag = false;
}
  • synchronized를 사용하면 스레드 간 동기화가 보장됩니다.
  • 하지만 성능이 저하될 수 있으므로 주의해야 합니다.

Lock 사용

Lock lock = new ReentrantLock();
lock.lock();
try {
    runFlag = false;
} finally {
    lock.unlock();
}
  • Lock을 사용하면 synchronized보다 유연한 동기화가 가능합니다.

📌 정리

  • 멀티 스레드 환경에서는 메모리 가시성 문제가 발생할 수 있다.
  • 캐시 메모리 때문에 스레드마다 다른 값을 볼 수도 있다.
  • 메모리 가시성 문제를 해결하려면 volatile, synchronized, Lock 등을 활용해야 한다.

이제 멀티 스레드 프로그래밍을 할 때 메모리 가시성을 꼭 고려 하시길 바랍니다.!

profile
배움을 추구하는 개발자

0개의 댓글