1. 동기화

동기화의 기능

  1. 배타적 실행 : 한 스레드가 변경하는 중이라서 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는다.
  2. 스레드 간 통신 : 한 스레드가 만든 변화를 다른 스레드에서 확인한다.

언어 명세상 longdouble 외의 변수를 읽고 쓰는 동작은 원자적(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 을 걸고 있더라도 읽기 메서드를 호출하면 수정 전의 값을 조회할 수도 있다.

동기화는 배타적 수행스레드 간 통신 이라는 두 가지 기능을 수행하는데, 이 코드에서는 그 중 통신 목적 으로만 사용되었다.

2. volatile

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++;
}