[Java] 메모리 가시성과 volatile

MEUN·2024년 10월 30일
0

🔒 상황

멀티 스레드를 사용하는 상황에서 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 순으로 속도가 점차 느려지며, 용량은 커진다.
일부 캐시는 프로세서별로 구조가 상이할 수 있다.

캐시 구조

L1 캐시

  • 직접 레지스터에 연결된 캐시로, 명령어 캐시와 데이터 캐시가 존재
  • 명령어와 데이터를 구분하여 조회하는 특수 캐시
  • 캐시 중 가장 빠르고 용량이 작음

L2 캐시

  • CPU 코어 또는 코어 근처에 위치하여 코어별로 사용되거나 코어들이 공유
  • L1 캐시가 커버하지 못하는 데이터와 명령어 저장

L3 캐시

  • CPU 내부에 위치
  • 캐시 중 가장 용량이 큼

🚌 버스란?

  • 메인보드에서 각 장치를 연결하여 데이터가 지나다니는 통로

🚌 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;
    }
}


🔧 함께 알아두기

저장 장치 계층 구조

레지스터 ➡ 캐시 ➡ 메모리 ➡ 저장장치

위 순으로 속도가 느려지며, 용량은 커진다.


캐시 내 변경사항 반영 방법

캐시 내 변경사항을 메모리에 반영하는 방법은 아래와 같다.

1) 즉시 쓰기 (Write Through)

  • 캐시 내 데이터 변경 즉시 메모리에 반영
  • 메모리와의 빈번한 데이터 전송으로 성능 저하

2) 지연 쓰기 (Write Back)

  • 캐시 내 데이터 변경 시 변경사항을 모아 주기적으로 반영
  • 성능상 이점이 존재하지만 메모리와 캐시 간 데이터 불일치 발생 가능


📚 참고 자료

0개의 댓글