티스토리 뷰

728x90

11장. 동시성

이번 장에서는 동시성 프로그램을 명확하고 정확하게 만들 수 있는 방법을 제시한다.

  1. 동기화(Synchronized)란 무엇인가?
  2. 동기화에 대한 흔한 오해
  3. 동기화 적용 방법
  4. volatile 키워드
  5. Java.util.concurrent.atomic 패키지

1. 동기화(Synchronized)란 무엇인가?

멀티 스레드 환경에서 하나의 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장하는 것을 의미한다.
자바에서는 synchronized 키워드를 이용해 동기화를 제공한다.

public synchronized void increase() {
}
synchronized(number) {
}

동기화의 주요 기능

  1. 배타적 실행
    한 스레드가 변경하는 중이라서 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막아준다.
  2. 스레드 사이의 안정적인 통신 ⭐️
    동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수도 있다.
    등기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.

동기화에 대한 흔한 오해

우리는 1번 기능만을 동기화로 오해하는 경우가 많다.

public class StopThread {

    private static boolean stopRequested;

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

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

        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

자바 언어 명세상 long, double 이외의 변수를 읽고 쓰는 동작은 원자적(atomic)이다.
메인 스레드가 1초 후 stopRequestedtrue로 설정하면 백그라운드 스레드는 반복문을 빠져나올 것처럼 보인다.
하지만 응답 불가(liveness failure) 상태에 빠진다.

엥? 왜 안되지?

메인 스레드가 수정한 stopRequested 값을 백그라운드 스레드가 언제 보게 될지 보장할 수 없기 때문이다.
배타적 실행은 보장되지만, 스레드 사이의 안정적인 통신이 보장되지 않고 있다.

  • 멀티 스레드 환경에서는 각 스레드 별로 할당 된 CPU Cache로 데이터를 불러와 사용한다.
  • 각각의 CPU Cache에 저장된 데이터가 다를 수 있기 때문에 값 불일치 문제가 발생할 수 있다.

KakaoTalk_Photo_2022-03-30-14-42-02

동기화를 통해 해결할 수 있다.

또한 OpenJDK와 같은 일부 JVM은 동기화가 없을 경우 다음과 같이 우리의 의도와 다른 최적화(hoisting)를 할 수도 있다.

//원래 코드
while(!stopRequested) {
    i++;
}

// 최적화한 코드
if(!stopRequested) {
    while(true) {
        i++;
    }
}

동기화 적용 방법

스레드 사이의 안정적인 통신을 위해 동기화를 적용한다.

public class StopThread {

    private static boolean stopRequested;

    private static synchronized void requestStop() {
        stopRequested = true;
    }

    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

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

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

        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested();
    }
}

기대한 대로 1초 후 종료된다.
쓰기 메서드(requestStop)와 읽기 메서드(stopRequested) 모두를 동기화해야한다.
쓰기 메서드만 동기화해서는 정상적인 동작을 보장할 수 없다.

반복문에서 매번 동기화하는 비용이 크진 않지만 volatile 키워드를 이용하면 성능을 개선할 수 있다.

volatile 키워드

자바 변수를 메인 메모리에 저장하겠다라고 명시해주는 것이다.
CPU Cache가 아닌 메인 메모리의 데이터를 직접적으로 사용(읽기/쓰기)한다.
따라서 동기화는 아니지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.
IMG_6323

public class StopThread {

    private static volatile boolean stopRequested;

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

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

        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

✅ 2번 기능(안정적 통신)만 필요하다면 volatile 키워드만으로 동기화 효과를 낼 수 있다.

  • long, double을 제외한 기본타입은 volitile 키워드를 사용하면 동기화를 생략해도 된다.

volatile 키워드 주의점

private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
    return nextSerialNumber++;
}

정상적으로 실행될 것 같지만 잘못된 결과를 계산하게 되는 안전 실패(safety failture)가 발생한다.
증감연산자는 실행 시 다음과 같이 nextSerialNumber에 두번 접근하게 되므로, 원자적으로 실행되지 않는다.

nextSerialNumber = nextSerialNumber + 1;

동기화로 해결 가능

private static int nextSerialNumber = 0;

public static synchronized int generateSerialNumber() {
    return nextSerialNumber++;
}

synchronized 키워드를 붙였다면 volitale 키워드는 제거해야 한다.
이 메서드를 더 견고하게 하려면 int 대신 long을 사용하거나 nextSerialNumber가 최댓값에 도달하면 예외를 던지게 하자.

long, double 동기화를 위해 제공되는 라이브러리

아이템 59. 라이브러리를 익히고 사용하라

Java.util.concurrent.atomic 패키지는 AtomicLong, AtomicDouble을 제공한다.
이 패키지에는 락 없이도(lock-free) 스레드 안전한 프로그래밍을 지원하는 클래스들이 담겨 있다.

private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}
  • syncronized 키워드를 이용한 동기화보다 우수한 성능을 제공한다.

결론

이러한 문제들을 피하는 가장 좋은 방법은 가변 데이터를 공유하지 않는것이다.
✅ 가변 데이터는 단일 스레드에서만 쓰도록 하자.
✅ 가변 데이터를 공유해야한다면 무조건 동기화를 해야 한다.
불변 데이터(아이템 17) 만 공유하거나 아무것도 공유하지 말자.

Ref.

728x90
댓글
공지사항
최근에 올라온 글