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

Jung Ho Seo·2020년 9월 1일
0

EffectiveJava

목록 보기
34/35
post-thumbnail

Syncronized

synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장된다.

동기화의 기능

한 객체가 생성되고, 이 객체 접근하는 메서드는 그 객체에 lock을 건다. 락을 건 메서드는 객체의 상태를 확인하고 필요하면 수정한다. 즉, 객체를 하나의 일관된 상태에서 다른 일관된 상태로 변화시킨다. 동기화를 제대로 사용하며 어떤 메서드도 이 객체의 상태가 일관되지 않은 순간을 볼 수 없을 것이다. 뿐만 아니라 동기화의 중요한 기능은 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수도 있다.

동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다

동기화가 없다면 한 스레드가 저장한 값이 드라느 스레드에게 언제 '보일지' 알 수 없다. 동기화는 배타적 실행 뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.

동기화를 잘못 사용한 코드

public class StopThread {
	private static boolean stopRequested;

	public static void main(String[] args) {
		Thread backgroundThread = new Thread(() -> {
			int i = 0;
			while(!stopRequested)
				i++;

		});
		backgroundThread.start();
		
		TimUnit.SECONDS.sleep(1);
		stopRequested = true;
	}

}

이 프로그램은 1초뒤에 종료되지 않고 영원히 수행된다. 원인은 동기화에 있다. 동기화 하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에 보게 될지 알 수 없다.

적절히 동기화 한 코드

public class StopThread {
	private static boolean stopRequested;

	private static synchronized void requestStop() {
		stopRequested = true;
	}

	private static synchronized boolean stopRequested() {
		return stopRequested;
	}

	public static void main(String[] args) {
		Thread backgroundThread = new Thread(() -> {
			int i = 0;
			while(!stopRequested())
				i++;

		});
		backgroundThread.start();
		
		TimUnit.SECONDS.sleep(1);
		requestStop();
	}

}

쓰기 메서드(requestStop)와 읽기 메서드(stopRequested) 모두를 동기화 했음에 주목하자. 쓰기와 읽기 모두 동기화되지 않으면 동작을 보장하지 않는다.

volatile

다른 대안으로 volatile 한정자를 사용하는 방법이 있다. volatile 한정자는 배타적 수행과는 상관 없지만 항상 가장 최근에 기록된 값을 보장한다.

예시

public class StopThread {
	private static volatile boolean stopRequested;

	public static void main(String[] args) {
		Thread backgroundThread = new Thread(() -> {
			int i = 0;
			while(!stopRequested)
				i++;

		});
		backgroundThread.start();
		
		TimUnit.SECONDS.sleep(1);
		stopRequested = true;
	}

}

사용시 주의할 점

private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
	return nextSerialNumber++;
}

이 메서드는 굳이 동기화하지 않더라도 불변식을 보호할 수 있어 보인다.

문제는 증가연산자(++)다. 이 연산자는 실제로 nextSerialNumber 필드에 두 번 접근한다. 먼저 값을 읽고, 그런 다음(1 증가한) 새로운 값을 저장하는 것이다. 만약 다른 스레드가 이 두 접근 사이를 비집고 들어오면 문제가 생길 수 있다. 이 메서드 synchronized 한정자를 붙이면 해결된다.

문제를 피하는 가장 좋은 방법

가장 좋은 방법은 애초에 가변 데이터를 공유하지 않는 것이다. 불변 데이터판 공유하거나 아무것도 공유하지 말자. 가변 데이터는 단일 스레드에서만 쓰도록 하다 혹은 한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유 할 때는 해당 객체에서 공유하는 부분만 동기화 해도 된다. 이런 객체를 사실상 불변(effectively immutable)이라 하고 다른 스레드에 이런 개게를 건네는 행위를 안전 발행(safe publication)이라 한다.

정리

여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화 해야 한다. 동기화 하지 않으면 한 스레드가 수행한 변경을 다른 스레드가 보지 못할 수도 있다.

profile
책, 글, 개발

0개의 댓글