ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Effective Java 3/E 정리 - 11장 동시성
    Study/이펙티브 자바 2021. 8. 11. 19:35
    반응형

     

    스레드는 여러 활동을 동시에 수행할 수 있게 해준다.

    그러나 동시성 프로그래밍은 여러 문제를 고려해야하기 때문에 다루기 힘들다.

    이번 장에서는 이러한 동시성 프로그램을 잘 만들 수 있는 조언을 정리해보자.

     


     

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

    동기화는 배타적 실행과 스레드 사이의 안정적인 통신에 사용된다.

    언어 명세상 long, double 외의 변수를 읽고 쓰는 동작은 원자적(atomic)이다.

    여러 스레드가 같은 변수를 동기화없이 수정하는 중이라도, 항상 어떤 스레드가 정상적으로 저장한 값을 읽어옴을 보장한다.

     

    이를 보고 원자적 데이터를 읽고 쓸 때 동기화를 사용하지 않는다면 위험하다.

    '수정이 완전히 반영된'값을 얻는 다는 것은 보장하지만,

    한 스레드가 저장한 값이 다른 스레드에게 '보이는가'에 대한 가시성은 보장하지 않기 때문이다.

     

    volitile를 사용하면 가장 최근에 기록된 값을 읽어옴을 보장할 수 있으나, 잘못된 결과를 반환할 수 있는 가능성이 있다.

    예를 들어 증가 연산자(++)는 같이 값을 읽고, 증가 시키는 두가지 동작을 수행한다.

    두 동작 사이에 다른 스레드가 값을 읽게되면 잘못된 값을 읽을 수 있으니 주의해야 한다.

    (이 문제 역시 동기화를 하면 해결된다. )

     

    그리고 이 모든 문제들은 가변 데이터를 공유하지 않으면 해결된다.

    가변데이터는 단일 스레드에서만 사용하자.

     

     

    Item79. 과도한 동기화는 피해라

    과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠지거나 예측할 수 없는 동작을 낳기도 한다.

     

    절대로 동기화 메서드나 동기화 블록 안에서는 제어를 클라이언트에 넘기면 안된다.

    예를 들어 재정의할 수 있는 메서드나, 클라이언트가 넘겨준 함수 객체를 호출해선 안된다.

    이런 통제할 수 없는 메서드를 외계인 메서드(alien method)라고 하는데,

    외계인 메서드 호출은 반드시 동기화 블록 바깥에서 이루어져야 한다.

     

    성능 측면에서는 경쟁하느라 낭비하는 시간,

    즉 병렬로 실행할 기회를 잃고 모든 코어가 메모리를 일관되게 보기 위한 지연 시간에 대한 비용이 발생한다.

    또한 가상머신의 코드 최적화를 제한한다는 숨은 비용도 생긴다.

     

    따라서 가변클래스는 다음 두 선택지를 따르자.

    1. 동기화를 전혀 하지말고, 그 클래스르르 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자.
    2. 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자. 단, 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때 선택하자.

     

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

    java.util.concurrent 에서 실행자 프레임워크(Executor Framework) 라는 인터페이스 기반의 유연한 태스크 실행 기능을 제공한다.

     

    1. 작업 큐(work queue) 생성

    ExecutorService exec = Executors.newSingleThreadExecutor();

    2. 실행자에서 태스크 실행

    exec.execute(runnable);

    3. 실행자 종료. (이 작업이 실패하면 VM 자체가 종료되지 않는다.)

    exec.shutdown();

     

    다음은 실행자 서비스의 주요 기능이다.

    • 특정 태스크가 완료되기를 기다린다.
    • 태스크 모음 중 아무것 하나 혹은 모든 태스크가 완료되기를 기다린다.
    • 실행자 서비스가 종료하기를 기다린다.
    • 완료된 태스크들의 결과를 차례로 받는다.
    • 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다.

    Executor Service는 안드로이드할 때 잠깐 봤었는데,

    생성 시기, 재사용에 대한 고려가 중요하다고 했던 것 같다.

    용도에 따라 스레드를 관리해야 하며 Executors 종류는 아래와 같이 간단하게 나눌 수 있다.

     

    Executors 종류

    • cached : 제한을 두지않지 않을때 (많은 요청이 한번에 들어올때)
    • fixed : 고정 개수
    • scheduled: 스케쥴러 사용할 경우 (추가 메서드 제공)
    • single : 1개.. 연속적인 작업을 직렬화 시킬때 → 예를 들면 결제같은....작업의 순서를 보장해야할때

     

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

    wait과 notify 사용하면서 까다로운 부분 동시성 유틸리티들이 잘 커버해준다.

    동시성 유틸리티를 쓰자.

    • 실행자 프레임워크: 이전 아이템 참고
    • 동시성 컬렉션: 표준 컬렉션 인터페이스에 동시성을 구현
    • 동기화 장치: 다른 스레드를 대기시킴

     

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

    모든 클래스는 스레드 안전성 정보를 명확하게 문서화해야한다.

    다음은 스레드 안전성 분류로 안전성이 높은 순이다.

    • 불변(immutable): 이 클래스의 인스턴스는 마치 상수와 같아서 외부 동기화도 필요 없다.
    • 무조건적 스레드 안전(unconditionally thread-safe): 이 클래스의 인스턴스는 수정될 수 있으나, 내부에서 충실히 동기화하여 별도의 외부 동기화 없이 동시에 사용해도 안전하다.
    • 조건부 스레드 안전(conditionally thread-safe): 무조건적 스레드 안전과 같으나, 일부 메서드는 동시에 사용하려면 외부 동기화가 필요하다.
    • 스레드 안전하지 않음(not thread-safe): 이 클래스의 인스턴스는 수정될 수 있다.
    • 스레드 적대적(thread-hostile): 이 클래스는 모든 메서드 호출을 외부 동기화로 감싸더라도 멀티스레드 환경에서 안전하지 않다.

    스레드 안전성 어노테이션을 사용해도 되고,

    조건부 스레드 안전한 스레드의 경우는 어떤 순서로 호출할 때 외부 동기화가 필요한지, 어떤 락을 얻어야하는지 알려줘야 한다.

     

     

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

    지연 초기화는 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다.

    지연 초기화는 필요할 때까지는 하지 않는게 최선이다.

    인스턴스 생성 시의 초기화 비용은 줄어들지만, 그 필드에 접근하는 비용이 커진다.

    결국 초기화가 이뤄지는 비율, 비용, 호출 빈도에 따라 더 성능을 느려지게 할 수 있다.

    그러나 필드를 사용하는 인스턴스의 비율이 낮고 초기화 하는 비용이 크다면 지연 초기화가 적합하다.

    • 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 관용구를 사용하자.
    • 인스턴스 필드를 지연 초기화해야 한다면 이중검사 관용구를 사용하자. → 싱글톤에서 사용한적이 있는듯. DCL...?

     

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

    운영체제의 스레드 스케줄러가 어떤 스레드를 얼마나 오래 실행할지 결정한다.

    따라서 운영체제마다 스케줄링 정책이 달라지기 때문에 정확성이나 성능이 스레드 스케줄러에 따라 달라지도록 구현하면 안된다.

    댓글