(Java) Thread와 메모리 가시성

BaekGwa·2024년 8월 16일
0

✔️ Java

목록 보기
2/12

Thread와 메모리 가시성

메모리 가시성

메모리 가시성 문제란?

메모리 가시성 문제는 멀티스레드 환경에서 한 스레드가 변경한 변수의 값이 다른 스레드에게 즉시 또는 올바르게 보이지 않는 상황을 말합니다.

  • 즉, A Thread에서 공유되는 자원인 value의 값을 변경하였고, 이후 B Thread 에서 접근하여 확인하는데, 변경값이 적용이 안되어있는 현상을 얘기합니다.
  • 같은 변수를 사용하는데, 다른 값이 나오면 데이터 정합성문제가 발생 할 듯 합니다.

왜 발생할까?

Java 에서는 각 스레드의 성능 향상을 위해, 자주 사용하는 변수는 CPU 캐시에 저장 하게 됩니다.

  • 쉽게 이해하자면, Thread A, Thread B가 각자 같은 변수를 읽어 오거나, 값을 변경 시키더라도, 자신만 사용하는 CPU 캐시에 이 값을 저장해두고 사용하게 되는 겁니다.

실제 코드와 결과를 통해 알아 보겠습니다.

발생 코드 살펴보기

  • PlsuThread는 생성자로 입력받은 숫자 만큼, 반복문을 돌며 값을 더합니다.
  • 저장되는 변수는 static 변수 plusCount에 저장됩니다.
  • static 으로 선언 되었기때문에, 해당 class로 여러 Thread 를 만들어도 같은 변수를 참조하게 됩니다.
package memory;

public class TestMain {

    public static void main(String[] args) {
        PlusThread plusThreadA = new PlusThread();

        Thread threadA = new Thread(plusThreadA, "ThreadA");

        threadA.start();
        System.out.println(Thread.currentThread().getName() + " is start!!");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //실행 Flag false처리 후, 값 변경 되었는지 확인.
        plusThreadA.runFlag = false;
        System.out.println("plusThreadA.isRunFlag() = " + plusThreadA.isRunFlag());
        
        System.out.println(Thread.currentThread().getName() + " is End!!");
    }

    private static class PlusThread implements Runnable {
        boolean runFlag = true;

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "시작");
            while (runFlag){
//                System.out.println(Thread.currentThread().getName() + "동작 중");
            }
            System.out.println(Thread.currentThread().getName() + "종료");
        }

        public boolean isRunFlag() {
            return runFlag;
        }
    }
}
  • ThreadA는 flag가 살아있는 동안 (true) 계속 Thread를 유지하고 있는 상태입니다.
  • ThreadA는 시작할때, 시작 로그를 찍고, 끝날때 종료 로그를 찍습니다.
  • 예상하기로는, main Thread에서 ThreadArunFlag를 false로 바꿨을 때, while문의 조건이 false가 되며 종료가 되어야 한다.
  • 로그상으로는 다음과 같은 순서를 가질 것으로 예상한다.
    main is start!!
    ThreadA시작
    plusThreadA.isRunFlag() = false
    main is End!!
    ThreadB종료

하지만 실제 동작은, ThreadA가 종료되지 않고 무한 대기 실행 상태에 빠지게 된다.

  • 이때, 이유는 앞서 설명한 메모리가시성문제가 발생되어 그렇다.
  • ThreadA 내부에서 참조해서 확인하는 runFlag메인 메모리에 있는 인스턴스의 runFlag가 아닌, CPU의 캐시에 저장된 runFlag 값을 사용해서 진행하게 된다.
  • 이때 CPU메모리(runFlag)는 메인메모리runFlag = true 값이 아직 반영되지 않은 것이다.
    이와 같은 문제를 어떻게 해결 할 수 있을까?

메모리 가시성 해결

volatile 사용

  • 이와 같은 문제는 volatile 키워드를 사용하면 간단하게 해결된다.
  • 다음 코드와 같이 volatile 키워드를 사용하면, 해당 키워드가 적용된 변수는, CPU 캐시를 사용하지 않고, 메인 메모리만을 사용하여 값을 가져오고, 저장하게 된다.
~~
private static class PlusThread implements Runnable {
        volatile boolean runFlag = true; 
~~
ThreadA시작
main is start!!
ThreadA종료
plusThreadA.isRunFlag() = false
main is End!!

volatile 문제점

  • 앞서 설명하였듯이, volatile 키워드를 사용한 변수는, CPU 캐시를 사용하지 않고, 메인 메모리만을 사용하여 값을 사용하기에 성능적으로 확실하게 느려진다.

그럼 CPU 캐시는 언제 메인메모리로 동기화 될까?

답은, 알 수 없다.

  • 사실 동기화는 여러 상황일 때, 시도하는데, 대표적으로 Thread가 휴식 상태일때 진행 할 수 도 있다.
  • 예를들어, 현재 while문 안에 콘솔출력 구문을 주석해제 해서 실행하게 되면, 콘솔 출력을 하게 되는데, 이때 스레드는 잠시 쉬며 대기를 하게 된다.
  • 이때 스레드는 휴식 상태에서 동기화를 진행 할 수도 있다.
  • 중요한 키 포인트는 진행 할 수도 있다 이다. 동기화를 할 수도 안할 수도 있기때문에 volatile을 대신해서 이런 기법을 사용한다면 보장되지 않는 멀티스레드 환경을 구축하는 것이다.
profile
현재 블로그 이전 중입니다. https://blog.baekgwa.site/

0개의 댓글