[Effective Java] Item 78. 공유 중인 가변 데이터는 동기화해 사용하라
synchronized는 해당 메서드나 블록을 한 번에 한 스레드씩 수행하도록 보장한다.
동기화의 두가지 용도
배타적 실행
- 한 스레드가 변경하는 중이라서 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는다.
스레드 사이의 안정적 통신
- 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수 있다.
- 동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메서드나 블록에 들어간 스레드가 같은 lock의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해 준다.
동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.
long, double 외의 변수를 일고 쓰는 동작은 원자적(atomic)이다
여러 스레드가 같은 변수를 동기화 없이 수정하는 중이라도, 항상 어떤 스레드가 정상적으로 저장한 값을 온전히 읽어옴을 보장한다는 뜻이다.
하지만 성능을 높이고자 원자적 데이터를 읽고 쓸 때는 동기화하지 말아야겠다는 생각은 위험한다.
자바 언어 명세는 스레드가 필드를 읽을 때 항상 수정이 완전히 반영된 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 보이는가는 보장하지 않는다.
이는 한 스레드가 만든 변화가 다른 스레드에게 언제 어떻게 보이는지를 규정한 자바의 메모리 모델 때문이다.
공유 중인 가변 데이터를 원자적으로 읽고 쓸 수 있을지라도 동기화에 실패하면 처참한 결과로 이어질 수 있다.
예를 살펴보자.
다른 스레드를 멈추는 작업
- 첫 번째 스레드는 자신의 boolean 필드를 폴링하면서 그 값이 true가 되면 멈춘다.
- 이 필드를 false로 초기화해 놓고, 다른 스레드에서 이 스레드를 멈추고자 할 때 true로 변경한다.
- 메인 스레드가 1초 후 stopRequestd를 true로 설정하면 backgroundThread 반복문을 빠져나오길 기대할 것이지만 무한 루프에 빠졌다.
- 원인은 동기화에 있는데, 동기화하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에나 보게 될지 보증할 수 없다.
동기화가 빠지면 OpenJDK 서버 VM의 hoisting 최적화 기법을 사용해 아래와 같은 최적화를 수행할 수도 있다.
// 원래 코드
while(!stopRequested)
i++;
// 최적화 코드
if(!stopRequested)
while(true)
i++;
솔루션 - 쓰기 메서드와 읽기 메서드를 모두 동기화
- 기대한 대로 1초 후에 종료된다.
- 쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다.
synchronized 대안
volatile은 반복문에서 매번 동기화하는 비용이 크진 않지만 속도가 더 빠른 대안이다.
- 위 메서드의 stopRequested 필드를 volatile로 선언하면 동기화를 생략해도 된다.
- volatile 한정자는 배타적 수행과는 상관없지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.
- volatile 필드를 사용해 스레드가 정상 종료한다.
volatile의 잘못된 사용 - 일련번호를 생성할 의도로 만든 코드
- 증가 연산자(++)는 코드상으로는 하나지만 실제로는 nextSerialNumber 필드에 두 번 접근한다.
- 먼저 값을 읽고, 그 후에 1 증가한 새로운 값을 저장하는 동작이다.
- 두 번째 스레드가 이 두 접근 사이를 비집고 들어와 값을 읽어가면 첫 번째 스레드와 똑같은 값을 돌려받게 된다.
- 이러한 오류를 안전 실패(safety failure)라고 한다.
synchronized 한정자를 붙이고 volatile을 제거하자.
더 견고하게 하려면 int 대신 long을 사용하거나 nextSerialNumber가 최댓값에 도달하면 예외를 던지게 하자.
java.util.concurrent.atomic 패키지의 AtomicLong
- 해당 패키지에는 락 없이 스레드 안전한 프로그래밍을 지원하는 클래스들이 담겨 있다.
- 성능도 동기화 버전보다 우수하다.
주의할 점
이러한 문제들을 피하는 가장 좋은 방법은 애초에 가변 데이터를 공유하지 않는 것이다.
- 불변 데이터만 공유하거나 아무것도 공유하지 말자.
- 가변 데이터는 단일 스레드에서만 쓰도록 하자.
사용하려는 프레임워크와 라이브러리를 깊이 이해하고 사용해야 한다.
- 외부 코드가 우리가 인지하지 못하는 스레드를 수행하는 복병으로 작용하는 경우도 있다.
한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때는 해당 객체에서 공유하는 부분만 동기화해도 된다.
- 객체를 다시 수정할 일이 생기기 전까지 다른 스레드들은 동기화 없이 자유롭게 값을 읽어갈 수 있다.
- 이런 객체를 사실상 불변(effectively immutable)이라 한고, 이런 객체를 건네는 행위를 안전 발행(safe publication)이라 한다.
결론
- 여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화해야 한다.
- 동기화하지 않으면 한 스레드가 수행한 변경을 다른 스레드가 보지 못할 수도 있다.
- 배타적 실행은 필요 없고 스레드끼리의 통신만 필요하다면 volatile 한정자로만 동기화할 수 있지만 올바로 사용하기 까다롭다.