동기화의 기능
- 배타적 실행
- 한 스레드가 변경 중일 때 다른 스레드는 접근할 수 없다.
- 즉, 일관된 상태를 보장한다.
- 안정적 통신
- 이전 수정의 최종 결과를 보게해줌
- long, double 같은 원자적 타입은 배타적 실행은 보장하지만 안정적 통신까지는 보장하지 않는다. 따라서 동기화가 필요하다.
- 자바의 메모리 모델 때문
가변데이터와 동기화
공유 중인 가변데이터의 동기화에 실패하면 매우 처참할 수 있다
ex) Thread.stop은 사용하지말자
그럼, 다른 스레드를 멈추게하고 싶다면?
boolean 필드 + polling 을 통해 스스로 멈추게하자.
주의할점
그럼 원자적 자원에 접근하는 코드는 동기화를 할 필요가 없을까?
답은 X 이다. 위에서 언급한대로 동기화는 안정적 통신도 지원하는데, 아래와 같은 코드는 그것을 보장할 수 없다.
package Chap11_Concurrency.item78;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
// 코드 78-1 잘못된 코드 - 이 프로그램은 얼마나 오래 실행될까? (415쪽)
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;
}
}
이 코드는 JVM hoisting 기법에의해 아래처럼 최적화되고, 결과적으로 멈추지 않는다.
CPU Cache: 이와 같은 변수는 CPU cache 에 저장되고 읽어오게되는데 멀티스레드 환경에서 이는 문제가 될 수 있다
hosting 기법: JVM 의 JIT 컴파일러는 코드의 최적화를 담당하는데, loop문의 변수 hoisting(끌어올리기) 기법도 그의 한 종류이다
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
if(!stopRequest){
while(true){
}
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1L);
stopRequested = true; // 1초 후 Thread가 for문 수행을 멈추기를 기대한다.
}
이 문제는 아래와 같이 synchronized 키워드를 작성하면 해결할 수 있다.
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();
}
}
집중해서 보아야할 점은 쓰기메서드와 읽기 메서드 둘 다 동기화 처리 했다는 점이다.
이곳에서 synchronized 키워드는 "통신"을 목적으로 사용되었기 때문에 쓰기, 읽기 작업 모두에 처리해줘야한다
또는, 필드에 volatile 한정자를 추가할 수 있다.
volatile: 변수를 Main Memory에서 읽어옴을 보장한다. 즉, 캐시에서 읽어오는 것이 아니기 때문에 가장 최근 변경사항을 가져올 수 있다
private static volatile boolean stopRequested;
항상 volatile로 해결할 수 있는 것은아니다.
private static volatile int nextSerialNumber= 0;
public static int generateSerialNumber(){
return nextSerialNumber++;
}
위 코드는 한번의 연산을 수행하는 것같지만 사실 두번 접근한다.
따라서 그 사이를 다른 스레드가 비집고 들어가면 올바르지 않은 결과를 받을 수 있는데, 이를 안전실패라고 한다.
즉, volatile은 통신은 해결할 수 있지만, 배타적 수행은 보장하지 못한다.
java.util.concurrent.atomic 패키지에는 이 문제들을 해결해주는 다양한 클래스가 정의되어있다.
근본적으로 웬만하면 가변 데이터를 공유하지말자. 또는 단일 스레드에서만 사용하도록하자.
불가피하게 사용해야한다면 꼭 문서에 남기도록하자.