[이펙티브 자바] 11장 - Concurrency

couque·2022년 9월 1일
0

[아이템78] 공유 중인 가변 데이터는 동기화해라

  • 시작하기에 앞서 동시성병렬성의 차이는 무엇일까?
  • 동시성 : 동시에 실행되는 것처럼 보이는 것 (논리적 개념)
    • 싱글 코어에서 멀티 스레드 작동
  • 병렬성 : 실제로 동시에 여러 작업이 수행되는 것 (물리적 개념)
    • 멀티 코어에서 멀티 스레드 동작

1. Synchronized

  • 메서드, 블록 내에서 한 번에 한 스레드만 수행하도록 보장
  • Synchronized는 아래 두 가지를 보장한다.

배타적 실행

  • 배타적 실행: A 스레드에 의해 객체가 수정되는 동안 B 스레드가 일관성 없는 상태로 객체를 보는 것을 방지하는 것이다.
  • 즉, 을 사용하여 공유하는 자원은 현재 실행 중인 스레드만 접근하게 하는 것이다.

가시성을 바탕으로 안정적인 통신 보장

  • 다른 스레드에서도 항상 가장 최신의 값을 조회할 수 있다.

2. 다른 스레드를 멈추는 올바른 방법

  • Thread.stop() 메서드는 안전하지 않아 사용되지 않는다.

잘못된 예시

// 영원히 수행된다
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;
    }}
  • vm의 hoisting 최적화에 의해 아래와 같이 최적화된다.
// 원래 코드
while (!stopRequested)
    i++;
 // 최적화한 코드
if (!stopRequested)
    while (true)
        i++;

올바른 예시

public class SyncStopThread {
    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로 선언하면 synchronized를 생략할 수 있다.

3. volatile 키워드

  • synchronized보다 속도가 더 빠른 대안이다.
  • 가시성을 보장하는 키워드이다. (동기화는 보장되지 않는다.)
private static volatile int nextSerialNumber = 0;
 public static int generateSerialNumber() {
    return nextSerialNumber++; 
}
  • nextSerialNumber에서
    (1) 값을 읽고 -> (2) 1을 증가시키고 -> (3) 값을 저장
    세 과정 사이에 다른 스레드가 끼어 배타적 실행이 보장되지 않을 수 있다.

개선하는 방법

  • synchronized 함수로 사용하기
  • AtomicLong 사용

4. Atomic

  • lock-free이기 때문에 스레드 간 안정적인 통신을 보장하고 원자성을 지원한다.
  • lock이 없어 성능이 우수하다.
private static volatile int nextSerialNumber = 0;
 public static int generateSerialNumber() {
    return nextSerialNumber++;
}

안정적인 통신 보장?

  • AtomicLong의 내부 구현은 다음과 같다.
public class AtomicLong extends Number implements java.io.Serializable {
    private volatile long value;
}
  • volatile 키워드를 사용하여 value의 가시성을 보장한다.

어떻게 lock-free를 가능하게 하는가?

  • AtomicInteger의 getAndIncresement 함수의 내부 구현이다.
  • CAS : Compare And Swap을 통해 가능하게 한다.
 @HotSpotIntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }
  • weakCompareAndSetInt 은 unSafe class의 compareAndSetInt를 호출한다.
  • 이는 CAS를 기반으로 하는데 멀티 스레드 환경에서 직렬화를 이루기 위한 원자적인 연산이다. 이전값과 새로운 값을 넘겨 이전값과 현재 메모리의 값이 같은 경우에 새로운 값을 변경하고 true를 반환한다.
  • weakCompareAndSetInt을 반복 수행하여 값을 변경한 뒤 getIntVolatile으로 값을 조회해 반환하는 것이다.

| 참고

  • 참고로 자바 언어 명세 상 long, double을 제외한 변수를 읽고 쓰는 동작은 atomic하다.
  • 여러 스레드가 같은 변수를 동기화없이 수정하더라도, 어떤 스레드가 저장한 값을 온전히 읽어온다.
    • 자바 언어 명세 상, 스레드가 필드를 읽을 때 항상 수정이 완전히 반영된 값을 얻는다고 보장하지만,
    • 저장한 값이 다른 스레드에게 보이는지는 보장하지 않기 때문에 동기화가 필요하다.
  • 이로 인해 AtomicInteger, AtomicLong의 set 함수 간의 차이가 발생한다.
   // AtomicLong
   public final void set(long newValue) {
        // See JDK-8180620: Clarify VarHandle mixed-access subtleties
        U.putLongVolatile(this, VALUE, newValue);
    }
    // AtomicInteger
    public final void set(int newValue) {
        value = newValue;
    }

[아이템79] 과도한 동기화를 피하라

  • 과도한 동기화는 성능을 저하시키고
  • 데드락에 빠뜨리고
  • 예측불가한 동작을 발생시킨다.

0개의 댓글