-
Notifications
You must be signed in to change notification settings - Fork 0
Description
📌 [아이템 78] 공유 중인 가변 데이터는 동기화해 사용하라
✨ 핵심 내용
synchronized키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장
<동기화의 기능>
-
배타적 실행, 객체에 락을 걸어 하나의 일관된 상태에서 다른 일관된 상태로 변화시킨다
-
한 스레드가 만든 변화를 다른 스레드에서 확인할 수 있게 해준다
즉, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다
자바 언어 명세는 스레드가 필드를 읽을 때 항상 수정이 완전히 반영된 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 보이는가는 보장하지 않는다
⇒ 동기화는 스레드 사이의 안정적인 통신에 꼭 필요함!
자바의 메모리 모델
스레드가 만든 변화가 다른 스레드에게 언제 어떻게 보이는지를 규정함
공유 중인 가변 데이터를 원자적으로 읽고 쓸 수 있을지라도 동기화에 실패하면 결과가 처참해질 수 있다
- Thread.stop 메서드는 이미 deprecated 되었다
- 사용 금지!
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++;
});
backgroundTh read.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}스레드1 : boolean 값을 폴링하면서 true가 되면 멈춤
다른 스레드에서 스레드1을 멈추기 위해 필드를 변경하는 방식
프로그램이 1초 후에 종료될거라 예상하지만, 해당 프로그램은 영원히 수행된다!!!
⇒ why? 동기화하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에나 보게 될지 보증할 수 없다
🔽 수정한 코드 이젠 동기화되어 스레드가 정상 종료된다
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();
}
}읽기와 쓰기 메서드 모두 동기화가 필요하다! 쓰기만 동기화해서는 충분하지 않다
🔽 volatile로 선언하면 동기화 생략 가능하다
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;
}
}volatile 한정자는 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다! 그렇기에 배타적 수행과는 상관없어도 동기화를 생략가능하다
☠️ ☠️ volatile 주의사항!
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}일련번호 생성 코드
문제는 ++ 연산자이다
++ 연산자는 실제로 nextSerialNumber 해당 필드에 두 번 접근한다
- 읽고
- 증가한 값을 저장
두번째 스레드가 두 과정 사이를 비집고 들어와 값을 읽어가면, 첫 번째 스레드와 똑같은 값을 돌려받게 된다
프로그램이 잘못된 결과를 계산해내는 오류를 안전 실패라고 함
<해결법>
generateSerialNumber 메서드 그 자체에 synchronized 한정자를 붙이면 된다
→ 동시에 호출해도 서로 간섭하지 않으며 이전 호출이 변경한 값을 읽게 됨
- 다만! synchronized를 붙으면
volatile는 제거해야함
AtomicLong (아이템 59)
java.util.concurrent.atomic을 이용한 락-프리 동기화
- 락 없이도 스레드 safe 프로그래밍 지원
private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNum.getAndlncrement();
}→ 성능도 동기화보다 우수함!
애초에 가변 데이터를 공유하지 말라!
가변 데이터는 단일 스레드에서만 쓰자!
-
한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때 해당 객체에서 공유하는 부분만 동기화 가능
→ 객체를 다시 수정할 일이 생기기 전까지 다른 스레드들은 동기화 없이 자유롭게 값 읽을 수 있음! 이런 객체를 불변이라하고, 이걸 건네는 행위를 안전 발행(safe publication)이라함
-
방식
- 클래스 초기화 과정에서 객체를 정적 필드, volatile 필드, final 필드, 락을 통해 접근하는 필드에 저장가능
💡 새롭게 알게 된 점
- volatile 한정자를 통해 동기화를 간접적으로 할 수 있다는 것을 알게 되었습니다
📚 정리
여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고, 쓰는 동작은 반드시 동기화 해야함
배타적 실행은 필요 없고 스레드끼리 통신만 필요하다면 volatile 한정자만으로 동기화 가능
📢 댓글로 각자의 학습 내용을 공유해주세요!