-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
11장동시성동시성
Description
공유 중인 가변 데이터는 동기화해 사용하라
- synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장한다.
- 많은 프로그래머가 동기화를 배타적 실행이라고 생각한다.
- 동기화에는 중요한 기능 하나 더 있다. 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수 있다. 동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
- 자바에서 원자적이란 여러 스레드가 같은 변수를 동기화 없이 수정하는 중이라도, 항상 어떤 스레드가 정상적으로 저장한 값을 온전히 읽어옴을 보장한다는 뜻이다.
- 그렇다고 '성능을 높이려면 원자적 데이터를 읽고 쓸 때는 동기화하지 말아야겠다'라는 생각을 할수 있는데 위험한 생각이다.
- 자바 언어는 스레드가 필드를 읽을 때 항상 '수정이 완전히 반영된' 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 '보이는가'는 보장하지 않는다.
동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.- 공유중인 가변 데이터를 비록 원자적으로 읽고 쓸 수 있을지라도 동기화에 실패하면 처참한 결과로 이어질 수 있다.
- Thread.stop 메서드는 안전하지 않아 이미 오래전에 사용 자제 API로 지정됐다
Thread.stop은 사용하지 말자!(이 메소드는 자바11에서 드디어 제거되었다.)
ex) 잘못된 코드 - 이 프로그램은 영원히 수행 된다.
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;
}
}- 원인은 동기화에 있다.
- 동기화하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에나 보게 될지 보증할 수 없다.
ex)적절히 동기화해 스레드가 정상 종료된다.
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();
}
}쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다.
- 둘중 하나만 동기화해도 동작하는 듯 보이지만, 겉모습에 속으면 안된다.
- 반복문에서 매번 동기화하는 비용이 크진 않지만 속도가 더 빠른 대안을 소개하겠다.
- stopRequested 필드를 volatile로 선언하면 동기화를 생략해도 된다.
volatile 한정자는 배타적 수행과는 상관없지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.
ex) volatile 필드를 사용해 스레드가 정상 종료된다.
public class StopThread {
private static volatile 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;
}
}- 단, volatile은 주의해서 사용해야 된다.
ex) 잘못된 코드-동기화가 필요하다.
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}- 이 메서드의 상태는 nextSerialNumber라는 단 하나의 필드로 결정되는데, 원자적으로 접근할 수 있고 어떤 값이든 허용한다. 따라서 굳이 동기화하지 않더라도 불변식을 보호할 수 있어 보인다.
- 하지만 이 역시 동기화없이는 올바로 동작하지 않는다.
- 이유는,
증가 연산자(++)이다. 코드상으로는 하나지만 실제로는 nextSerialNumber필드에 두번 접근한다. 먼저 값을 읽고, 그 다음 새로운 값을 저장하는 것이다. 만약 두 번째 스레드가 이 두 접근 사이를 비집고 들어온다면 첫 번째 스레드와 똑같은 값을 돌려받게 된다. - 이런 오류를 `안전 실패(safety failure)라고 한다.
ex) java.util.concurrent.atomic을 이용한 락-프리 동기화
private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}- 이 방법을 사용하면 우리가 원했던 그 기능을 수행 가능하다.
- volatile은 동기화의 두 효과중 통신 쪽만 지원하지만 이 패키지는 원자성(배타적 실행)까지 지원한다.
가변 데이터는 단일 스레드에서만 쓰도록 하자.
- 이번 아이템에서 언급한 문제들을 피하는 가장 좋은 방법은 물론 애초에 가변 데이터를 공유하지 않는것이다.
- 불변 데이터만 공유하거나 아무것도 공유하지 말자.
- 한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때는 해당 객체에서 공유하는 부분만 동기화해도 된다.
- 이런 객체를 사실상 불변(effectively immutable)이라 하고 다른 스레드에 이런 객체를 건네는 행위를 안전 발행(safe publication)이라 한다.
핵심 정리
여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화 해야한다.- 동기화 하지 않으면 한 스레드가 수행한 변경을 다른 스레드가 보지 못할 수도 있다.
- 공유되는 가변 데이터를 동기화하는데 실패하면 응답 불가 상태에 빠지거나 안전 실패로 이어질 수 있다.
- 배타적 실행은 필요없고 스레드끼리의 통신만 필요하다면 volatile 한정자만으로 동기화할 수 있지만, 올바르게 사용하기 까다롭다.