[ 이펙티브 자바 ] 아이템 79 과도한 동기화는 피하라

Dayeon myeong·2022년 3월 6일
0

이펙티브자바

목록 보기
13/15

활동성 liveness

liveness 활동성이란 프로그램 또는 객체가 끝까지 멈추지 않고 원하는 결과를 만들어 낼 수 있는지를 의미합니다. 데드락 등이 발생하면 활동성에 큰 제약이 생기는 셈이죠.

liveness failure란 이 활동성이 실패하는 것. 즉, 프로그램 또는 객체의 동작이 중간에 멈추는 것을 얘기합니다.

동기화 메서드나 동기화 블럭에서 제어를 클라이언트에 양도하면 안된다.

liveness failure와 안전 실패(?)를 피하려면 동기화 메서드나 동기화 블럭에서 제어를 클라이언트에 양도하면 안된다. (안전 실패는 데이터 훼손이라고 봐야되나봄)

외계인 메서드는 동기화된 영역에서 수행되는 메서드로 해당 메서드는 무슨 일을 할지 알지 못하며 통제할 수 없다. 이런 메서드는 동기화된 영역을 포함한 클래스 관점에서는 모두 바깥세상에서 온 외계인이다. 예를 들어 동기화된 영역에서 재정의할 수 있는 메서드나 클라이언트가 넘겨준 함수 객체같은 경우를 들 수 있다.

통제할 수 없는 외계인 메서드가 하는 일에 따라 동기화된 영역은 예외를 일으키거나, 교착상태에 빠지거나 (liveness failure), 데이터를 훼손할 수 있다.(safe failure)

동기화 블록 안에서 외계인 메서드 호출

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

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

//동기화 블록 안에서 통제할 수 없는 외계인 메서드를 호출한다.
//observer의 added는 동기화된 영역에서 재정의할 수 있는 메서드로 클라이언트에게 제어권이 있다.
    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);
        return result;
    }
}
@FunctionalInterface
public interface SetObserver<E> {
    // ObservableSet에 원소가 추가되면 호출된다.
    void added(ObservableSet<E> set, E element);
}

public class Main {
    public static void main(String[] args) {
        ObservableSet<Integer> set =
                new ObservableSet<>(new HashSet<>());

        set.addObserver(new SetObserver<Integer>() {
            @Override
            public void added(ObservableSet<Integer> set, Integer element) {
                System.out.println(element);
                if (element == 23)
                    set.removeObserver(this);//문제 발생
            }
        });

        for (int i = 0; i < 100; i++) {
            set.add(i);
        }
    }
}

observer의 added는 동기화된 영역에서 재정의할 수 있는 메서드로 클라이언트에게 제어권이 있다. 그래서 클라이언트는 관찰자를 추가할 때 람다식이나 익명 클래스를 사용하여 added 메서드를 재정의했다. 동기화된 영역에서 사용되는 added 메서드를 동기화 영역 밖에서 재정의한 것이다.

위 코드를 실행하면 set.add()로 0부터 23까지 호출한 후에 concurrentModificationException을 던진다.

ConcurrentModification은 동기화된 컬렉션에 대해서 반복문을 실행하는 도중에 컬렉션 클래스 내부의 값이 변경되는 상황이 포착되면 그 즉시 ConcurrentModificationException이 발생하고 즉시 멈춤 fail fast를 하는 것을 말한다.

List<Widget> widgetList = Collections.synchronizedList(new ArrayList<>());
...
//ConcurrentModificationException이 발생
for (int i = 0; i< widgetList.size(); i++) {
	doSomething(widgetList.get(i));
}

이 말은 즉, set.add()로 0부터 23까지 element를 넣을 때 notifyElementAdded()메서드가 호출된다. 그리고 이 메서드는 반복문을 사용하여 각 관찰자를 순회하며 관찰자의 added 메서드를 호출한다.
그런데 added 메서드는 element가 23일 때 removeObserver 메서드를 호출하며 자기 자신을 넘겨준다.
이미 notifyElementAdded() 함수에서 동기화된 컬렉션 observers에 대해서 반복문을 실행하는 중에 removeObserver로 컬렉션 내부의 값을 삭제 하려하기 때문에 그 즉시 ConcurrentException이 터진다.

