본문 바로가기

컴퓨터/JAVA

자바 고급 스터디 12 . 동시성 문제

반응형

자바 고급 스터디 목록

공부 시간 : 2021/05/29 19:50 ~ 22:40

11주차. 동시성 문제

동시성 프로그래밍에서 발생할 수 있는 문제점

CPU 가 어떠한 작업을 위해 RAM에 저장되어 있는 일부분을 CPU cache memory로 읽어들인다. 작업을 수행하고 나면 CPU cache memory에서 ram으로 데이터를 쓰게된다. 하지만, 이러한
과정에서 CPU 작업이 끝난 직후 ram에 데이터를 쓰는 것은 아니다. (가시성 문제)

위 이유와 함께 ram의 데이터를 cpu core1과 core2 에서 동시에 읽고, ram에 쓰는 시점은 다를 때(동시 접근 문제) 발생하게 되는 문제를 동시성(병렬성) 문제라고 한다.

public class Thread {

    private static boolean button;

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

        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!button)
                i++;
        });

        backgroundThread.start();
        Thread.sleep(1000);
        button = true;
    }
}

위 코드가 어떻게 작동할지 생각해보자. 메인 쓰레드에서 button을 true로 수정하는 즉시 backgroundThread 에서는 while문을 탈출 하게 될 것처럼 보인다. 하지만, while문을 탈출 한다고
보장할 수 없다.

좀 더 상세한 작동원리를 살펴보면 아래와 같다.

  1. 메인쓰레드와 백그라운드쓰레드 각각의 cpu cache memory에 RAM에 저장되어 있는 button 변수를 읽어온다.
  2. 메인쓰레드의 cpu cache memory에 있는 button 변수는 true로 수정되었지만, ram에 언제 쓰여질지 모른다.
  3. 또한 백그라운드쓰레드가 ram에서 업데이트된 button 변수를 어떤 시점에 읽어올지 모른다.

그렇기 때문에, 위 상황에서 while문을 탈출하는 것을 보장 할 수 없다.

이미지
이미지

디버깅 결과 while을 탈출할때 i 값이 일정하지 않은 것을 확인 할 수 있다. 이것 과 별개로 실행을 하면 위 코드를 실행하면 무한루프에 빠지기 때문에 10분이 지나도 출력이 되지 않는데, 디버깅시에는 바로 브레이크포인트 지점에서 멈추게 된다. 왜 이렇게
되는지는 모르겠다..

이미지

while문 안에 println을 넣으니 귀신같이 while문을 탈출하는 것을 볼 수 있다. 오랜시간 찾아본 결과 메모리장벽 이라는 녀석 때문에 발생하는 현상이다.

이미지

println 코드를 까보니 synchronized 키워드가 붙어있고, synchronized는 메모리 장벽을 세울수 있는 키워드라고 한다. synchronized 가 붙은 구문이 시작할때, 즉 메모리장벽을 만나기
전 cpu cache memory 와 ram의 데이터를 동기화시키는 과정을 거치게 된다. 항상 동기화하지 않는 이유가 있는데 이 과정에 소모되는 자원이 크기 때문이다.

그래서, 위 상황을 다시 생각해보면 while 문안에 i++ 만 있을때는 synchronized 키워드가 포함되어 있지 않아 동기화가 일어나지 않고 backgroundThread의 cpu cache memory에는
button이 항상 false로 유지되었고, while문을 탈출하지 못하고 무한루프에 빠지게 된다.

그런데, 왜?? 디버깅시에는 while 문을 탈출하는지는 아직 이유를 찾지 못했다. 추측하기로는 디버깅시에 지속적으로 메모리 동기화과정을 거치는 것 같다.

button 변수를 public static volatile boolean button; 와 같이 volatile를 붙혀 선언하면 CPU cache memory를 거치지 않고 ram에서 데이터를 읽고 쓰기 때문에
가시성 문제를 해결 할 수 있다. 실행결과도 정상으로 나오는 것을 확인 할 수 있다.

public class example {

    public static int value;

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

        Thread backgroundThread = new Thread(() -> {
            for (int i = 0; i < 100000; i++)
                value++;
        });

        backgroundThread.start();

        for (int i = 0; i < 100000; i++)
            value++;

        Thread.sleep(1000);
        System.out.println("value = " + value);
    }
}

위 코드를 실행시켜보면서 1초 후에 200,000이 출력될 것으로 기대했다. 하지만 128819, 120331, 125338 등으로 계속해서 기대한 값보다 작은 값이 출력되었다.
이러한 문제가 발생하는 이유는 mainThread와 backgroundThread에서 동시에 value 값을 변화시켰기 때문이다.

public static synchronized void add() {
        value++;
        }

value++; 부분을 synchronized 키워드가 붙은 add() 함수로 바꿔주게되면 기대했던 200,000이 출력된다.

그렇다면,

public class example {

    public static int value;

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

        Thread backgroundThread = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                ++value;
                System.out.println("value = " + value);
            }
        });

        backgroundThread.start();

        for (int i = 0; i < 100000; i++) {
            ++value;
            System.out.println("value = " + value);
        }

        Thread.sleep(1000);
        System.out.println("end");
    }
}

이런식으로 synchronized 블럭이 포함된 println 함수를 계속해서 출력해주면 200,000이 출력될지 궁금해졌다.
결과는 200,000보다 조금 작은수를 출력하는 것을 확인 할 수 있다. 쓰레드내에서 synchronized 블럭으로 묶인 부분을 만나면 해당 쓰레드가 점유하고 있는 cpu cache memory와 ram의 동기화를 수행하지만
다른 쓰레드와의 동기화를 보장하지는 못하는 듯하다.

이미지

좀 더 찾아보니 synchronized() 에서 매개변수로 받은 인스턴스를 기준으로 동기화를 진행한다고 하고, 메서드에 synchronized 키워드가 붙은 경우
synchronized(this) 와 같은 의미라고 할 수 있다. 즉, public static synchronized void add() 에서는 synchronized의 기준이 value가 포함된 인스턴스이기 때문에 200,000이 정상적으로 출력되었으며,
println() 이 포함된 구문에서는 println이 실행되고 있는 쓰레드를 기준으로 동기화를 진행하여 200,000보다 작은 값이 나온 것으로 예상된다. (동기화가 되는 짧은 순간에 다른 쓰레드에서 값을 수정한 것으로 생각됨.)

참고 사이트 :

반응형