멀티 스레드를 사용하는 상황에서 Runnable
을 구현하여 실행한 다수의 Thread가 Thread 내부 변수의 변경된 값을 읽지 못하는데,
이러한 문제를 메모리 가시성 문제라고 한다.
public class TaskRunner {
private static class Task implements Runnable {
boolean flag = true;
@Override
public void run() {
while (!flag) {}
System.out.println("Task 종료");
}
}
public static void main(String[] args) {
Task task = new Task();
task.start();
task.flag = false;
}
}
문제는 상대적으로 속도가 느린 메모리의 접근 횟수를 줄이기 위해 값을 캐싱하여 사용하는 데 있다.
캐시는 메모리 중 일부를 미리 가져오고 CPU는 메모리에 접근 전 먼저 캐시 내 원하는 데이터가 존재하는지 확인한다.
이때 L1 ➡ L2 ➡ L3 순으로 캐시 검색 후 캐시 미스 시 메모리를 조회한다.
위 코드 기준으로 Task
Thread는 메모리에 반영된 변경 값(false
)이 캐시에 반영 전까지 flag
변수를 true
로 참조하여 예상대로 동작하지 않는 것이다.
L1 ➡ L2 ➡ L3 순으로 속도가 점차 느려지며, 용량은 커진다.
일부 캐시는 프로세서별로 구조가 상이할 수 있다.
🚌 버스란?
- 메인보드에서 각 장치를 연결하여 데이터가 지나다니는 통로
🚌 CPU 내부 버스란?
- CPU 내부 장치를 연결하는 버스로, BSB(Back Side Bus) 또는 후면 버스라 불림
🚌 시스템 버스란?
- 메모리와 주변 방치를 연결하는 버스로, FSB(Front Side Bus) 또는 전면 버스라 불림
- 메인보드의 동작 속도를 의미
일반적인 컴퓨터의 캐시 적중률은 약 90%라고 한다.
Thread 내 공유 변수에 volatile
키워드를 사용한다.
단, 메모리 내 데이터를 직접 읽고 쓰기 때문에 성능은 저하된다.
사용하지 않을 경우 캐시 메모리 갱신 전까지 메모리와 캐시 간 데이터가 일치하지 않는 문제가 발생할 수 있다.
주로 컨텍스트 스위칭 시 캐시 메모리도 함께 갱신되지만 이는 달라질 수 있다.
public class TaskRunner {
private static class Task implements Runnable {
volatile boolean flag = true; // volatile 키워드 사용!
@Override
public void run() {
while (!flag) {}
System.out.println("Task 종료");
}
}
public static void main(String[] args) {
Task task = new Task();
task.start();
task.flag = false;
}
}
레지스터 ➡ 캐시 ➡ 메모리 ➡ 저장장치
위 순으로 속도가 느려지며, 용량은 커진다.
캐시 내 변경사항을 메모리에 반영하는 방법은 아래와 같다.