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

들어가며

동기화는 배타적 실행과 스레드간의 안정적 통신에 사용한다. 다음 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class item78 {
private static boolean stopRequested;

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

backgourndThread.start();

TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}

이 코드는 1초후 종료될 것 같지만 끝나지 않는다. JVM이 호이스팅이라는 최적화 기법을 사용하기 때문이다. 즉 while문을 다음과 같이 바꾼다.

1
2
3
if(!stopRequested)
while(true)
i++;

이는 stopRequested 필드의 읽기와 쓰기를 모두 동기화해 접근하면 문제를 해결할 수 있다. 필드를 volatile로 선언해도 동작할 수 있다. 다음은 volatile을 잘못 사용한 예이다.

1
2
3
4
5
private static volatile int nextSerialNumber = 0;

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

이유는 여기서 확인할 수 있다. 즉, 할당되어 있는 메모리에 직접 접근했더라도 연산 자체가 원자성을 띄지 않기 때문이다.

동기화가 필요할 땐 java.util.concurrent 패키지를 사용할 수 있다. 락 없이도 thread-safe를 지원하는 클래스들이 존재한다. 내부적으로 락에 대한 연산을 정의해 놨기 때문에 클라이언트 코드에서는 락을 사용하지 않아도 깔끔한 코드를 작성할 수 있다.

이런 문제들을 피하는 가장 좋은 방법은 애초에 가변 데이터를 공유하지 않는 것이다. 불변 데이터만 공유하거나 아무것도 공유하지 말자. 가변 데이터는 단일 스레드에서만 쓰도록 하자. 한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때는 해당 객체에서 공유하는 부분만 동기화 해도 된다. 그러면 그 객체를 다시 수정할 일이 생기기 전까지 다른 스레드들은 동기화 없이 자유롭게 값을 읽어갈 수 있다. 이런 객체를 사실상 불변이라 하고 다른 스레드에 이런 객체를 건네는 행위를 안전 발행이라 한다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/23/Languages/Effective%20JAVA/item78/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.