페이지 불러오는 중
한 문장 요약: "공유 + 가변 + 최소 1개의 쓰기"가 겹치면 데이터가 더럽혀지고(원자성), 변경이 안 보이며(가시성), 순서가 뒤바뀐다(재정렬). 이를 Lock으로 막으면 이번엔 경합·데드락·기아라는 새 문제가 생긴다.
문제가 성립하는 조건 세 가지가 동시에 만족될 때만 스레드 안전 문제가 발생합니다.
그래서 해결의 1순위는 항상 "셋 중 하나를 없애기"(주로 공유를 줄이거나 불변으로 만들기)입니다. Lock은 셋 다 못 없앨 때의 수단입니다(6장).
이 노트는 JVM 메모리 모델(JMM) 계층을 다룹니다. DB 트랜잭션 격리 수준의 "dirty read"와 용어가 겹치지만 계층이 다릅니다.
원인은 공통적으로 "한 번에 끝나야 할 연산이 원자적이지 않다" 입니다.
class Counter {
private int count = 0;
void increment() { count++; } // 실제로는 read → +1 → write 3단계
}count=10을 읽고, 각자 11을 써서 한 번의 증가가 사라짐.class Range {
private int lower, upper; // 불변식: lower <= upper
void set(int l, int u) { this.lower = l; this.upper = u; } // 두 줄 사이가 노출
}set(5, 10) 도중 lower=5만 반영된 순간, B가 읽으면 (5, 이전 upper=3) → 불변식이 깨진 객체를 사용.HashMap을 여러 스레드가 동시에 put → 리사이즈(rehash) 중 링크가 꼬여 조회가 무한 루프(Java 7), size가 실제와 불일치, 엔트리 유실.ArrayList 동시 add → ArrayIndexOutOfBoundsException 또는 null 슬롯.한 스레드의 쓰기가 다른 스레드에 영원히 반영되지 않을 수 있습니다.
class Worker {
private boolean running = true; // 일반 필드
void run() { while (running) { /* ... */ } } // 멈추지 않을 수 있음
void stop() { running = false; } // 다른 스레드가 호출
}running을 매번 메인 메모리에서 읽으리란 보장이 없음.volatile boolean running → 읽기/쓰기가 메인 메모리를 거치도록 강제(가시성 확보).volatile은 가시성만 준다. count++ 같은 복합 연산의 원자성은 여전히 미보장.if (instance == null) { // 1차 검사
synchronized (lock) {
if (instance == null)
instance = new Singleton(); // 객체 참조 대입이 생성자 완료보다 먼저 보일 수 있음
}
}
return instance; // 다른 스레드가 "생성 덜 된" 인스턴스를 받을 위험instance를 volatile로 선언(재정렬 차단) 또는 정적 홀더 클래스(Initialization-on-demand holder) 사용.동기화는 1~3장을 해결하지만, "락을 걸어서" 생기는 문제가 별도로 존재합니다.
BLOCKED 상태 비율로 확인.// 스레드1: transfer(a, b) → lock(a) 후 lock(b) 대기
// 스레드2: transfer(b, a) → lock(b) 후 lock(a) 대기 → 서로 영구 대기tryLock(timeout)으로 포기 경로 마련.jstack → Found one Java-level deadlock.ReentrantLock(true)(공정 모드)로 완화(처리량은 하락).synchronized(this) — 외부에서 이 객체로 락을 걸면 간섭.String 리터럴·Integer(−128~127 캐시)로 락 → 의도치 않게 다른 코드와 같은 락 객체 공유.new Object() 를 락으로 → 매번 다른 락 → 보호 실패.private final Object lock = new Object();동기화 수단은 happens-before 관계를 만들어, "앞의 쓰기가 뒤의 읽기에 보인다"를 보장합니다.
핵심: 동기화(
synchronized/volatile/java.util.concurrent)가 하나도 없으면 아무것도 보장되지 않습니다. "지금까지 잘 돌았다"는 우연입니다.
final + 방어적 복사), ThreadLocal, 지역 변수(스택 한정), 스레드 간 전달은 불변 메시지로.AtomicInteger/LongAdder(CAS), ConcurrentHashMap, BlockingQueue. 락보다 경합에 강함.tryLock(timeout), ReadWriteLock(읽기 多), 전용 private final 락 객체.
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.