Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ Item 78 ] 공유 중인 가변 데이터는 동기화해 사용하라 #78

Open
kmswlee opened this issue Jun 19, 2022 · 0 comments
Open
Assignees
Labels
11장 동시성

Comments

@kmswlee
Copy link

kmswlee commented Jun 19, 2022

공유 중인 가변 데이터는 동기화해 사용하라

  • synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장한다.
  • 많은 프로그래머가 동기화를 배타적 실행이라고 생각한다.
  • 동기화에는 중요한 기능 하나 더 있다. 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수 있다. 동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
  • 자바에서 원자적이란 여러 스레드가 같은 변수를 동기화 없이 수정하는 중이라도, 항상 어떤 스레드가 정상적으로 저장한 값을 온전히 읽어옴을 보장한다는 뜻이다.
  • 그렇다고 '성능을 높이려면 원자적 데이터를 읽고 쓸 때는 동기화하지 말아야겠다'라는 생각을 할수 있는데 위험한 생각이다.
  • 자바 언어는 스레드가 필드를 읽을 때 항상 '수정이 완전히 반영된' 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 '보이는가'는 보장하지 않는다.
  • 동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.
  • 공유중인 가변 데이터를 비록 원자적으로 읽고 쓸 수 있을지라도 동기화에 실패하면 처참한 결과로 이어질 수 있다.
  • Thread.stop 메서드는 안전하지 않아 이미 오래전에 사용 자제 API로 지정됐다
    Thread.stop은 사용하지 말자! (이 메소드는 자바11에서 드디어 제거되었다.)

ex) 잘못된 코드 - 이 프로그램은 영원히 수행 된다.

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;
    }
}
  • 원인은 동기화에 있다.
  • 동기화하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에나 보게 될지 보증할 수 없다.

ex)적절히 동기화해 스레드가 정상 종료된다.

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);
      requestStop();
    }
  }

쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다.

  • 둘중 하나만 동기화해도 동작하는 듯 보이지만, 겉모습에 속으면 안된다.
  • 반복문에서 매번 동기화하는 비용이 크진 않지만 속도가 더 빠른 대안을 소개하겠다.
  • stopRequested 필드를 volatile로 선언하면 동기화를 생략해도 된다.
    volatile 한정자는 배타적 수행과는 상관없지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.

ex) volatile 필드를 사용해 스레드가 정상 종료된다.

public class StopThread {
  private static volatile 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;
    }
}
  • 단, volatile은 주의해서 사용해야 된다.

ex) 잘못된 코드-동기화가 필요하다.

private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
  return nextSerialNumber++;
}
  • 이 메서드의 상태는 nextSerialNumber라는 단 하나의 필드로 결정되는데, 원자적으로 접근할 수 있고 어떤 값이든 허용한다. 따라서 굳이 동기화하지 않더라도 불변식을 보호할 수 있어 보인다.
  • 하지만 이 역시 동기화없이는 올바로 동작하지 않는다.
  • 이유는, 증가 연산자(++)이다. 코드상으로는 하나지만 실제로는 nextSerialNumber필드에 두번 접근한다. 먼저 값을 읽고, 그 다음 새로운 값을 저장하는 것이다. 만약 두 번째 스레드가 이 두 접근 사이를 비집고 들어온다면 첫 번째 스레드와 똑같은 값을 돌려받게 된다.
  • 이런 오류를 `안전 실패(safety failure)라고 한다.

ex) java.util.concurrent.atomic을 이용한 락-프리 동기화

private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
  return nextSerialNum.getAndIncrement();
}
  • 이 방법을 사용하면 우리가 원했던 그 기능을 수행 가능하다.
  • volatile은 동기화의 두 효과중 통신 쪽만 지원하지만 이 패키지는 원자성(배타적 실행)까지 지원한다.

가변 데이터는 단일 스레드에서만 쓰도록 하자.

  • 이번 아이템에서 언급한 문제들을 피하는 가장 좋은 방법은 물론 애초에 가변 데이터를 공유하지 않는것이다.
  • 불변 데이터만 공유하거나 아무것도 공유하지 말자.
  • 한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때는 해당 객체에서 공유하는 부분만 동기화해도 된다.
  • 이런 객체를 사실상 불변(effectively immutable)이라 하고 다른 스레드에 이런 객체를 건네는 행위를 안전 발행(safe publication)이라 한다.

핵심 정리

  • 여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화 해야한다.
  • 동기화 하지 않으면 한 스레드가 수행한 변경을 다른 스레드가 보지 못할 수도 있다.
  • 공유되는 가변 데이터를 동기화하는데 실패하면 응답 불가 상태에 빠지거나 안전 실패로 이어질 수 있다.
  • 배타적 실행은 필요없고 스레드끼리의 통신만 필요하다면 volatile 한정자만으로 동기화할 수 있지만, 올바르게 사용하기 까다롭다.
@kmswlee kmswlee added the 11장 동시성 label Jun 19, 2022
@kmswlee kmswlee self-assigned this Jun 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
11장 동시성
Projects
None yet
Development

No branches or pull requests

1 participant