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

📌 1. 발표 전 알아야 할 개념

아이템 78의 스레드 - 락 - 동기화의 개념에 대해 다시 읽어보자

📕 2. 과도한 동기화는 피하라

동기화는 필수지만, 과도한 사용은 시스템 전체를 망가뜨릴 수 있다.

2-1. 과도한 동기화란?

  • 필요 이상의 넓은 범위에 락을 거는 것

  • 동기화 블록 내에서 외부 메서드를 호출하는 것

  • 긴 시간 동안 락을 유지하는 것

  • 복잡한 계산이나 I/O 작업을 동기화 블록 내에서 수행하는 것 등

  • 🔥 에일리언 메서드(Alien Method) 호출은 특히 위험하다

  • 에일리언 메서드 : 자신이 정의하지 않은 외부 객체의 메서드

  • 이 메서드를 synchronized 블록 안에서 호출하면, 그 내부에서 또 다른 락을 걸거나 블록되는 연산을 수행할 수 있어 데드락(Deadlock)이 발생할 수 있다.

public class AlienMethodExample {
    private final List<String> list = new ArrayList<>();

    public synchronized void process(Consumer<String> action) {
        for (String item : list) {
            action.accept(item); // 외부에서 정의한 로직을 동기화 블록 내에서 실행
        }
    }
}

2-2. 과도한 동기화의 위험성

  • 성능 저하 : 동기화된 코드는 한 번에 하나의 스레드만 실행할 수 있어, 병렬 처리 효율이 떨어짐

  • 데드락(Deadlock) : 두 스레드가 서로 상대방이 가진 자원을 기다림

  • 응답 불가(Livelock) : 스레드가 계속 작업을 시도하지만 진전이 없는 상태

  • 예측할 수 없는 동작 : 동기화 블록 안에서 외부 메서드를 호출할 경우 발생할 수 있는 안정성 문제

[예시]

public class OverSyncExample {
    private final List<String> list = new ArrayList<>();

    public synchronized void add(String item) {
        list.add(item); // 단순한 연산인데 메서드 전체를 락으로 감쌈
    }

    public synchronized String get(int index) {
        return list.get(index); // 읽기 작업도 굳이 락 필요 없음
    }
}

🧷 3. 과도한 동기화를 해결하는 법

  1. Collections.synchronizedList() vs CopyOnWriterArrayList

  • synchronizedList : 리스트에 동기화 래퍼(synchronized wrapper)를 씌워, 동시 접근 시 하나의 스레드만 접근할 수 있도록 만들어줌

  • for-each와 같은 여러 번 접근하는 반족 작업은 명시적으로 동기화 블록으로 감싸줘야 한다.

// 1. Collections.synchronizedList(): 명시적 동기화가 필요한 경우
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());

// 사용 시 주의: 순회할 때 반드시 동기화 필요
synchronized (synchronizedList) {
    for (String item : synchronizedList) {
        System.out.println(item);  // 안전하게 접근
    }
}

- `CopyOnWriterArrayList` : 읽기 작업이 많을 때 유리 - 내부적으로 데이터를 변경할 때마다 새로운 배열을 복사하여 생성, 동기화 없이 안전하게 순회 가능

// 2. CopyOnWriteArrayList: 명시적 동기화가 필요 없는 경우
List<String> concurrentList = new CopyOnWriteArrayList<>();

// 순회할 때 동기화 불필요
for (String item : concurrentList) {
    System.out.println(item);  // 자체적으로 안전성 보장
}

따라서, 쓰기가 자주 있을 때는 'synchroziedList', 읽기가 압도적으로 만들 때 'CopyOnWriterArrayList'

  1. 필요한 범위에서만 동기화 하자(minimize lock scope)

  • 전체 메서드를 동기화하는 대신 필요한 부분만 synchronized로 감싼다.

public void addIfAbsent(String item) {
    synchronized (list) {
        if (!list.contains(item)) {
            list.add(item);
        }
    }
}
  1. 불변 객체를 사용하자

  • 읽기만 하고 변경이 필요 없다면 불변 객체를 사용하자 -> 동기화 자체가 필요 없음

  • 변경할 필요가 있으면 복사후 새 객체를 만들어서 처리함

List<String> immutableList = List.of("A", "B", "C");

🤖 최종 결론

동기화는 필수이지만, 과도한 사용은 시스템 전체를 망가뜨릴 수 있다.


😶‍🌫️ 느낀점

  • 단순히 synchronized만 붙인다고 스레드 안전한 것이 아니다!

Last updated