티스토리 뷰
반응형
synchronized 키워드
해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장
동기화의 기능
1) 배타적 실행
- 한 스레드가 변경하는 중이라서 상태가 일관되지 않을 때 객체를 다른 스레드가 보지 못하게 막는 것
- 객체를 하나의 일관된 상태에서 다른 일관된 상태로 변화 : 한 객체가 일관된 상태를 가지고 생성 → 객체에 접근하는 메서드는 그 객체에 락(lock)을 걸음 → 락을 건 메서드는 필요하면 객체를 수정
2) 스레드 사이 안정적인 통신
- 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수 있다.
- 변경된 데이터의 최종 결과 값을 읽을 수 있게 한다.
원자적 (atomic)
- 자바에서 long / double 외 변수를 읽고 쓰는 동작은 원자적이다. (동기화 없이도 항상 어떤 스레드가 정상적으로 저장한 값을 온전히 읽어옴을 보장함)
- 원자적 데이터를 읽고 쓸 때는 동기화가 필요 없는거 아닌가? → 스레드가 필드를 읽을 때 수정이 완전히 반영된 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 보이는가는 보장하지 않음 (동기화는 배타적 실행 뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요)
다른 스레드를 멈추는 방법
다른 스레드를 멈추는 Thread.stop()은 deprecated API로 지정되었으니 사용하지 말자.
1) synchronized
자신의 boolean 필드를 두고 false로 초기화한 후 반복문을 돌게 만든다. 그리고 다른 스레드에서 해당 스레드를 멈추게 하고 싶은 경우에 true로 초기화하게 만들어 멈추게 하자.
public class StopThread {
private static boolean stopRequested; // false 초기화
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
int i = 0;
while(!stopRequested){
i++;
}
}).start();
// 1초 뒤에 저 스레드를 멈추게 하자.
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
위 코드는 무한 루프를 돌게 된다.
동기화의 두번째 기능인 스레드간의 통신을 생각하지 않았기 때문 → 동기화하지 않으면, 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에 보게 될 지 보증할 수 없다.
//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를 사용하여 동기화 시켰다.
쓰기와 읽기 모두가 동기화되어야 동작을 보장한다.
2) volatile 으로 선언
- 필드를 volatile로 선언하면 동기화를 생략해도 된다.
- volatile 한정자는 배타적 수행과는 상관없지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.
public class StopThread {
// volatile 사용
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
}
}).start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
- 멀티 스레드 환경에서는 원래 성능 향상을 위해 변수 데이터들을 메인 메모리가 아닌 CPU cache 에 저장하고 활용한다. 그런데 volatile 필드는 멀티 스레드 상황에서도 메인 메모리에 데이터를 저장하고 읽기에 같은 값을 가질 수 있었던 것이다.
- volatile 주의점
private static volatile int nextSerialNumber= 0;
public static int generateSerialNumber(){
return nextSerialNumber++;
}
- 안전 실패 오류 : 한 스레드가 저장하기 이전에 읽어버리면 스레드 간 nextSerialNumber 이 중복된 값이 된다.
- 메서드에 synchronized를 붙이고 volatile을 제거하여 해결할 수 있다.
동기화를 지원하는 클래스
- java.util.concurrent.atomic 패키지의 AtomicLong을 사용해보자. 이 패키지에는 락 없이도 스레드 안전한 프로그래밍을 지원하는 클래스들이 있다.
- volatile은 동기화의 두번째 기능 (스레드 간 안전 통신)만 지원하지만, 이 패키지는 배타적 실행까지 지원
- 성능도 기존 동기화 버전보다 좋음
동기화 문제를 피하는 방법
- 애초에 가변 데이터를 공유하지 않는 것 (가변 데이터는 단일 스레드에서만 사용하도록)
- 사용하려는 프레임워크와 라이브러리가 스레드를 수행하는 지 잘 인지하도록 하자.
- 안전 발행 : 한 스레드가 데이터를 수정한 후 다른 스레드에 공유할 때 해당 객체에서 공유하는 부분만 동기화해도 된다.
결론
- 여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화해야함
- 이에 실패하면 응답 불가 상태에 빠지거나 안전 실패로 이어짐
- 배타적 실행은 필요 없고 스레드끼리 통신만 필요하다면 volatile 한정자만으로 동기화 가능
반응형
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 80.스레드보다는 실행자, 태스크, 스트림을 애용하라 (0) | 2022.04.19 |
---|---|
[Effective Java] 79.과도한 동기화는 피하라 (0) | 2022.04.19 |
[Effective Java] 77.예외를 무시하지 말라 (0) | 2022.04.16 |
[Effective Java] 76.가능한 한 실패는 원자적으로 만들라 (0) | 2022.04.16 |
[Effective Java] 75.예외의 상세 메세지에 실패 관련 정보를 담으라 (0) | 2022.04.16 |
댓글