이 글은 김영한님의 스프링 고급편 강의중 제목과 관련된 부분을 블로그장의 취향대로 요약한 것이며 강의 자료 및 출처는 가장 아래에서 확인할 수 있습니다.
1. CPU 캐시 메모리 이해
public class VolatileFlagMain {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread t = new Thread(task, "work");
log("runFlag = " + task.runFlag);
t.start();
sleep(1000);
log("runFlag를 false로 변경 시도");
task.runFlag = false;
log("runFlag = " + task.runFlag);
log("main 종료");
}
static class MyTask implements Runnable {
boolean runFlag = true;
// volatile boolean runFlag = true;
@Override
public void run() {
log("task 시작");
while (runFlag) {
// runFlag가 false로 변하면 탈출
}
log("task 종료");
}
}
}
main스레드와 work스레드 두 개가 있다.
로직을 간략히 설명하면 다음과 같다.
main()와 work()모두 runFlag라는 공통적인 값을 참조하고 있다.
main()에서 work스레드를 생성 후, work스레드의 run()이 실행되는데 runFlag가 true인 상태이기 때문에,
work스레드는 while()문을 계속 반복할 것이다.
이후, main()에서 runFlag를 false로 변경하면서, "main 종료"와 "task 종료"가 모두 출력될 것이다.
실제 결과는 다음과 같다.

main만 종료되고, task종료는 출력되지 않았다.
왜 이렇게 될까?

CPU는 처리 성능을 개선하기 위해 중간에 캐시 메모리라는 것을 사용한다.

main 스레드에서 runFlag를 false로 변경하면 메인 메모리에 값이 즉시 반영되지 않고 먼저, 캐시 메모리의 runFlag 값만 변환된다.
그렇다면, 캐시 메모리에 있는 runFlag값은 메인 메모리에 언제 반영될까?
이 부분에 대한 정답은 CPU 설계 방식과 종류의 따라 다르기때문에 알 수 없다. 극단적으로 평생 반영되지 않을 수도 있다.
만약, 메인 메모리에 반영을 했다고 가정해보자.

이번엔 메인 메모리에 있는 runFlag 값을 work 스레드의 캐시 메모리에 저장해야한다.
이 부분 또한 언제 반영되는지 알아야할텐데 CPU 설계 방식과 종류에 따라 다르기때문에 알 수 없다.
2.메모리 가시성
이처럼 멀티스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 문제를 메모리 가시성이라고 한다.
3. 메모리 가시성 문제 해결 - volatile
캐시 메모리를 사용하면 CPU 처리 성능을 개선할 수 있지만, 여러 스레드에서 같은 시점에 정확히 같은 데이터를 보는 것이 더 중요한 상황이라면 문제가 될 수 있다.
따라서, 성능을 약간 포기하더라도 자바에서는 이런 경우 volatile 키워드를 제공한다.
public class VolatileFlagMain {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread t = new Thread(task, "work");
log("runFlag = " + task.runFlag);
t.start();
sleep(1000);
log("runFlag를 false로 변경 시도");
task.runFlag = false;
log("runFlag = " + task.runFlag);
log("main 종료");
}
static class MyTask implements Runnable {
// boolean runFlag = true;
volatile boolean runFlag = true;
@Override
public void run() {
log("task 시작");
while (runFlag) {
// runFlag가 false로 변하면 탈출
}
log("task 종료");
}
}
}
결과는 다음과 같다.


main스레드와 work스레드 모두 캐시 메모리가 아닌, 메인 메모리에서 직접 값을 반영하고 조회하는 것을 확인할 수 있다.
4.자바 메모리 모델(Java Memory Model)
자바 메모리 모델(JMM)은 자바 프로그램이 어떻게 메모리에 접근하고 수정할 수 있는지 규정하는데,
특히 멀티스레드 프로그래밍에서 스레드 간의 상호작용을 정의한다.
JMM의 핵심은 여러 스레드들의 작업 순서를 보장하는 happens-before 관계에 대한 정의이다.
4-1.happens-before 관계
happens-before 관계는 자바 메모리 모델에서 스레드 간의 작업 순서를 정의하는 개념이다.
만약 A 작업이 B 작업보다 happens-before 관계에 있다면, A 작업에서의 모든 메모리 변경 사항은 B 작업에서 볼 수 있다.
즉, A 작업에서 변경된 내용은 B 작업이 시작되기 전에 모두 메모리에 반영된다.
4-2. happens-before 관계가 발생하는 경우
1.단일 스레드 내에서, 프로그램의 순서대로 작성된 모든 명령문
예를 들어,
int a = 1; int b = 2;
위 경우에서 a = 1은 b = 2보다 먼저 실행된다.
2. volatile 변수 규칙
한 스레드에서 volatile 변수에 대한 쓰기 작업은 해당 변수를 읽는 모든 스레드에 보이도록 한다.
즉, volatile변수에 대한 쓰기 작업은 그 변수를 읽는 작업보다 happens-before 관계를 형성한다.
3. 스레드 시작 규칙
Thread t = new Thread(task);
t.start();
start() 호출 전에 수행된 모든 작업은 새로운 스레드가 시작된 후의 작업보다 happens-before 관계를 가진다.
4. 모니터 락 규칙
한 스레드에서 synchronized 블록을 종료한 후, 그 모니터 락을 얻는 모든 스레드는 해당 블록 내의 모든 작업을 볼 수 있다.
예를 들어, synchronized(lock) { ... } 블록 내에서의 작업은 블록을 나가는 시점에 happens-before 관계가 형성된다. 뿐만 아니라 ReentrantLock과 같이 락을 사용하는 경우에도 happens-before 관계가 성립한다.
이외에도 스레드 종료 규칙, 인터럽트 규칙, 객체 생성 규칙 등등이 있다.
* 출처 자료
김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성| 김영한 - 인프런 강의
현재 평점 5점 수강생 7463명인 강의를 만나보세요. 멀티스레드와 동시성을 기초부터 실무 레벨까지 깊이있게 학습합니다. 멀티스레드, 동시성, 스레드 풀과 Executor 프레임워크, 프로세스와 스레
www.inflearn.com
'Java' 카테고리의 다른 글
| Java) 프로세스 & 스레드, 컨텍스트 스위칭 (2) | 2025.07.23 |
|---|---|
| Java) 인스턴스화를 막으려거든 private 생성자를 사용하라 (3) | 2024.11.07 |
| Java) 생성자나 열거 타입으로 싱글턴임을 보증하라 (1) | 2024.11.05 |
| Java) 생성자에 매개변수가 많다면 빌더를 고려하라 (4) | 2024.11.04 |
| Java) 생성자 대신 정적 팩토리 메서드 (1) | 2024.10.30 |