본문 바로가기

Java

Java volatile - 메모리 가시성

1. 개요

본 포스팅은 김영한 강사님의 인프런 강의 "자바 고급 1편" 중 volatile 키워드에 대해 정리한 내용이다.

2. 멀티스레드 동작 방식과 메모리 가시성

다음 코드가 있다고 가정해보자.

package thread.volatile1;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class VolatileFlagMain {

    public static void main(String[] args) {
        MyTask myTask = new MyTask();
        Thread t = new Thread(myTask, "work");
        log("runFlag = " + myTask.runFlag);

        t.start();


        sleep(1000);
        log("runFlag를 false로 변경 시도");
        myTask.runFlag = false;
        log("runFlag = " + myTask.runFlag);
        log("main 종료");

    }

    static class MyTask implements Runnable{

        //변수를 메인 메모리에 직접 read, write
        boolean runFlag = true;

        @Override
        public void run() {
            log("task start");
            while (runFlag) {

            }
            log("task end");
        }
    }
}

 

간단히 설명하자면 다음과 같다.

  • 메인 스레드에서 MyTask를 구현한 스레드 t를 작동시킨다.
  • myTask는 flag가 false가 될 때 까지 계속 작업을 수행한다.
  • 메인 스레드는 t에서 동작하는 myTask의 flag를 false로 만들고 종료한다.

예상대로라면 메인스레드가 runFlag를 false로 만들 때 t는 종료되어야 하지만 실제로 돌려보면 그렇게 동작하지 않는다. 그 이유는 프로그램의 동작 원리를 보면 알 수 있다.

 CPU에는 속도 향상을 위해 L1 캐시가 내장되어 있다. 참고로 그리진 않았지만 CPU 밖에 존재하는 캐시를 L2 캐시라 한다. 캐시는 저장소 중 속도가 레지스터 다음으로 높지만, 그만큼 가격이 비싸 위와 같은 방식으로 보통 사용한다.

이제 메인 스레드에서 flag를 false로 바꾸어보자. 각 코어는 캐시에 해당 변수가 없는 때에만 메인 메모리에서 값을 불러온다. 하지만 위와 같은 상황에서 t는 캐시에 있는 flag를 불러오므로 연산을 멈추지 않게 된다. 이처럼 한 스레드에서의 변경 사항이 다른 스레드에 언제 보이는지이 대한 문제를 memory visibility라 한다.

3. Volatile

public class VolatileFlagMain {

    public static void main(String[] args) {
        MyTask myTask = new MyTask();
        Thread t = new Thread(myTask, "work");
        log("runFlag = " + myTask.runFlag);

        t.start();


        sleep(1000);
        log("runFlag를 false로 변경 시도");
        myTask.runFlag = false;
        log("runFlag = " + myTask.runFlag);
        log("main 종료");

    }

    static class MyTask implements Runnable{

        //변수를 메인 메모리에 직접 read, write
        volatile boolean runFlag = true;

        @Override
        public void run() {
            log("task start");
            while (runFlag) {

            }
            log("task end");
        }
    }
}

위 코드는 flag에 키워드 volatile을 추가한 코드이다. 이를 실행하면 처음 의도했던대로 동작하는 것을 알 수 있다.

 

volatile은 해당 변수를 메인 메모리의 힙 영역에 넣고 직접 사용하겠다고 명시하는 키워드이다. 이를 사용하여 2에서 나온 문제를 해결할 수 있다.

 하지만 위에서 언급했듯이 캐시는 성능 향상을 위한 설계이다. 따라서 volatile 키워드를 아무데나 사용하는 것은 올바르지 않다.

'Java' 카테고리의 다른 글

[Java] BlockingQueue로 생산자-소비자 문제 해결하기  (0) 2024.09.10
[Java] 동시성 문제 해결하기  (0) 2024.09.03
Java Record  (0) 2024.08.31