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. 과도한 동기화를 해결하는 법
Collections.synchronizedList()
vsCopyOnWriterArrayList
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'
필요한 범위에서만 동기화 하자(minimize lock scope)
전체 메서드를 동기화하는 대신 필요한 부분만 synchronized로 감싼다.
public void addIfAbsent(String item) {
synchronized (list) {
if (!list.contains(item)) {
list.add(item);
}
}
}
불변 객체를 사용하자
읽기만 하고 변경이 필요 없다면 불변 객체를 사용하자 -> 동기화 자체가 필요 없음
변경할 필요가 있으면 복사후 새 객체를 만들어서 처리함
List<String> immutableList = List.of("A", "B", "C");
🤖 최종 결론
동기화는 필수이지만, 과도한 사용은 시스템 전체를 망가뜨릴 수 있다.
😶🌫️ 느낀점
단순히 synchronized만 붙인다고 스레드 안전한 것이 아니다!
Last updated