[개발 일기] 2025.01.25 - volatile

2025. 1. 25. 23:08·개발 일기

개요

 

volatile은 멀티 스레드를 공부할 때 많이 봤던 키워드이다.

 

 

오늘은 volatile에 대해 정리해보자.

 

 

 

volatile

 

자바의 volatile 키워드는 변수의 가시성 문제를 해결하기 위해 사용된다.

 

 

쉽게 말해서 volatile가 달린 변수를 메인 메모리에 저장할 때 사용된다.

 

 

가시성 문제

가시성 문제란 쓰레드가 변경한 변수의 값이 CPU의 캐시에만 저장되었고, 메인 메모리엔 반영되지 않는 문제를 말한다. 이러한 문제 때문에 해당 변수를 조회하려는 다른 쓰레드는 이 값을 알지 못한다.

 

 

non-volatile 로 선언된 변수에서 가시성 문제가 발생하는 과정은 다음과 같다.

 

  1. 스레드가 변수 값을 읽으려고 할 때, CPU는 먼저 변수의 값을 CPU 캐시에서 가져온다.

  2. 만약 CPU 캐시에 해당 변수가 없다면, 메인 메모리에서 값을 읽어 CPU 캐시에 저장한다.

  3. 스레드가 변수 값을 변경하면, 변경된 값은 즉시 메인 메모리에 반영되지 않고 CPU 캐시에만 반영될 수 있다.

  4. 다른 스레드는 메인 메모리의 값이나 자신의 CPU 캐시에 있는 값을 참조하기 때문에 최신 변경 내용을 알지 못한다.

 

다음은 non-volatile로 선언된 변수를 사용한 예시 코드이다.

 

class NonVolatileExample {
    private static boolean running = true; // volatile 없이 선언된 변수

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (running) {
                System.out.println("thread 동작중..");
            }
            System.out.println("thread 종료!");
        });

        thread.start();

        Thread.sleep(1000);
        running = false;
        System.out.println("Main 스레드 종료!");
    }
}

 

위의 코드를 volatile을 선언하지 않고 실행하면 무한 루프에 빠지지않고 정상적으로 메인 스레드가 종료된다.

 

 

공유 변수인 running이 non-volatile이지만 가시성 문제가 발생하지 않은 것이다.

 

여기서 주목해야할 점은 non-volatile여도 가시성 문제가 발생하지 않을 수 있다라는 점!

 

 

다음은 공유 변수에 volatile이 선언된 코드이다.

 

class VolatileExample {
    private static volatile boolean running = true; // volatile이 선언된 변수

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (running) {
                System.out.println("thread 동작중..");
            }
            System.out.println("thread 종료!");
        });

        thread.start();

        Thread.sleep(1000);
        running = false;
        System.out.println("Main 스레드 종료!");
    }
}

 

위 코드도 당연히 무한 루프에 빠지지 않고 정상적으로 종료된다.

 

 

 

volatile 원자성 문제

 

그렇다고 해서 멀티 스레드 환경에서 공유 변수에 발생하는 모든 문제를 volatile이 해결해주진 않는다.

 

 

대표적으로 원자성 문제이다.

 

public class VolatileWithAtomicityProblem {
    private static volatile int count;  // volatile 키워드 사용

    public static void main(String[] args) throws InterruptedException {

        int idx = 0;

        while (idx++ < 10000) {
            count = 0;

            // 1000번 증가시키는 스레드 2개 생성
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    count++;
                }
            });

            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    count++;
                }
            });

            t1.start();
            t2.start();

            t1.join();
            t2.join();

            System.out.println("최종 count 값: " + count);
        }
    }
}
[결과]
최종 count 값: 2000
최종 count 값: 2000
최종 count 값: 2000
최종 count 값: 2000
최종 count 값: 1825 -- 원자성 X
최종 count 값: 2000
최종 count 값: 1605 -- 원자성 X
최종 count 값: 2000
...

 

