배타적 실행
: 한 스레드가 변경하는 중이라서 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는다.스레드 간 통신
: 한 스레드가 만든 변화를 다른 스레드에서 확인한다.언어 명세상 long
과 double
외의 변수를 읽고 쓰는 동작은 원자적(atomic)
이다. 여러 스레드가 같은 변수를 동기화없이 수정하는 중이라도, 항상 어떤 스레드가 정상적으로 저장한 값을 온전히 읽어옴을 보장한다.
하지만, 성능을 위해서 원자적 데이터를 읽고 쓸 때 동기화를 하지 않는 것은 좋은 방법이 아니다. 자바 언어 명세는 스레드가 필드를 읽을 때 항상 수정이 완전히 반영된 값
을 얻는다고 보장한다. 하지만 한 스레드가 저장한 값이 다른 스레드에게 보이는가
는 보장하지 않는다. 동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다. 공유 중인 가변 데이터를 비록 원자적으로 읽고 쓸 수 있을지라도 동기화에 실패하면 처참한 결과로 이어질 수 있다.
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++;
어떤 값이 여러 스레드에 걸쳐서 공유되는 값이라고 JVM 에게 알려주어야
그에 맞는 최적화를 한다. 아래는 올바르게 작동하도록 수정한 코드이다.
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) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested()) // 메서드 호출하여 값 조회
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop(); // 메서드 호출하여 값 수정
}
}
위의 코드는 사실 코드가 단순해서 동기화 없어도 원자적으로 동작한다. 그와 별개로 올바른 동기화를 하기 위해서는 위와 같이 쓰기
와 읽기
모두 동기화를 해야 한다. 쓰기만 동기화해도 될 것 같으나 실제로 값을 수정할 때 하나의 스레드가 Lock 을 걸고 있더라도 읽기 메서드를 호출하면 수정 전의 값을 조회할 수도 있다.
동기화는 배타적 수행
과 스레드 간 통신
이라는 두 가지 기능을 수행하는데, 이 코드에서는 그 중 통신 목적
으로만 사용되었다.
volatile
한정자는 배타적 수행과는 상관이 없지만 항상 가장 최근에 기록된 값
을 읽는다. 반복문에서 매번 동기화하는 비용이 크진 않지만 그래도 속도가 더 빠른 방법이다.
public class StopThread() {
private static volatile boolean stopRequested; // volatile 설정
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;
}
}
하지만 volatile 은 주의해서 사용해야 한다.
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}