-
item 78 공유 중인 가변 데이터는 동기화해 사용하라책/이펙티브 자바 2022. 4. 23. 11:27
ITEM 78 공유 중인 가변 데이터는 동기화해 사용하라
synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장한다
많은 개발자가 동기화를 배타적 실행 용도로 생각한다
- 한 스레드가 변경하는 중이라 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는 용도로만 생각한다
- 한 객체가 일관된 상태를 가지고 생성
- 해당 객체에 접근하는 메서드는 그 객체에 락(lock)을 건다
- 락을 건 메서드는 객체의 상태를 확인 후 필요하면 수정
- 즉 객체를 하나의 일관된 상태에서 다른 일관된 상태로 변화시키며 동기화를 제대로 사용하면 어떤 메서드도 이 객체의 상태가 일관되지 않은 순간을 볼 수 없다
- 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못 할 수 있다
- 동기화는 일관성이 깨진 상태를 볼 수 없게 하며 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다
언어 명세상 변수를 읽고 쓰는 동작은 원자적이다(long, double은 예외)
- 여러 스레드가 같은 변수를 동기화 없이 수정하는 중이라도 항상 어떤 스레드가 정상적으로 저장한 값을 온전히 읽어 오는지 보장한다는 듯
- "성능을 높이려면 원자적 데이터를 읽고 쓸 때는 동기화하지 말아야겠다"고 생각하기 쉬운데 아주 위험한 발상이다
- 자바 언어 명세는 스레드가 필드를 읽을 때 항상 "수정이 완전히 반영된"값을 얻는다고 보장한다
- 하지만 한 스레드가 저장한 값이 다른 스레드에게 보이는가는 보장하지 않는다
동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다
- 한 스레드가 만든 변화가 다른 스레드에게 언제 어떻게 보이는지를 규정한 자바의 메모리 모델 때문
공유중인 가변 데이터를 원자적으로 읽고 쓰는게 가능해도 동기화에 실패하면 처참한 결과로 이어질 수 있다
- 다른 스레드를 멈추는 작업을 생각해보자
- Thread.stop 메서드는 안전하지 않아서 오래전에 deprecated API로 지정되었으니 사용하지 말자
- 스레드를 멈추는 올바른 방법
- 첫 번째 스레드는 자신의 boolean 필드를 폴링하며 그 값이 true 가 되면 멈춘다
- 이 필드를 false로 초기화해놓고 다른 스레드에서 이 스레드를 멈추고자 할 때 true로 변경하는 식이다
- boolean 필드를 읽고 쓰는 작업은 원자적이라 어떤 개발자는 이런 필드에 접근할 때 동기화를 제거하기도 한다
잘못된 코드 이 프로그램은 얼마나 오래 실행될까 public class StopThread { private static boolean stopRequested; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(() -> { int i = 0; while (!stopRequested) i++; }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; } } 이 프로그램은 1초 후에 종료될 것 같지만 저자의 컴퓨터에서는 영원히 실행 되었다 원인은 동기화다 동기화 하지 않으면 스레드가 수정한 값을 백그라운 프로세스가 언제쯤에나 볼지 보증할 수 없다 동기화가 빠지만 가상머신이 다음과 같은 최적화를 수행할 수도 있는 것이다 //원래 코드 while (!stopRequested) i++; //최적화 코드 if (!stopRequested) while (true) i++; OpenJDK 서버 VM이 실제로 적용하는 끌어올리기(hoisting)라는 최적화 기법이다 이 결과 프로그램은 응답 불가(liveness failure) 상태가 되어 더이상 진전이 없다 stopRequested 필드를 동기화해 접근하면 문제 해결이 가능하다 적절히 동기화해 스레드가 종료된다 public class StopThread { private static boolean stopRequested; private static synchronized void requestStop() { stopRequested = true; } private static synchronized boolean stopRequestStop() { return stopRequested; } public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(() -> { int i = 0; while (!stopRequested()) i++; }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; } } 위 코드는 기대한 대로 1초 후에 종료 되며 쓰기 메서드(requestStop)와 읽기 메서드(stopRequested) 모두를 동기화했음에 주목하자
쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다
- 쓰기 메서드만 동기화해서는 충분하지 않다
- 어디서는 둘 중 하나만 동기화해도 정상 동작처럼 보이지만 속지말자
- 사실 이 두메서드는 단순해 동기화 없어도 원자적으로 동작한다
- 앞에서 이야기 했듯이 동기화는 배타적 수행과 스레드 간 통신이라는 두가지 기능을 수행하는데 여기서는 통신 목적으로만 사용됐다
동기화 관련 문제들을 피하는 가장 좋은 방법은 애초에 가변데이터를 공유하지 않는 것이다
- 불변데이터만 공유하거나 아무것도 공유하지 말자
- 가변데이터는 단일 스레드에서만 사용하자
- 이 정책을 따른다면 그 사실을 문서에 남겨 유지보수 과정에서도 정책이 계속 지켜지도록 하는게 중요하다
- 사용하려는 프레임워크와 라이브러리를 깊이 이해하는 것도 중요하다
- 이런 외부 코드는 개발자가 인지하지 못한 스레드를 수행하는 복병으로 작용 할 수 있다
한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때는 해당 객체에서 공유하는 부분만 동기화 해도 된다
- 그 객체를 다시 수정할 때 까지 다른 스레드들은 동기화 없이 자유롭게 값을 읽어 갈 수 있다
- 이런 객체를 사실상 불변(effectively immutable)이라 하며 다른 스레드에 이런 객체를 건네는 행위를 안전발행(safe publication)이라 한다
- 객체를 안전하게 발행하는 방법은 많다
- 클래스 초기화 과정에서 객체를 정적 필드, volatile 필드, final 필드, 혹은 보통의 락을 통해 접근하는 필드에 저장해도 된다
여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화 해야 한다
동기화 하지 않으면 한 스레드가 수행한 변경을 다른 스레드가 보지 못할 수도 있다
공유되는 가변 데이터를 동기화하는 데 실패하면 응답 불가 상태에 빠지거나 안전 실패로 이어질 수 있다
이는 디버깅 난이도가 가장 높은 문제에 속하며 간헐적이거나 특정 타이밍에만 발생할 수도 있고, VM에 따라 현상이 달라지기도 한다
배타적 실행은 필요 없고 스레드끼리의 통신만 필요하다면 volatile 한정자만으로 동기화할 수 있지만 올바로 사용하기 어렵다
'책 > 이펙티브 자바' 카테고리의 다른 글
item 80 스레드보다는 실행자, 태스크, 스트림을 애용하라 (0) 2022.04.23 item 79 과도한 동기화는 피하라 (0) 2022.04.23 item 77 예외를 무시하지 말라 (0) 2022.04.23 item 76 가능한 한 실패 원자적으로 만들라 (0) 2022.04.23 item 75 예외의 상세 메시지에 실패 관련 정보를 담으라 (0) 2022.04.22 - 한 스레드가 변경하는 중이라 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는 용도로만 생각한다