본문 바로가기

STUDY REVIEW/이펙티브 자바 독서스터디

[EFFECTIVE JAVA] 이펙티브 자바 독서스터디 - 11장 동시성

11장 동시성
아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라
아이템 79. 과도한 동기화는 피하라
아이템 80. 스레드보다는 실행자, 태스크, 스트림을 애용하라
아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라
아이템 82. 스레드 안전성 수준을 문서화하라
아이템 83. 지연 초기화는 신중히 사용하라
아이템 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라

 

읽고 느낀 점

스레드는 멀티코어 프로세서의 힘을 제대로 활용하려면 반드시 내 것으로 만들어야 하는 지식이다.

스레드부터 공부하다보면 이해하지 않을까?


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

 

동기화는 배타적 실행뿐만 아니라 스레드 사이의 안정적인 통신에도 필요하다.

단, Thread.stop 은 사용하지 말자.

이 메서드를 사용하면 데이터가 훼손될 수 있다고 한다.

여러 스레드가 가변하는 데이터를 사용한다면 읽고 쓰는 동작은 반드시 동기화를 해야한다.


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

 

Java.util.concurrent 패키지의 CopyOnWriteArrayList를 사용한 예제

private final List<SetObserser<E>> observers = new CopyOnWriteArrayList<>();

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

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

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

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

 

책에나오는 실행자 서비스의 예시

  • 특정 태스크가 완료되기를 기다린다 (코드 79-2 get 메서드)
 @DisplayName("하나의 worker를 가지는 쓰레드풀")
    @Test
    void single() throws ExecutionException, InterruptedException {
        ExecutorService exec = Executors.newSingleThreadExecutor();

        exec.submit(() -> {
            System.out.println("First");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).get();
        exec.execute(() -> System.out.println("Second"));
    }
  • 태스크 모음 중 아무것 하나 혹은 모든 태스크가 완료되기를 기다린다.
@DisplayName("태스크 모음 중 아무것 하나 혹은 모든 태스크가 완료되기를 기다린다.")
    @Test
    void any() throws InterruptedException, ExecutionException {
        ExecutorService exec = Executors.newFixedThreadPool(3);

        List<Future<String>> returnStr = exec.invokeAll(tasks());
        System.out.println(returnStr.get(0).get()); // FIRST
        System.out.println(returnStr.get(1).get()); // SECOND
        System.out.println(returnStr.get(2).get()); // THRID
    }

    List<Callable<String>> tasks() {
        return Arrays.asList(() -> {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return "FIRST";
                },
                () -> {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return "SECOND";
                },
                () -> "THIRD");
    }
@DisplayName("태스크 모음 중 아무것 하나 혹은 모든 태스크가 완료되기를 기다린다.")
    @Test
    void any() throws InterruptedException, ExecutionException {
        ExecutorService exec = Executors.newFixedThreadPool(3);

        System.out.println(exec.invokeAny(tasks())); // THRID 출력 후 
    }

    List<Callable<String>> tasks() {
        return Arrays.asList(() -> {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace(); // Interrupt
                    }
                    return "FIRST";
                },
                () -> {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace(); // Interrupt
                    }
                    return "SECOND";
                },
                () -> "THIRD");
    }
  •  실행자 서비스가 종료하기를 기다린다
@DisplayName("실행자 서비스가 종료하기를 기다린다")
    @Test
    void waitForTerminate() throws InterruptedException {
        ExecutorService exec = Executors.newSingleThreadExecutor();

        exec.execute(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("TEST"); // 출력안된다
        });

        exec.awaitTermination(2000, TimeUnit.MILLISECONDS);
    }

@DisplayName("실행자 서비스가 종료하기를 기다린다")
    @Test
    void waitForTerminate() throws InterruptedException {
        ExecutorService exec = Executors.newSingleThreadExecutor();

        exec.execute(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("TEST"); // 출력된다
        });

        exec.awaitTermination(3000, TimeUnit.MILLISECONDS);
    }
  •  완료된 태스크들의 결과를 차례로 받는다
    @DisplayName("완료된 태스크들의 결과를 차례로 받는다")
    @Test
    void sequence() throws ExecutionException, InterruptedException {
        CompletionService<String> cs = new ExecutorCompletionService<>(Executors.newFixedThreadPool(3));
        List<Callable<String>> tasks = tasks();
        tasks.forEach(cs::submit);
        for (int i = tasks.size(); i > 0; i--) {
            String r = cs.take().get();
            if (r != null)
                System.out.println(r); // THRID, SECOND, FISRT 순서로 출력
        }
    }
  •  테스크를 특정 시간에 혹은 주기적으로 실행하게 한다.
@DisplayName("테스크를 특정 시간에 혹은 주기적으로 실행하게 한다.")
    @Test
    void schedule() throws InterruptedException {
        ScheduledExecutorService exc = Executors.newSingleThreadScheduledExecutor();

        exc.schedule(() -> System.out.println("Hello"), 3000, TimeUnit.MILLISECONDS);
        exc.scheduleAtFixedRate(() -> System.out.println("Hello!"), 3000, 3000, TimeUnit.MILLISECONDS); /// 3번출력
        Thread.sleep(10000);
    }

https://javabom.tistory.com/84

 

[아이템 80] 스레드보다는 실행자, 태스크, 스트림을 애용하라

동시성 작업을 할 때는 작업 큐를 직접 생성할 수도 있겠지만 복잡한 작업들(안전실패, 응답불가 예방)이 필요하다. 때문에 java.util.concurrent 패키지의 실행자, 태스크, 스트림을 이용하는 편이 더

javabom.tistory.com


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

 

새로 코드를 작성한다면 wait와 notify를 사용하지 말고, 이를 사용하는 레거시 코드를 유지보수한다면.

wait는 while문 안에서 사용하고, notify 보다는 notifyAll을 사용하자.


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


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

 

지연 초기화가 초기 순환성을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용하자.

private static class FieldHolder {
  static final FieldType field = computeFieldValue();
}

private static FieldType getField() { return FieldHolder.field; }

성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 관용구를 사용하자.

private FieldType field2;
private synchronized FieldType getField2() {
  if (field2 == null)
    field2 = computeFieldValue();
  return field2;
}

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

 

스레드는 당장 처리해야 할 작업이 없다면 실행해서는 안된다.

응답 불가 문제를 스레드 우선순위로 해결하는 시도는 하지말자.