동기화
동기화란 멀티 쓰레드 환경에서 하나의 메서드 혹은 블록을 한 번에 하나의 쓰레드만 수행하도록 보장하는 것을 의미합니다.
- 싱글 쓰레드 환경에서는 동기화 걱정 안 해도 됨
- 멀티 쓰레드 환경에서는 여러 개의 쓰레드가 하나의 객체를 공유해서 사용하는 경우가 있으므로 동기화 처리 필요
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 > Effective Java' 카테고리의 다른 글
[아이템 80] 쓰레드보다는 실행자, 태스크, 스트림을 애용하라 (0) | 2024.04.07 |
---|---|
[아이템 79] 과도한 동기화는 피하라 (0) | 2024.04.07 |
[아이템 77] 예외를 무시하지 말라 (2) | 2024.03.29 |
[아이템 76] 가능한 한 실패 원자적으로 만들라 (0) | 2024.03.29 |
[아이템 75] 예외의 상세 메시지에 실패 관련 정보를 담으라 (0) | 2024.03.29 |