JAVA/Effective Java

[아이템 79] 과도한 동기화는 피하라

꾸준함. 2024. 4. 7. 13:29

과도한 동기화는 다음과 같은 부작용을 초래합니다.

  • 성능을 떨어뜨리고
  • Deadlock 상태에 빠드리고
  • 심지어 예측할 수 없는 동작을 낳을 수 있음

 

응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 내 제어를 절대로 클라이언트에 양도하면 안 됩니다.

동기화된 클래스 관점에서, 다음과 같은 메서드들은 "외계인 메서드(alien method)"로 알려져 있는데, 이는 이러한 메서드들이 어떤 동작을 수행할지 확신할 수 없으며, 예외를 발생시키거나, 데드락 상태에 빠뜨리거나, 데이터를 손상시킬 수 있기 때문입니다.

  • 동기화된 코드 블록 내 재정의 가능한 메서드
  • 클라이언트가 전달한 함수 객체

 

외계인 메서드(Alien Method)


 

코드 부연 설명

  • 관찰자들은 addObserver()와 removeObserver() 메서드를 호출해 구독을 신청하거나 해지
  • 두 메서드 모두 다음 콜백 인터페이스의 인스턴스를 메서드에 전달


 

인터페이스 부연 설명

  • 해당 인터페이스는 구조적으로 BiConsumer<ObservableSet, E>와 동일
  • 커스텀 함수형 인터페이스를 정의한 이유는 이름이 더 직관적이고 다중 콜백을 지원하도록 확장할 수 있기 때문

 

오동작하는 예시 #1


 

코드 부연 설명

  • 익명 클래스를 사용한 이유는 s.removeObserver 메서드에 함수 객체 자신을 넘겨야 하는데 람다는 자신을 참조할 수단이 없기 때문

 

예상한 결과

  • 0 ~ 23까지 출력한 뒤 Observer 자신을 구독 해제 한 후 아무 로그 출력 없이 종료할 것으로 예상

 

실제 결과

  • 0 ~ 23까지 출력한 뒤 ConcurrentModificationException 예외 던짐

 

 

 

오동작 원인

  • notifyElementAdded() 메서드가 Observer들의 리스트를 순회하는 도중에 Observer의 added() 메서드가 호출되기 때문
    • 리스트에서 원소를 제거하려는데 마침 해당 원소를 순회하는 중
    • 이는 허용되지 않은 동작

 

  • notifyElementAdded() 메서드에서 수행하는 순회는 동기화 블록 내 있으므로 동시 수정이 발생하지 않지만 정작 자신이 콜백을 거쳐 되돌아와 수정하는 것을 방지하지 못함

 

오동작하는 예시 #2


 

코드 부연 설명

  • 프로그램을 실행하면 Deadlock 상태에 빠짐

 

Deadlock 상태에 빠지는 이유

  • 백그라운드 쓰레드가 s.removeObserver() 호출 시 락 획득을 시도하지만 메인 쓰레드가 이미 락을 획득하고 있음
    • removeObserver() 메서드가 synchronized 키워드가 달려있기 때문에 실행 시 락 획득

 

  • 동시에 메인 쓰레드는 백그라운드 쓰레드가 Observer를 제거할 때까지 대기
  • 정리하자면 메인 쓰레드는 백그라운드 쓰레드가 Observer를 제거할 때까지 락을 풀지 않을 것이고 반대로 백그라운드 쓰레드는 메인 쓰레드가 락을 풀 때까지 락 획득 시도를 할 것이기 때문에 교착 상태

 

다소 억지스러운 상황을 연출한 것이지만 실제 시스템에서도 동기화된 영역 내 외계인 메서드를 호출하여 데드락 상태에 빠지는 사례가 자주 발생하기 때문에 주의해야 합니다.

 

오동작하는 예시 해결 방법 #1

 

외계인 메서드 호출을 동기화 블록 바깥으로 옮기면 해결이 됩니다.

 

 

코드 부연 설명

  • 동기화 영역 바깥에서 호출되는 외계인 메서드를 열린 호출(open call)이라고 지칭
  • 외계인 메서드는 얼마나 오래 실행될지 알 수 없는데, 동기화 영역 내에서 호출된다면 그 동안 다른 쓰레드는 보호된 자원을 사용하지 못하고 대기해야 함 (얼마나 오래 대기해야 할지 모르는 상황)
  • 따라서 열린 호출은 실패 방지 효과 외에도 동시성 효율을 크게 개선

 

 

오동작하는 예시 해결 방법 #2

 

외계인 메서드 호출을 동기화 블록 바깥으로 옮기는 것보다 더 나은 방법은 java.util.concurrent 패키지의 CopyOnWriteArrayList를 사용하는 것입니다.

