EffectiveJava

[EffectiveJava] 11장 동시성

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

// 78-1 잘못된 코드 - 이 프로그램은 얼마나 오래 실행될까?
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;
}
// 78-2 적절히 동기화해 스레드가 정상 종료한다.
public class StopTread{
	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();**
	}
}
// 78-3 volatile 필드를 사용해 스레드가 정상 종료한다.
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;
    }
}
// 78-4 잘못된 코드 - 동기화가 필요하다.
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
    return nextSerialNumber++;
}
// 78-5 java.util.concurrent.atomic을 이용한 락-프리 동기화
private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}

79. 과도한 동기화는 피하라

// 79-1 잘못된 코드. 동기화 블록 안에서 외계인 메서드를 호출한다.
public class ObservableSet<E> extends ForwardingSet<E> {
    public ObservableSet(Set<E> set) {
        super(set);
    }

    private final List<SetObserver<E>> observers = new ArrayList<>();

    public void addObserver(SetObserver<E> observer) {
        synchronized(observers) {
            observers.add(observer);
        }
    }

    public boolean removeObserver(SetObserver<E> observer) {
        synchronized(observers) {
            return observers.remove(observer);
        }
    }

    private void notifyElementAdded(E element) {
        synchronized(observers) {
            for (SetObserver<E> observer : observers)
                **observer.added(this, element);**
        }
    }

    @Override public boolean add(E element) {
        boolean added = super.add(element);
        if (added)
            notifyElementAdded(element);
        return added;
    }

    @Override public boolean addAll(Collection<? extends E> c) {
        boolean result = false;
        for (E element : c)
            result |= add(element); // notifyElementAdded를 호출한다.
        return result;
    }
}
@FunctionalInterface
public interface SetObserver<E> {
    // ObservableSet에 원소가 추가되면 호출된다.
    void added(ObservableSet<E> set, E element);
}
public static void main(String[] args) {
		ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
		set.addObserver((s,e)-> System.out.println(e));
		
		for(int i=0; i<100; i++)
			set.add(i);
}
set.addObserver(new SetObserver<>() {
    public void added(ObservableSet<Integer> s, Integer e) {
        System.out.println(e);
        **if ( e == 23)**
            **s.removeObserver(this);**
    }
});
// 79-2 쓸데없이 백그라운드 스레드를 사용하는 관찰자
set.addObserver(new SetObserver<>() {
    public void added(ObservableSet<Integer> s, Integer e) {
        System.out.println(e);
        if ( e == 23) {
            ExecutorService exec = Execitors.newSingleThreadExecutor();
            try {
                exec.submit(() -> s.removeObserver(this)).get();
            } catch (ExecutionException | InterruptedException ex) {
                throw new AssertionError(ex);
            } finally {
                exec.shutdown();
            }
    }
}
});
// 79-3 외계인 메서드를 동기화 블록 바깥으로 옮겼다.
private void notifyElementAdded(E element) {
    List<SetObserver<E>> snapshot = null;
    synchronized (observers) {
        snapshot = new ArrayList<>(observers);
    }
    for (SetObserver<E> observer : snapshot) {
        observer.added(this, element);
    }
}
// 79-4 CopyOnWriteArrayList를 사용해 구현한 스레드 안전하고 관찰 가능한 집합
private final List<SetObserver<E>> observers = 
									new CopyOnWriteArrayList<>();

public void addObserver(SetObserver<E> observer) {
    observers.add(observer);
}

public boolean removeObserver(SetObserver<E> observer) {
    return observers.remove(observer);
}

private void notifyElementAdded(E element) {
    for (SetObserver<E> observer : observers)
        observer.added(this, element);
}

80. 스레드보다는 실행자, 태스크, 스트림을 애용하라

ExecutorService exec = Executors.newSingleThreadExecutor(); // 작업 큐를 생성하는 방법
exec.execute(runnable); // 실행자에 실행할 테스트를 넘기는 방법
exec.shutdown(); // 우하하게 종료시키는 방법

81. wait와 notify보다는 동시성 유틸리티를 애용하라

// 81-1 ConcurrentMap으로 구현한 동시성 정규화 맵 - 최적은 아니다.
private static final ConcurrentMap<String, String> map = 
				new ConcurrentHashMap<>();

