개요
- 김영한 강사님의 강의를 듣고 정리하였다.
본론
자바에는 volatile
이라는 키워드가 존재한다.
volatile은 메인 메모리에 값을 읽고쓴다는 의미이다.
왜 이런 키워드가 존재하고 어디서 사용하는 것일까?
문제 상황
public class VolatileFlagMain{
public static void main(String[] args){
MyTask task = new MyTask();
Thread t = new Thread(task, "work");
t.start();
sleep(1000);
task.runFlag = false;
}
static class MyTask implements Runnable{
boolean runFlag = true;
@Override
public void run(){
while(runFlag){
// runFlag가 false로 변경되면 탈출한다.
}
}
}
}
- 메인 스레드는 새로운 스레드를 만들고 runFlag를 사용한 무한 루프 작업을 스레드에 할당하여 start()한다.
- 메인 스레드는 1초를 대기하고, 새롭게 만든 스레드의 작업에 있는 runFlag를 false로 전환하여 스레드를 종료시킨다.
우리가 기대한 결과는 runFlag가 false로 전환되면서 work스레드가 종료되는 것을 기대한다.
하지만 이 코드를 실행해보면 work 스레드는 while문을 빠져나오지 못하고, 계속 while문을 실행한다.
왜 이런 결과가 나오는 걸까?
문제 상황 이해
앞에서 메인 스레드에서 수정한 runFlag와 work 스레드의 runFlag가 다른 것일까?
이는 메모리 가시성이라고 불리는 문제이다.
왜 이런 문제가 발생하는 걸까?
일반적으로 생각하는 메모리 접근 방식
보통 생각하는 접근 방식은 앞에서 봤던 것과 동일하다.
- 메인 스레드가 runFlag를 false로 변환한다.
- 따라서 work 스레드는 while(false)가 되어 반복문을 탈출하고 작업을 종료한다.
하지만, 실제로는 이런 방식으로 작동하지 않는다.
실제 메모리 접근 방식
우리가 간과한 점은 CPU가 처리 성능을 개선하기 위해서 메인 메모리 사이에 캐시 메모리를 두고 사용한다는 점이다.
우리가 앞에서 생각한 메모리 접근 방식에서 수정한 runFlag는 위치적으로 다른 위치에 존재하고 있다는 것이다!
- 메인 스레드가 runFlag를 캐시 메모리에 불러온다.
- work 스레드도 runFlag를 캐시 메모리에 불러온다.
- 메인 스레드는 캐시 메모리에 있는 runFlag를 false로 수정한다.
- work 스레드의 캐시 메모리에 있는 runFlag는 false로 변경된 적이 없으므로 계속 while문을 반복한다.
그렇다면 캐시 메모리에 있는 정보가 언제 work 스레드도 알 수 있을까?
- 이는 2번의 단계를 거치는데 첫 번째로 메인 스레드의 runFlag값이 메인 메모리에 반영되어야 하고, 메인 메모리에 반영된 값이 work 스레드의 캐시에도 반영되어야 한다.
메인 메모리의 반영 시기
- CPU의 구현과 작동 방식에 따라 다르기 때문에 우리는 알 수 없다.
메인 메모리에서 work 스레드의 캐시로의 반영 시기
- 이 때도 우리는 정확하게 알 수 없다.
하지만 두 가지 경우 모두 “언젠가” 되기는 한다.
그렇지만 우리는 기대한 결과를 얻기 위해서 volatile 키워드를 사용해 메인 메모리에 직접 읽고 쓰고를 하도록 하는 것이다.
메모리 가시성, memory visiblity
멀티 스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는가에 대한 문제를 메모리 가시성이라고 한다.
자바에서는 이를 해결하기 위해 간편하게 volatile 키워드를 사용할 수 있다.
public class VolatileFlagMain{
public static void main(String[] args){
MyTask task = new MyTask();
Thread t = new Thread(task, "work");
t.start();
sleep(1000);
task.runFlag = false;
}
static class MyTask implements Runnable{
volatile boolean runFlag = true;
@Override
public void run(){
while(runFlag){
// runFlag가 false로 변경되면 탈출한다.
}
}
}
}
runFlag를 volatile 키워드로 선언해주면 메인 메모리에 직접 읽고 쓰기 때문에 우리가 기대한 결과가 나오는 것을 확인할 수 있다.
- 하지만 캐시 메모리보다 훨씬 속도가 떨어지는 메인 메모리에 직접 읽고 쓰기 때문에 비교적 성능이 많이 떨어진다는 단점이 있다.
참고
'JAVA' 카테고리의 다른 글
[JAVA] java.util.concurrent.LockSupport 알아보자 (0) | 2024.09.24 |
---|---|
[JAVA] 자바의 임계 영역과 동기화, synchronized (0) | 2024.09.23 |
[JAVA] 스레드의 양보 Thread.yield() (0) | 2024.09.21 |
[JAVA] 대기중인 스레드를 RUNNABLE로 깨우려면 어떻게 해야할까? (0) | 2024.09.21 |
[JAVA] 스레드의 상태 (getState()) (3) | 2024.09.02 |