CopyOnWriteArrayList는 ArrayList를 구현한 클래스로 다음 동작을 수행합니다.

  • 내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행
  • 내부의 배열은 절대 수정되지 않아 락을 사용하지 않음 (빠름)

 

CopyOnWriteArrayList를 다른 용도로 사용할 경우 매번 깊은 복사를 수행해 느리겠지만, 수정할 일이 드물고 순회만 번번히 일어나는 Observer 리스트 용으로는 적합합니다.


 

ConcurrentModificationException 발생 안함

 

synchronization 블록 대신 ReentrantLock을 사용한다면?

오동작하는 예시 #1에서 synchronization 블록 대신 ReentrantLock을 사용할 경우 불변식이 임시로 깨질 수 있습니다.

  • 자바 언어의 락(ReentrantLock)은 Compare And Swap 기법을 사용하기 때문에 재진입을 허용하므로 Deadlock 상태에 빠지지 않음
  • ReentrantLock 사용 시 외계인 메서드를 호출하는 쓰레드는 이미 락을 쥐고 있으므로 다음번 락 획득도 성공
    • 해당 락이 보호하는 데이터에 대해 개념적으로 관련이 없는 다른 작업이 진행 중임에도 불구하고 락 획득 성공할 수도 있으며 이 때문에 참혹한 결과가 빚어질 수 있음
    • 문제의 주 원인은 락이 제 구실을 하지 못했기 때문
    • 재진입 가능 락은 객체 지향 멀티 쓰레드 프로그램을 쉽게 구현할 수 있도록 해주지만, 응답 불가(교착 상태)가 될 상황을 안전 실패(데이터 훼손)으로 변모시킬 수 있음

 

* 위 내용에 대한 예제 코드를 작성하신 분이 있다면 공유 부탁 드립니다...

 

동기화 성능

동기화 영역을 지정하는 것은 성능과의 필연적인 상충 관계를 가지고 있습니다.

자바가 꾸준히 버전업 되면서 동기화 비용은 빠르게 낮아져 왔지만, 과도한 동기화를 피하는 일은 오히려 과거 어느 때보다 중요합니다.

멀티 코어가 일반화된 오늘 날 과도한 동기화가 초래하는 진짜 비용은 락을 얻는데 드는 CPU 시간이 아닙니다.

진짜 비용은 다음과 같습니다.

  • 과도한 동기화로 인해 쓰레드 간 경쟁하느라 낭비하는 시간
  • 즉 병렬로 실행할 기회를 잃고, 모든 코어가 메모리를 일관되게 보기 위한 지연 시간
  • 가상 머신의 코드 최적화를 제한한다는 점도 과도한 동기화의 또 다른 숨은 비용

 

따라서 기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 수행하는 것입니다.

  • 동기화 영역에서는 락을 얻고, 공유 데이터를 검사하고, 필요시 수정하고, 락 해제
  • 오래 걸리는 작업일 경우 아이템 78 원칙을 어기지 않으면서 동기화 영역 바깥으로 옮기는 방법을 모색하는 것을 추천

 

또한 가변 클래스를 작성하는 경우 다음 두 선택지 중 하나를 따르는 것을 권장합니다.

  • 동기화를 전혀 하지 말고 가변 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화
  • 동기화를 내부에서 수행해 thread-safe한 클래스를 생성
    • 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때만 해당 방법 이용
    • 락 분할(lock splitting), 락 스트라이핑(lock striping), 비차단 동시성 제어(nonblocking concurrency control) 등 다양한 기법을 동원해 동시성을 높여줄 수 있음
    • 위 내용을 한번 알아보자..

 

java.util은 전자의 방식을 선택했고 java.util.concurrent는 후자의 방식을 선택했습니다.

 

정리

  • 교착 상태와 데이터 훼손을 피하려면 동기화 영역 내 외계인 메서드를 절대 호출하지 말자
  • 동기화 영역 안에서의 작업은 최소한으로 줄이자
  • 성능을 위해 멀티코어를 최대한 활용해야하므로 과도한 동기화를 피하는 것이 과거 어느 때보다 중요하다
  • 가변 클래스를 설계할 때는 스스로 동기화해야 할지 고민하자
    • 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화했는지 여부를 문서에 명확히 밝히자

 

참고하면 좋은 카테고리

 

https://jaimemin.tistory.com/category/JAVA/RxJava

 

'JAVA/RxJava' 카테고리의 글 목록

메일: jaimemin@naver.com

jaimemin.tistory.com

 

참고

이펙티브 자바

반응형