본문 바로가기
Language/Effective java

동시성

by y.j 2022. 9. 2.
728x90

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

한 메서드가 여러 메서드를 호출할 때 그 메서드가 어떻게 동작하느냐는 클라이언트와의 중요한 계약이다. API문서에 아무런 언급도 없으면 그 클래스 사용자는 나름의 가정을 해야만 한다.

 

멀티스레드 환경에서도 API를 안전하게 사용하게 하려면 클래스가 지원하는 스레드 안전성 수준을 정확히 명시해야 한다.

  • 불변(immutable) : 이 클래스의 인스턴스는 마치 상수같아서 외부 동기화가 필요없다. ( String, Long, BigInteger )
  • 무조건적 스레드 안전 : 인스턴스가 수정 될 수 있으나 내부에서 동기화하여 외부 동기화 없이 사용해도 안전하다. ( AtomicLong, ConcurrentHashMap )
  • 조건부 스레드 안전 : 무조건적 스레드 안전과 같으나 일부 메서드는 외부 동기화가 필요하다. ( Collections.synchronized 래퍼 메서드가 반환하는 컬렉션 )
  • 스레드 안전하지 않음 : 외부 동기화로 감싸더라도 멀티스레드 환경에서 안전하지 않다. 문제를 고쳐 재배포하거나 사용을 자제해야 한다.

 

조건부 안전한 문서화

스레드 안전성은 보통 클래스의 문서화 주석에 기재하지만, 독특한 특성의 메서드라면 해당 메서드의 주석에 기재하도록 하자.

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

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

...

synchronized(m) {  // s가 아닌 m을 사용해 동기화해야 한다.
    for(K key : s)
        key.f();
}

 

비공개 lock이 중요한 이유

  • private final로 락을 선언하면 비공개인 동시에 락 객체가 교체되는 일을 예방해준다.
  • 호출순서에 따른 락이 무엇인지 클라이언트에게 알려줘야 하므로 스레드 안전 클래스에서는 사용 할 수 없다.
  • 상속용 클래스에서는 하위 lock과 상위 lock이 서로 방해할 수 있기 때문에 더 유용하다.
private final Object lock = new Object();

public void foo() {
    synchronized (lock) {
        ...
    }
}

 

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

지연초기화란 필드가 필요할 때까지 초기화를 늦추는 기법이다.

 

 

지연 초기화의 주의할점

  • 클래스 혹은 인스턴스 생성 시의 초기화 비용은 줄지만 지연 초기화 필드에 접근하는 비용은 커진다.
  • 초기화 시점, 비용, 호출 횟수에 따라 성능을 더 느려지게 할 수 있다.

 

지연 초기화가 필요한 시점

필드를 사용하는 인스턴스의 비율이 낮은 반면, 초기화 비용이 클 때 사용하면 좋다. 하지만 정말 이러한 상황인지 적용 전후의 성능을 측정해 보아야 한다.

 

 

대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다. 

 

일반적인 초기화 방법

private final FieldType field = computeFieldValue();

 

지연 초기화 방법

지연 초기화가 조기화 순환성(initialization circularity)을 깨디릴 것 같으면 synchronized를 단 접근자를 사용하자.

private FieldType field;

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

 

정적필드를 지연 초기화

정적 필드 지연 초기화는 지연 초기화 홀더 클래스(lazy initalization holder class) 관용구를 사용하자. 처음 getField를 호출할 때만 field를 초기화하고 그 다음부터는 바로 접근이 되어 성능에 문제가 없다.

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

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

 

인스턴스 필드를 지연 초기화해야 한다면 이중검사 관용구를 사용하라.

한번은 동기화 없이 검사하고 두 번째는 동기화하여 검사한다. 두 번째 검사에도 초기화가 되지 않았다면 초기화한다. 하지만 초기화된 이후로는 동기화하면 안되므로 volatile를 사용해야 한다. 

 

private volatile FieldType field;

private FieldType getField {
    FieldType result = field;
    if(reuslt != field) {
        return field;
    }
    
    synchronized (this) {
        if(field == null)
            field = computeFieldValue();
        return field
    }
}

result를 쓰게 되면 초기화된 상황에서는 그 필드를 한 번만 읽도록 해주는 역할을 한다. 성능을 높여주고 저수준 동시성 프로그래밍에 표준적으로 적용되는 더 우아한 방법이다.

 

단일검사 관용구

필드를 여러번 초기화 및 계산해도 상관없는 경우에 적용 할 수 있다. 속도는 빠르지만 스레드당 최대 한 번 더 이뤄질 수 있다.

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

 

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

운영체제마다 구체적인 스케쥴링 정책이 다르기 때문에 이 정책에 좌지우지 않는 프로그램을 작성해야 한다. 정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램이라면 다른 플랫폼에 이식하기 어렵다.

 

  • 스레드의 평균적인 수를 프로세서 수보다 지나치게 많아지지 않도록 하자
  • 실행준비가 된 스레드들은 맡은 작업을 완료할 때까지 계속 실행되도록 하자
  • 전체 스레드 수와 실행가능한 스레드의 수를 구분해야 한다.
  • 작업이 완료된 후에는 다음 일거리가 생길때까지 대기하도록 하자. 스레드가 당장 처리해야 할 작업이 없다면 실행되어서는 안된다.
  • 작업의 시간을 너무 짧게 분배하면 성능을 떨어뜨릴 수 있다.

 

스레드는 바쁜 대기(busy waiting) 상태가 되면 안된다.

바쁜 대기(busy waiting)이란 스레드가 권한을 얻을때까지 확인하는 것을 의미한다. 실행가능한 스레드를 조사(await)하기 위해서 너무 많은 시간이 걸린다. CountDownLatch와 비교했을 때 약 10배이상 더 소요된다.

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--;
    }
}

 

Thread.yield를 써서 문제를 고쳐보려는 유혹을 떨쳐내자.

Thread.yield는 프로세서가 다른 스레드에게 일을 실행하도록 명령한다. JVM에 상태에 따라 성능이 더 저하될 가능성이 있으며 이식성도 낮다. Thread.yield는 테스트할 수단도 없다.

 

우선순위를 바꾸는 방법도 이식성이 낮고 근본적인 문제 해결을 해주진 못한다.

728x90

'Language > Effective java' 카테고리의 다른 글

직렬화  (0) 2022.09.03
직렬화  (0) 2022.09.03
동시성  (0) 2022.08.27
동시성  (0) 2022.08.24
동시성  (0) 2022.08.22

댓글