반응형
이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라
synchronized
- 해당 메서드나 블록을 한 번에 한 스레드씩 수행하도록 보장하는 키워드
- 많은 프로그래머가 동기화를 배타적 실행, 즉 한 스레드가 변경하는 중이라서 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는 용도
- 한 객체가 일관된 상태를 가지고 생성되고, 이 객체에 접근하는 메서드는 그 객체에 lock을 건다.
- 락을 건 메서드는 객체의 상태를 확인하고 필요하면 수정한다. (객체를 하나의 일관된 상태에서 다른 일관된 상태로 변화시킨다.)
- 동기화를 제대로 사용하면 어떤 메서드도 이 객체의 상태가 일관되지 않은 순간을 볼 수 없을 것이다.
- 위의 내용이 맞지만, 동기화의 중요한 기능으로는 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수 있다.
- 동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메서드나 블록에 들어간 스레드가 같은 lock의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
- 언어 명세상 long과 double 외의 변수를 읽고 쓰는 동작은 원자적이다. (여러 스레드가 같은 변수를 동기화 없이 수정하더라도 항상 어떠한 스레드도 정상적으로 저장한 값을 온전히 읽어옴을 보장함)
- 이 원자적 특성을 믿고 원자적 데이터를 읽고 쓸 때는 동기화하지 않겠다고 생각하면 좋지 않다.
- 한 스레드가 저장한 값이 다른 스레드에게 보이는 지에 대한 보장하지 않음.
- 즉, 동기화는 배타적 실행 뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.
- 공유중인 가변 데이터를 비록 원자적으로 읽고 쓸 수 있더라도 동기화에 실패하면 실패할 수 있다.
- 가변 데이터는 단일 스레드에서만 쓰도록 하자.
Thread를 멈추는 작업
- (Thread.stop은 데이터를 훼손할 가능성이 있고 안전하지 않으므로 사용하지 말자.)
- 첫 번째 스레드는 자신의 boolean 필드를 폴링하면서 그 값이 true가 되면 멈춘다.
- 이 필드를 false로 초기화해놓고, 다른 스레드에서 이 스레드를 멈추고자 할 때 true로 변경하는 방법
ex 1) Thread를 멈추는 잘못된 예
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초 후 stopRequested를 true로 설정하면 backgroundThread는 반복문을 빠져나올 것처럼 보이지만, 영원히 수행된다.
- 원인은 동기화다. 동기화하지 않으면 메인 스레드가 수정한 값을 backgroundThread가 언제 볼 지 보증할 수 없다.
ex 2) ex1를 개선한 예
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();
}
}
- 쓰기 메서드(requestStop)와 읽기 메서드(stopRequested) 모두를 동기화했다.
- 쓰기 메서드만 동기화하면 안되고, 쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다.
volatile
ex 3) volatile 필드를 사용한 Thread 종료 예
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;
}
}
- stopRequested 필드를 volatile 으로 선언하면 동기화를 생략해도 된다.
- volatile 한정자는 배타적 수행과는 상관없지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.
ex 4) 동기화가 필요한 잘못된 예 (일련번호를 생성하는 메서드)
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
- 이 메서드는 매번 고유한 값을 반환할 의도로 만들어졌다.
- 이 메서드의 상태는 nextSerialNumber라는 단 하나의 필드로 결정되는데, 원자적으로 접근할 수 있고 어떤 값이든 허용한다.
- 따라서 굳이 동기화를 하지 않더라도 불변식을 보호할 수 있어 보이지만, 동기화 없이는 올바르게 동작하지 않는다.
- 증가 연산자(++)는 코드상으로 하나지만 실제로는 enxtSerialNumbeer 필드에 두 번 접근한다.
- 먼저 값을 읽고, 그 다음 1을 증가시킨 새로운 값을 저장하는 것이다.
- 만약 두 번째 스레드가 이 사이에 값을 읽어가면 첫 번째 스레드와 똑같은 값을 돌려받게 된다.
- 이렇게, 프로그램이 잘못된 결과를 계산하는 오류를 ‘안전 실패’ 라고 한다.
- generateSerialNumber 메서드에 synchronized 한정자를 붙이면 이 문제가 해결된다.
- 동시에 호출해도 서로 간섭하지 않으며 이전 호출이 변경한 값을 읽게 된다는 뜻이다.
- 단, 메서드에 synchronized를 붙였다면 nextSerialNumber 필드에서 volatile을 제거해야 한다.
AtomicLong
- 이 패키지에는 락 없이도(lock-free) 스레드 안전한 프로그래밍을 지원하는 클래스들이 담겨 있다.
- volatile은 동기화의 두 효과 중 통신 쪽만 지원하지만 이 패키지는 원자성(배타적 실행)까지 지원한다.
- 우리가 generateSerialNumber에 적용할 수 있는 기능이다.
ex 5) java.util.concurrent.atomic을 이용한 lock-free 예
private static final AtomicLong nextSerialNum = 0;
public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}
정리
- 여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화해야 한다.
- 동기화하지 않으면 한 스레드가 수행한 변경을 다른 스레드가 보지 못할 수 있다.
- 공유되는 가변 데이터를 동기화하는 데 실패하면 응답 불가 상태에 빠지거나 안전 실패로 이어질 수 있따.
- 이는 디버깅 난이도가 가장 높은 문제에 속한다.
- 간헐적이거나 특정 타이밍에만 발생할 수도 있고, VM에 따라 현상이 달라지기도 한다.
- 배타적 실행은 필요 없고 스레드끼리의 통신만 필요하다면 volatile 한정자만으로 동기화할 수 있지만, 올바로 사용하기 까다롭다.
반응형
'BE > Java' 카테고리의 다른 글
[Effective Java] 아이템 80. 스레드보다는 실행자, 태스크, 스트림을 이용하라 (0) | 2022.12.21 |
---|---|
[Effective Java] 아이템 79. 과도한 동기화는 피하라 (0) | 2022.12.20 |
[Effective Java] 아이템 77. 예외를 무시하지 말라 (0) | 2022.12.16 |
[Effective Java] 아이템 76. 가능한 한 실패 원자적으로 만들라 (0) | 2022.12.16 |
[Effective Java] 아이템 75. 예외의 상세 메시지에 실패 관련 정보를 담으라 (0) | 2022.12.15 |