도당탕탕
Item78 : Shared Mutable Data에 대한 접근을 동기화하라. 본문
Synchronization이 보장하는 것
- 함수에서 consistent 상태의 객체 읽기
- 스레드들이 서로가 변경한 사항들을 읽기
동기화 방법
1. synchronized 사용
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;
}
}
위 코드는 정상동작 하지 않는다. 왜냐면 vm이 코드를 다음처럼 변환하기 때문이다.
// before
while (!stopRequested) i++;
// after
if (!stopRequested)
while (true) i++;
위 프로그램은 다음처럼 수정하면 정상동작한다.
// Properly synchronized cooperative thread termination
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();
}
}
여기서 중요한 것은 read/write 작업에 모두 동기화를 해야 한다는 점이다. 여기서 위의 작업들은 atomic 하지만 스레드들 간의 커뮤니케이션을 위해서 동기화를 한 것이다.
2. volatile 사용
mutual exclusion 없이 스레드들 간의 커뮤니케이션만 가능하게 하는 방법으로 volatile
을 이용한 방법이 있다.
volatile
은 가장 최근에 변경된 값을 읽도록 허용한다.
// Cooperative thread termination with a volatile field
public class StopThread {
private static volatile 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;
}
}
위의 예제에서는 mutual exclusion은 필요 없으므로, volatile 사용은 적절하다. 반면 다음과 같은 상황에서는 volatile
을 사용하면 안 된다.
// Broken - requires synchronization!
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
이유는 nextSerialNumber++
작업이 원자적이지 않기 때문이다. 즉, 값을 읽는 과정과 더하는 과정이 독립적이기 때문에, 멀티스레드 환경에서 동기화 문제가 발생한다. 이를 safety failure(프로그램이 잘못된 결과를 계산)
라 한다.
이는 두 가지 방법으로 수정 가능하다.
- synchronized 사용
private static int nextSerialNumber = 0; public static int synchronized generateSerialNumber() { return nextSerialNumber++; }
- AtomicLong 사용 : 1번 보다 성능이 우수하다.
// Lock-free synchronization with java.util.concurrent.atomic private static final AtomicLong nextSerialNum = new AtomicLong(); public static long generateSerialNumber() { return nextSerialNum.getAndIncrement(); }
정리
- 스레드들이 서로 mutable data를 공유한다면, read/write 작업은 동기화되어야 한다.
- 만약 thread communication만 필요하고 mutual exclusion이 필요 없는 경우라면 volatile 키워드를 사용하라.
'JAVA' 카테고리의 다른 글
Item80 : Executor, Task, Stream 을 Thread 보다 선호하라. (0) | 2023.02.03 |
---|---|
Item79 : 과도한 동기화는 피하라 (0) | 2023.02.03 |
Item77 : 예외를 무시하지 말라 (0) | 2023.02.02 |
Item76 : Failure Atomicity(실패 원자성)을 보장하려고 노력하라. (0) | 2023.02.01 |
Item75 : 예외의 상세 메시지에 실패 관련 정보를 담으라 (0) | 2023.02.01 |
Comments