본문 바로가기
인프런/김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성

[인프런] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 / 5. 메모리 가시성

by hxxyeoniii 2025. 2. 26.

volatile, 메모리 가시성

* volatile은 자바에서 예약된 키워드로 패키지 이름으로 사용할 수 없으므로 volatile1을 패키지 이름으로 사용

 

VolatileFlagMain.java

package thread.volatile1;

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

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);
        task.runFlag = false;
        log("runFlag = " + task.runFlag);
        log("main 종료");
    }

    static class MyTask implements Runnable {
        boolean runFlag = true;

        @Override
        public void run() {
            log("task 시작");
            while(runFlag) {
                // runFlag가 false로 변하면 탈출
            }
            log("task 종료");
        }
    }
}

 

 

실행 결과

-> runFlag를 false로 변경하였으나 task 종료가 되지 않음

 

 

 

메모리 가시성 문제

일반적으로 생각하는 메모리 접근 방식

 

> main 스레드와 work 스레드는 각각의 CPU 코어에 할당되어 실행됨

> CPU 코어가 1개라면 빠르게 번갈아 가며 실행되기도 함

 

 

 

but.. 실제 메모리 접근 방식은 아래와 같다.

 

> 메인 메모리는 CPU 입장에서 거리도 멀고 속도도 느림(가격이 저렴해 큰 용량을 쉽게 구할 수는 있음)

> CPU 연산이 매우 빠르기에 CPU 가까이에 매우 빠른 메모리가 필요함 = 캐시 메모리

> 캐시 메모리는 CPU와 가까이 있고 속도도 빠르나 가격이 비싸 큰 용량을 구성하기 힘듬

> 현대 CPU는 대부분은 코어 단위로 캐시 메모리를 각각 보유하고 있음

 

 

 

실행 흐름

 

> 점선 위쪽은 스레드의 실행흐름, 아래쪽은 하드웨어를 나타냄

1. 자바 프로그램을 실행하고 main, work 스레드는 모두 runFlag 값을 읽고 각 캐시 메모리에 불러온다.

2. 프로그램 시작 시점에는 runFlag를 변경하지 않기 때문에 true로 읽는다.

 

3. main 스레드가 runFlag를 false로 설정하고 이때 캐시 메모리의 runFlag도 false로 설정된다.

4. 여기서 핵심은 캐시 메모리의 runFlag만 변한다는 것..! 메인 메모리에 이 값이 즉시 반영되지 않음

   > main 스레드가 runFlag의 값을 변경해도 CPU 코어 1이 사용하는 캐시 메모리의 runFlag만 false로 변경됨

   > work 스레드의 캐시 메모리 runFlag 값은 여전히 true..

 

 

 

그렇다면 캐시 메모리의 runFlag는 언제 메인 메모리에 반영될까?

: 알 수 없다. 아주 극단적으로는 평생 반영되지 않을 수도 있음

-> 메인 메모리에 반영된다 해도, 메인 메모리의 runFlag 값을 work 스레드의 캐시 메모리에 다시 불러와야 함

-> CPU 설계 방식과 종류에 따라 다르며, 주로 컨텍스트 스위칭이 될 때 캐시 메모리도 함께 갱신되기는 하나 이 역시 환경에 따라 달라질 수 있다.

 

 

 

메모리 가시성(memory visibility)

이처럼 멀티스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 문제를 메모리 가시성이라 한다.

= 메모리에 변경한 값이 보이는가? 보이지 않는가?의 문제

 

 

 

volatile 사용

: 값을 읽을 때와 쓸 때 모두 메인 메모리에 직접 접근!!

 

volatile 키워드 사용

: 기존 코드에 volatile 추가

 

실행 결과

-> runFlag를 false로 변경하자마자 task 종료가 출력되는 것 확인

 

 

=> 캐시 메모리를 사용할 때 보다 성능이 느려지는 단점이 있기에 주의해 사용해야 한다.


자바 메모리 모델

Java Memory Model(JMM)

: 자바 프로그램이 어떻게 메모리에 접근하고 수정할 수 있는지를 규정하며, 특히 멀티스레드 프로그래밍에서 스레드 간 상호작용을 정의함

 

 

 

happens-before

: happens-before 관계는 자바 메모리 모델에서 스레드 간 작업 순서를 정의하는 개념

: 만약 A 작업이 B 작업보다 happens-before 관계에 있다면, A 작업에서의 모든 메모리 변경 사항은 B 작업에서 볼 수 있다

: 즉, A 작업에서 변경된 내용은 B 작업이 시작되기 전 모두 메모리에 반영된다.

 

> happens-before 관계는 이름 그대로, 한 동작이 다른 동작보다 먼저 발생함을 보장

> 스레드 간 메모리 가시성을 보장하는 규칙

> 즉, 한 스레드에서 수행한 작업을 다른 스레드가 참조할 때 최신 상태가 보장되는 것

 

 

 

=> volatie 또는 스레드 동기화 기법을 사용하면 메모리 가시성 문제가 발생하지 않는다!!