public static String intern(String s) {
    String previousValue = map.puIfAbsent(s, s);
    return previousValue == null ? s : previousValue;
}
// 81-2 ConcurrentMap으로 구현한 동시성 정규화 맵 - 더 빠르다!
public static String intern(String s) {
    String result = map.get(s);
    if (result == null) {
        result = map.puIfAbsent(s, s);
        if(result == null) 
            result = s;
    }
		return result;
}
// 81-3 동시 실행 시간을 재는 간단한 프레임워크
public static long time(Executor executor, int concurrency, Runnable action) throws InterruptedException {
      CountDownLatch ready = new CountDownLatch(concurrency);
      CountDownLatch start = new CountDownLatch(1);
      CountDownLatch done = new CountDownLatch(concurrency);

      for (int i = 0; i < concurrency; i++) {
					executor.execute(() -> {
							// 타이머에게 준비를 마쳤음을 알린다.
							ready.countDown(); 
							try {
									// 모든 작업자 스레드가 준비될 때까지 기다린다.
									start.await(); 
									action.run();
							} catch (InterruptedException e) {
									Thread.currentThread().interrupt();
							} finally {
									// 타이머에게 작업을 마쳤음을 알린다.
									done.countDown(); 
							}
          });
      }

      ready.await(); // 모든 작업자가 준비될 때까지 기다린다.
      long startNanos = System.nanoTime();
      start.countDown(); // 작업자들을 깨운다.
      done.await(); // 모든 작업자가 일을 끝마치기를 기다린다.
      return System.nanoTime() - startNanos;

  }
// 81-4 wait 메서드를 사용하는 표준 방식
synchronized (obj){
  while(<조건이 충족되지 않았다>)
      obj.wait(); // 락을 걸어 놓고, 깨어나면 다시 잡는다.
  
  ...// 조건이 충족됐을 때의 동작을 수행한다.
}

82. 스레드 안전성 수준을 문서화하라

// synchronizedMap이 반환한 맵의 컬렉션 뷰를 순회하려면 
// 반드시 그 맵을 락으로 사용해 수동으로 동기화 하라.

Map<K, V> m = Collections.synchronizedMap(new HashMap<>());
Set<K> s = m.keySet(); // 동기화 블록 밖에 있어도 된다.

synchronized(m) { // s가 아닌 m을 사용해 동기화해야 한다!
    for (K key : s) 
        key.f();
}
// 이대로 따르지 않으면 동작을 예츨할 수 없다.
// 82-1 비공개 락 객체 관용구 - 서비스 거부 공격을 막아준다.
private final Object lock = new Object();
public void foo() {
  synchronized(lock) {
      ...
  }
}

83. 지연 초기화는 신중히 사용하라

// 83-1 인스턴스 필드를 초기화하는 일반적인 기법
private final FieldType field = computeFieldValue();
// 83-2 인스턴스 필드의 지연 초기화 - synchronized 접근자 방식
private FieldType field;

private synchronized FieldType getField() {
    if (field == null) 
        field = computeFieldValue();
    return field;
}
// 83-3 정적 필드용 지연 초기화 홀더 클래스 관용구
// 성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구를 사용하자
private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}

private static FieldType getField() { return FieldHolder.field; }
// 83-4 인스턴스 필드 지연 초기화용 이중검사 관용구
private volatile FieldType field;
private FieldType getField() {
    FieldType result = field;
    if (result != null) { // 첫 번째 검사 (락 사용 안함)
        return result;

    synchronized(this) {
        if (field == null) // 두 번째 검사(락 사용)
            field = computeFieldValue();
        return field;
    }
}
//83-5 단일검사 관용구 - 초기화가 중복해서 일어날 수 있다.
private volatile FieldType field;

private FieldType getField() {
  FieldType result = field;
  if (result == null)
    field5 = result = computeFieldValue();
  return result;
}

84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라

// 84-1 끔찍한 CountDownLatch 구현 - 바쁜 대기 버전! (447쪽)
public class SlowCountDownLatch {

  private int count;

  public SlowCountDownLatch(int count) {
    if (count < 0) {
      throw new IllegalArgumentException(count + " < 0");
    }
    this.count = count;
  }

  public void await() {
    **while (true) {
      synchronized (this) {
        if (count == 0) {
          return;
        }
      }
    }**
  }

  public synchronized void countDown() {
    if (count != 0) {
      count--;
    }
  }
}

Leave a Reply