Search

Item 78. 공유 중인 가변 데이터는 동기화해 사용하라

생성일
2023/08/15 11:53
챕터
11장 -동시성

요약

멀티 스레딩 환경에서 가변 데이터를 사용하려면 동기화가 꼭 필요하다.
가변 데이터는 가급적 단일 스레드에서만 사용하자.

스레드 동기화

언어 명세상 long과 double을 제외한 변수를 읽고 쓰는 동작은 원자적(atomic)이다. 하지만 이를 믿고 동기화를 하지 않는다면, 스레드가 필드를 읽을 때 항상 수정이 반영된 값을 얻는 것은 보장하지만 다른 스레드가 해당 값을 제대로 읽어 간다고 보장하지 못한다.
public Class stopThread { private static int testValue = 0; public static void main(String[] args) { for (int i = 0; i < 1000; i++) { new Thread(() -> { for (int j = 0; j < 10; j++) testValue++; }).start(); new Thread(() -> { for (int j = 0; j < 5; j++) testValue--; }).start(); } Thread.sleep(1000); System.out.println(testValue); } }
Java
복사
실제 위 코드를 여러 번 동작시킨 결과 5000, 4991, 5002, 4990, 5000, 4998의 값이 출력되었다. 이렇게 프로그램이 잘못된 결과를 계산해내는 것을 안전 실패(safety failure)라고 한다.
public Class stopThread { private static volatile int testValue = 0; public static void main(String[] args) { for (int i = 0; i < 1000; i++) { new Thread(() -> { for (int j = 0; j < 10; j++) testValue++; }).start(); new Thread(() -> { for (int j = 0; j < 5; j++) testValue--; }).start(); } Thread.sleep(1000); System.out.println(testValue); } }
Java
복사
이와 같이 volatile 키워드를 적용하더라도 5000, 5000, 5010, 5000, 5000, 4990 값이 출력되었다.
volatile은 CPU 캐싱 없이 메모리에서 데이터를 읽어오기만 할 뿐 동기화 작업을 해주지는 않는다. 위 코드에서 문제는 testValue++과 같이 데이터를 읽고 더하는 2번의 명령이 수행되는 사이에 다른 스레드가 비집고 들어와 값을 바꾸는 것이 문제이다.
public Class stopThread { private static int testValue = 0; private synchronized static void increase() { testValue++; } private synchronized static void decrease() { testValue--; } public static void main(String[] args) { for (int i = 0; i < 1000; i++) { new Thread(() -> { for (int j = 0; j < 10; j++) increase(); }).start(); new Thread(() -> { for (int j = 0; j < 5; j++) decrease(); }).start(); } Thread.sleep(1000); System.out.println(testValue); } }
Java
복사
하지만 이와 같이 synchronized 키워드를 통해 읽기와 쓰기 모두 동기화 후에는 항상 5000의 값만 출력된다.
public Class stopThread { private static AtomicInteger testValue = new AtomicInteger(0); public static void main(String[] args) { for (int i = 0; i < 1000; i++) { new Thread(() -> { for (int j = 0; j < 10; j++) testValue.incrementAndGet(); }).start(); new Thread(() -> { for (int j = 0; j < 5; j++) testValue.decrementAndGet(); }).start(); } Thread.sleep(1000); System.out.println(testValue.get()); } }
Java
복사
혹은 이와 같이 java.tuil.concurrent.atomic 패키지의 AtomicInteger를 사용하면, 별도의 동기화 없이도 스레드 안전한 프로그램을 작성할 수 있다.

가변 데이터와 스레드

가변 데이터를 멀티 스레딩 환경에서 사용하면 위와 같은 문제들이 발생한다.
그러니 가장 좋은 방법은 가변 데이터를 공유하지 않는 것이다. 멀티 스레딩 환경에서는 불변 데이터만 공유하거나 아무것도 공유하지말고, 가변 데이터는 단일 스레드에서만 사용하자.
하나의 스레드가 데이터를 다 수정한 후 다른 스레드에 공유하는 부분만 동기화 해도 사실상 불변(effectively immutable)로, 다른 스레드에 이런 객체를 건네는 행위를 안전 발생(safe publication)이라 한다.