<aside> 💡
스레드는 여러 활동을 동시에 수행할 수 있게 해준다. 하지만 동시성 프로그래밍은 단일 스레드 프로그래밍보다 어렵다. 잘못될 수 있는 일이 늘어나고 문제를 재현하기도 어려워지기 때문이다. 하지만 멀티코어 프로세서의 힘을 제대로 활용하려면 반드시 내 것으로 만들어야만 하는 기술이기 때문에, 이번 장을 통해 동시성 프로그래밍을 명확하고 정확하게 만들고 잘 문서화하는 데 도움이 되는 조언들을 살펴보자.
</aside>
synchronized
키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장한다.
많은 프로그래머가 동기화를 배타적 실행, 막는 용도로만 생각한다. 동기화를 제대로 사용하면 어떤 메서드도 이 객체의 상태가 일관되지 않은 순간을 볼 수 없을 것이다.
맞는 설명이지만, 동기화에는 중요한 기능이 하나 더 있다. 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수 있다.
동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
언어 명세상 long과 double 외의 변수를 읽고 쓰는 동작은 원자적이다. 이 말을 듣고 '성능을 높이려면 원자적 데이터를 읽고 쓸 때는 동기화하지 말아야겠다"고 생각하기 쉬운데, 아주 위험한 발상이다. 항상 '수정이 완전히 반영된' 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 '보이는가'는 보장하지 않는다.
동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다. (자바의 메모리 모델 때문) 공유 중인 가변 데이터를 비록 원자적으로 읽고 쓸 수 있더라도 동기화에 실패하면 처참한 결과로 이어질 수 있다. 예시로 다른 스레드를 멈추는 작업을 생각할 수 있다. (Thread.stop은 사용하지 말자! 안전하지 않아 이미 오래전에 deprecated되었다)
다른 스레드를 멈추는 올바른 방법은 boolean 필드를 폴링하면서 true가 되면 멈추고. 다른 스레드에서 이 스레드의 해당 변수를 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초 후에 종료되리라 생각하는가? 하지만 저자의 컴퓨터에서는 도통 끝날 줄 모르고 영원히 수행되었다. 원인은 동기화에 있다. 동기화하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에나 보게 될지 보장할 수 없다. 동기화가 빠지면 JVM이 다음과 같은 최적화를 수행할 수도 있는 것이다.
// 원래 코드
while (!stopRequested)
i++;
// 최적화한 코드
if (!stopRequested)
while (true)
i++;
실제 OpenJDK 서버 VM이 실제로 적용하는 hoisting 최적화 기법이다. 이는 다음과 같이 바꿔야한다.
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchroized boolean stopRequested() {
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);
requestStop();
}
}
쓰기 메서드만 동기하해서는 충분하지 않다. 쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다. 겉모습에 속아서는 안 된다. 사실 이 두 메서드는 단순해서 동기화 없이도 원자적으로 동작한다. 하지만 동기화의 배타적 수행과 스레드 간 통신이라는 두 가지 기능 중에, 통신 목적으로만 이 코드에서는 사용한 것이다.