JAVA/Effective Java

[아이템 78] 공유 중인 가변 데이터는 동기화해 사용하라

꾸준함. 2024. 3. 29. 19:45

동기화

동기화란 멀티 쓰레드 환경에서 하나의 메서드 혹은 블록을 한 번에 하나의 쓰레드만 수행하도록 보장하는 것을 의미합니다.

  • 싱글 쓰레드 환경에서는 동기화 걱정 안 해도 됨
  • 멀티 쓰레드 환경에서는 여러 개의 쓰레드가 하나의 객체를 공유해서 사용하는 경우가 있으므로 동기화 처리 필요

 

1. 동기화 과정

 

  • 한 객체가 일관된 상태를 가지고 생성되었을 때 해당 객체에 접근하는 메서드는 다른 쓰레드가 메서드를 실행할 수 없도록 락을 검
  • 락을 건 메서드는 객체의 상태를 확인하거나 필요하면 수정
    • 정리하자면 일관된 하나의 상태에서 다른 일관된 상태로 변화시킴

 

  • 메서드 실행이 끝나면 락을 해제

 

2. 동기화 특징

 

  • 동기화를 제대로 사용할 경우 어떤 메서드도 해당 객체의 상태가 일과되지 않은 순간을 목격할 수 없음
  • 동기화가 없을 경우 여러 쓰레드가 접근해 시점에 따라 일관된 상태가 아닐 수 있기 때문에 동기화 없이는 한 쓰레드가 많은 변화를 다른 쓰레드에서 확인할 수 없음
  • 언어 명세상 long과 double 외의 변수를 읽고 쓰는 동작은 원자적
    • 다수의 쓰레드가 하나의 변수에 동기화 없이 접근하더라도 항상 정상적으로 저장한 값을 온전히 읽어옴을 보장

 

  • `원자적 데이터를 읽고 쓸 때는 동기화하지 않아도 되겠다`라고 생각하는 것은 위험한 발상
    • 필드를 읽을 때 항상 수정이 완전히 반영된 값을 얻지만
    • `한 쓰레드가 저장한 값이 다른 쓰레드에도 보이는가?` 즉, 가시성은 보장하지 않기 때문
    • 가시성은 한 쓰레드에서 수정된 변수의 변경이 다른 쓰레드에서 즉시 반영되도록 하는 특성을 의미

 

공유 중인 가변 데이터를 동기화 처리하지 않은 코드

 

 

 

예상 결과

  • 1초 뒤에 `stopRequested` 필드가 false로 변경되어 쓰레드 내 while 문이 종료될 것으로 예상

 

실제 결과

  • `stopRequested` 필드가 동기화되지 않아 메인 쓰레드에서 수정한 `stopRequested` 필드 값을 백그라운드 쓰레드가 언제 즘에나 보게 될지 보증할 수 없음
  • 따라서 무한 루프에 빠지게 됨
  • 설상가상으로 동기화 처리를 하지 않을 경우 JVM이 다음과 같은 최적화를 수행할 수도 있음
    • 자바에서는 JIT(Just-In-Time) 컴파일러를 통해 코드를 최적화하며 최적화 과정에서는 다양한 최적화 기법이 사용될 수 있으며, 이 중에는 "hoisting"과 유사한 최적화 기법이 존재
    • 호이스팅(Hoisting)은 일반적으로 변수 및 함수 선언을 해당 스코프의 최상위로 옮기는 것을 가리키는데, 이는 주로 JavaScript에서 발생하는 현상
    • 이 결과 프로그램은 응답 불가 상태가 되어 더 이상 진전이 없음

 

 

 

공유 중인 가변 데이터를 동기화 처리한 코드

 

1. 메서드에 synchornized 키워드 부여

 

 

 

코드 부연 설명

  • 자바에서 제공하는 synchronized 키워드를 통해 동기화 처리
  • 읽기/쓰기 메서드 모두 synchronized 키워드를 통해 동기화 처리
    • 쓰기 메서드만 동기화 처리를 하고 읽기 메서드에는 동기화 처리를 하지 않을 경우 정상적인 동작을 보장할 수 없음
    • 둘 다 동기화 처리를 했기 때문에 예상대로 1초만에 프로그램 종료

 

2. 가변 변수에 volatile 키워드 부여


 

코드 부연 설명

  • volatile 변수를 읽어 들일 때 CPU 캐시가 아니라 메인 메모리로부터 값을 읽어 들임
    • 즉, 읽을 때도 CPU 캐시가 아닌 메인 메모리에서 읽어오고, 쓸 때도 CPU 캐시가 아닌 메인 메모리에 쓰기를 수행
    • long, double을 제외한 primitive type 자료형은 volatile 키워드 부여 시 syncrhonized 키워드를 통한 동기화를 생략해도 예상한 대로 동작

 

2.1 volatile 키워드 사용 시 주의할 점

  • volatile 키워드는 mutex 즉, 단일 메서드 혹은 블록을 단일 쓰레드만 수행하도록 하는 기법과는 무관
    • mutex를 원한다면 volatile 키워드와 함께 syncrhonized 키워드도 사용해야 함

 

 

 

예상 결과

  • generateSerialNumber() 메서드를 호출할 때마다 1씩 증가한 고유한 nextSerialNumber을 반환할 것으로 예상
    • nextSerialNumber가 long이나 double이 아닌 int이고 volatile 키워드가 부여되었기 때문에 최신 값을 읽을 수 있을 것이라고 예상

 

실제 결과

  • `nextSerialNumber++;`를 풀어서 작성하면 `nextSerialNumber = nextSerialNumber + 1;`과 같은 형태
    • 결국 nextSerialNumber 값을 한 번 읽어와 + 1 한 다음 다시 nextSerialNumber 변수에 저장하는 형태
    • volatile 키워드만 부여되었으므로 nextSerialNumber + 1 연산이 이루어지는 시점에 다른 쓰레드가 비집고 들어오면 특정 숫자가 두 번 리턴되는 상황
    • 이런 오류를 안전 실패(safety failure)라고 지칭

 

해결 방법

  • mutex를 보장하기 위해 generateSerialNumber() 메서드에 synchronized 키워드를 부여하면 됨
  • synchronized 메서드 또는 블록을 사용하는 경우 변수에 대한 가시성을 보장하기 위해 volatile을 사용할 필요가 없음
  • synchronized 블록을 통해 동기화가 이미 이루어지고 있기 때문에 메모리에 쓰인 값이 다른 쓰레드에게 정확하게 보장되기 때문
    • 따라서, generateSerialNumber() 메서드에 synchronized 키워드 부여 시 nextSerialNumber 변수의 volatile 키워드는 제거해도 됨

 

 

 

long, double을 사용할 때는 주의하자

  • long, double 타입 자료형에 동기화 처리가 필요시 java.util.concurrent.atomic 패키지에서 제공하는 AtomicLong, AtomicDouble 사용하는 것을 권장
    • 위 패키지는 락 없이도 thread-safe 한 프로그래밍을 지원하는 클래스들이 담겨 있음
    • CAS 기법(Compare And Swap)을 사용하기 때문에 락 없이도 thread-safe 함을 보장
    • java.util.concurrent.atomic 패키지는 가시성과 함께 mutex까지 지원

 

참고하면 좋은 카테고리

 

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

 

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

메일: jaimemin@naver.com

jaimemin.tistory.com

 

참고

이펙티브 자바

반응형