notifyElementAdded 메서드에서 수행하는 순회는 동기화 블록 안에 있으므로 동시 수정이 일어나지 않도록 보장하지만, 정작 setObserver 자신이 콜백을 거쳐 되돌아와 수정하는 것까지 막지는 못한다.

락의 재진입(reentrant)

자바 언어의 락은 락의 재진입을 허용하므로 교착상태에 빠지지않는다.

외부 메서드를 호출하는 스레드는 이미 락을 쥐고 있으므로 다음번 락 획득도 성공한다. 그 락이 보호하는 데이터에 대해 개념적으로 관련이 없는 다른 작업이 진행 중이어도 락 획득을 성공하고, 이는 원하지 않는 결과를 유발할 수도 있다.

재진입 가능 락은 객체 지향 멀티스레드 프로그램을 쉽게 구현할 수 있도록 해주지만, 응답 불가(교착상태)가 될 상황을 안전 실패(데이터 훼손)로 변모시킬 수도 있습니다.

문제를 해결하기 위해선 외계인 메서드 호출을 동기화 블록 바깥으로 옮기면된다. notifyElementAdded메서드에서라면 관찰자 리스트를 복사해 쓰면 락 없이도 안전하게 순회할 수 있다.

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

자바의 동시성 컬렉션 CopyOnWriteArrayList가 정확히 이 목적으로 설계된 것이다.

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

CopyOnWriteArrayList

CopyOnWriteArrayList는 병렬성과 동시성을 확보한 클래스이다.

CopyOnWriteArrayList는 변경할 때마다 복사하는 방식을 취해 ConcurrentModificationException이 발생하지 않는다. 이전까지는 다중 연산인 반복문에서 즉시 멈춤으로 인해 ConcurrentModificationException이 터졌다.

변경할 때마다 복사하는 방식은
컬렉션 내용이 벼경될 때마다 복사본을 새로 만들어낸다 즉, 변경할 때마다 새로운 불변객체를 만드는 것이다. 반복문 Iterator를 뽑아내는 시점의 컬렉션 데이터를 기준으로 반복하며 만약 반복하는 동안에 컬렉션에 변경이 일어난다면 반복문과는 상관없는 복사본을 대상으로 변경이 반영되기 때문에 동시 사용에 문제가 없다.

동기화의 기본 규칙

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

동기화 영역 바깥에서 호출되는 외부 메서드를 열린 호출(open call)이라 한다. 외계인 메서드는 얼마나 오래 실행될 지 알 수가 없습니다. 동기화 영역 안에서 호출한다면 그동안 다른 스레드는 보호된 사원을 사용하지 못하고 대기해야만 하는 상황이 발생할 수 있다.따라서 열린 호출 방식은 실패 방지 효과외에도 동시성 효율을 개선시킨다.

기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 해야한다

동기화의 비용

과도한 동기화가 초래하는 진짜 비용은 락을 얻는 데 드는 CPU 시간이 아니다. 진짜 비용은 멀티 코어 시대에서 경쟁하느라 낭비하는 시간, 즉 여러 CPU 코어가 병렬로 실행할 기회를 잃고, 모든 CPU 코어가 메모리를 일관되게 보기 위한 지연 시간이다.

가변 클래스를 동기화하는 방법

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

클래스를 내부에서 동기화하기로 했다면, 락 분할(lock splitting), 락 스트라이핑(lock striping), 비차단 동시성 제어(nonblocking concurrency control) 등 다양한 기법을 동원해 동시성을 높여줄 수 있다.

여러 스레드가 호출할 가능성이 있는 메서드가 정적 필드를 수정한다면 그 필드를 사용하기 전에 반드시 동기화해야 한다.

참고

자바 병렬 프로그래밍

이펙티브 자바

자바봄 블로그
https://javabom.tistory.com/83?category=833277

https://icarus8050.tistory.com/116

https://velog.io/@meme2367/%EB%A9%B4%EC%A0%91-%EC%A4%80%EB%B9%84-%EC%9E%90%EB%B0%94-%EC%BB%AC%EB%A0%89%EC%85%98-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC

profile
부족함을 당당히 마주하는 용기

0개의 댓글