위의 코드는 volatile 키워드가 적용된 공유 변수 count를 Thread1, 2 가 1000번씩 증가시키는 코드이다.

 

 

두 쓰레드가 1000번씩 증가시키기 때문에 우리가 예상하는 결과는 2000이 나와야 한다.

 

 

하지만 결과를 보면 1825나 1605처럼 2000보다 작은 수가 나온다.

 

 

그 이유는 count++ 이라는 연산은 조회 → 연산(수정) → 쓰기 순서로 이루어진다. 만약 본인이 진행한 연산이 다른 스레드에 의해 덮어씌어 버린다면? 위처럼 예상치 못한 결과나 나오는 것이다.

 

 

우리가 아는 원자성은 여러 쓰레드가 동시에 같은 변수에 접근하여 값을 변경할 때, 그 변경이 중간에 다른 쓰레드가 끼어들지 않고 완전하게 이루어지는 특성이다.

 

 

volatile 키워드는 각각의 쓰레드가 작업한 내용을 메인 메모리에 즉시 반영하는 것이지, 다른 쓰레드의 접근까지는 막지 못한다.

 

 

이러한 문제를 극복하기 위해 원자적 연산이 필요하다면 AtomicInteger 또는 synchronized 키워드와 같은 다른 방법을 사용해야 한다.

 

 

 

volatile 성능 문제

 

우리가 CPU 내부의 캐시 메모리를 사용하는 이유는 조회를 더 빠르게 하기 위해서, 불필요한 메인 메모리를 사용한 조회 과정을 줄이기 위해서이다.

 

 

하지만 volatile 키워드를 사용한다면 CPU 캐시 메모리의 장점을 온전히 사용할 수 없다.

 

 

반드시 조회를 위해선 메인 메모리를 거쳐야 하고, 변경 내용도 메인 메모리에 곧바로 반영해야 하기 때문에다.

 

 

이는 애플리케이션의 성능 저하로 이어질 수 있다.

 

 

 

 

volatile 을 사용해야 할 때는?

 

내 생각으론 volatile 키워드는 공유 변수에 여러 쓰레드가 읽기/쓰기 작업을 수행하는 경우는 절대 적절치않다!

 

 

보통 하나의 쓰레드에서만 쓰기 작업을 수행하고, 나머지 쓰레드에선 단순 조회만 하는 상황이 volatile 키워드를 사용하기 적절한 시기가 아닐까 싶다.

'개발 일기' 카테고리의 다른 글

[개발 일기] 2025.01.27 - Nginx (Feat : Apache Web Server)  (0) 2025.01.27
[개발 일기] 2025.01.26 - 논리적 동치성 (Feat : equals())  (1) 2025.01.26
[개발 일기] 2025.01.24 - 낙관적 락 vs 비관적 락  (0) 2025.01.24
[개발 일기] 2025.01.23 - 직렬화  (0) 2025.01.23
[개발 일기] 2025.01.22 - static  (0) 2025.01.22
'개발 일기' 카테고리의 다른 글
  • [개발 일기] 2025.01.27 - Nginx (Feat : Apache Web Server)
  • [개발 일기] 2025.01.26 - 논리적 동치성 (Feat : equals())
  • [개발 일기] 2025.01.24 - 낙관적 락 vs 비관적 락
  • [개발 일기] 2025.01.23 - 직렬화
오도형석
오도형석
  • 오도형석
    형석이의 성장일기
    오도형석
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • MSA 모니터링 서비스
        • DB
      • 스파르타 코딩클럽
        • SQL
        • Spring
      • 백엔드
        • Internet
        • Java
        • DB
      • 캡스톤
        • Django
        • 자연어처리
      • Spring
        • JPA
        • MSA
      • ETC
        • ERROR
      • 개발 일기 N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 인기 글

  • 태그

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
오도형석
[개발 일기] 2025.01.25 - volatile
상단으로

티스토리툴바