F-LAB JAVA · 4주차 · Phase 4 · 동기화: synchronized와 메모리 가시성
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
synchronized 블록은 메서드의 일부 코드만 동기화하여 동기화 범위를 최소화하고, 잠금 대상을 명시적으로 지정할 수 있는 방식이다.
synchronized (lock) { ... }형태로 임계 영역만 감싸므로, 메서드 전체를 잠그는 synchronized 메서드보다 동기화 범위가 좁아 성능이 좋다 (비임계 영역은 병렬 실행).
잠금 대상은 임의의 객체를 지정할 수 있으며 (this,클래스.class, 별도 객체),synchronized (new Object())는 매번 새 객체라 동기화 효과가 없다 (각 스레드가 다른 락).
임계 영역을 너무 넓게 잡으면 불필요한 부분까지 직렬화 되어 병렬성이 떨어지고, 너무 좁으면 데이터 일관성 이 깨질 수 있다 — 적절한 범위가 핵심이다.
실무에서는private final락 객체 를 별도로 두어 외부에서 락에 간섭하지 못하게 하고 (캡슐화), 여러 독립 자원에 여러 락 객체 를 두어 세밀하게 동기화한다.
synchronized 메서드 = 회의실 전체 잠금:
- 회의실 들어가는 순간부터 나갈 때까지
- 자료 정리, 발표, 토론 전부 동안 잠금
- 다른 사람 못 들어옴 (비효율)
synchronized 블록 = 금고만 잠금:
- 회의실은 자유 (병렬)
- 금고 (공유 자원) 사용할 때만 잠금
- 짧게 → 효율적
잠금 대상 명시:
- 어느 금고 (락 객체) 인지 지정
- 금고 A, 금고 B 따로
new Object() 매번 = 매번 새 금고:
- 각자 새 금고 → 잠금 의미 없음
- 서로 다른 금고라 충돌 X (잘못)
private final 락 = 전용 금고 열쇠:
- 외부에서 못 건드림
- 안전한 동기화
→ synchronized 블록 = 필요한 부분만 잠금, 잠금 대상 명시.
1. synchronized 블록의 정의
2. 동기화 범위 최소화
3. 잠금 대상 명시
4. synchronized (this) vs new Object()
5. 너무 넓은 임계 영역의 문제
6. 메서드 vs 블록 선택
7. private final 락 객체
8. 여러 락 객체 (세밀한 동기화)
9. 면접 + 자기 점검
public void increment() {
// 비동기 영역 (병렬)
doSomethingElse();
synchronized (lock) { // 임계 영역만
count++;
}
// 비동기 영역 (병렬)
doMore();
}
synchronized (락_객체) {
// 임계 영역
}
// 락_객체:
// - this (현재 인스턴스)
// - 클래스.class (Class 객체)
// - 별도 객체 (private final Object lock)
synchronized 블록 동작:
블록 진입 시:
- 락 객체의 모니터 락 획득
- 성공 → 진입
- 실패 → BLOCKED
블록 종료 시:
- 락 자동 반납
→ 블록 내부만 동기화
// synchronized 메서드 (전체)
public synchronized void method() {
heavyWork(); // 락 안 (불필요)
count++; // 임계 영역
moreWork(); // 락 안 (불필요)
}
// synchronized 블록 (일부)
public void method() {
heavyWork(); // 락 밖 (병렬)
synchronized (this) {
count++; // 임계 영역만
}
moreWork(); // 락 밖 (병렬)
}
synchronized 블록의 효과:
메서드와 동일한 효과:
- 상호 배제
- 가시성
- 원자성
추가:
- 범위 최소화
- 잠금 대상 선택
- 성능 ↑
@Service
public class ShipmentBlockExample {
private int counter = 0;
private final Object lock = new Object();
public void process(Shipment shipment) {
// 비임계 영역 (병렬)
BigDecimal freight = calculateFreight(shipment); // 무거운 계산
validateShipment(shipment); // 검증
// 임계 영역만 (짧게)
synchronized (lock) {
counter++;
}
// 비임계 영역 (병렬)
repository.save(shipment); // DB 저장
}
private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
private void validateShipment(Shipment s) { }
}
synchronized 블록의 정의는?
답:
1. 정의:
문법:
동작:
효과:
동기화 범위 최소화:
임계 영역 (공유 자원 접근) 만 동기화.
나머지는 락 밖에서 (병렬).
목적:
- 병렬성 ↑
- 락 점유 시간 ↓
- 성능 ↑
범위 최소화의 효과:
넓은 범위 (메서드 전체):
- 모든 스레드 직렬화
- 무거운 작업도 대기
- 병목
좁은 범위 (블록):
- 임계 영역만 직렬화
- 무거운 작업 병렬
- 처리량 ↑
// ❌ 넓은 범위 (느림)
public synchronized void processWide(Shipment shipment) {
BigDecimal freight = expensiveCalc(shipment); // 1초 (락 안)
counter++; // 즉시
repository.save(shipment); // 0.5초 (락 안)
// 전체 1.5초 동안 다른 스레드 대기
}
// ✓ 좁은 범위 (빠름)
public void processNarrow(Shipment shipment) {
BigDecimal freight = expensiveCalc(shipment); // 1초 (병렬)
synchronized (lock) {
counter++; // 즉시 (락 안, 짧게)
}
repository.save(shipment); // 0.5초 (병렬)
// 임계 영역만 직렬화 (counter++)
}
범위 비교 (3 스레드):
넓은 범위:
A: [████████████] 1.5초
B: [████████████] 1.5초 (대기 후)
C: [████████████]
전체: 4.5초 (직렬)
좁은 범위:
A: [계산 병렬][lock][저장 병렬]
B: [계산 병렬][lock][저장 병렬]
C: [계산 병렬][lock][저장 병렬]
전체: ~1.5초 (계산/저장 병렬, lock 만 직렬)
너무 좁으면 안 됨:
데이터 일관성 필요한 부분은
함께 동기화.
예:
// ❌ 너무 좁음 (일관성 깨짐)
synchronized (lock) { check(); }
synchronized (lock) { act(); }
// check 와 act 사이 다른 스레드 개입
// ✓ 일관성 필요하면 함께
synchronized (lock) {
check();
act(); // check-then-act 원자적
}
@Service
public class ShipmentRangeOptimization {
private int stock = 100;
private final Object lock = new Object();
// ✓ 적절한 범위
public boolean reserve(Shipment shipment, int quantity) {
// 비임계 — 병렬
validateShipment(shipment);
logRequest(shipment);
// 임계 영역 (check-then-act 함께)
synchronized (lock) {
if (stock >= quantity) { // 확인
stock -= quantity; // 차감 (함께 — 일관성)
return true;
}
return false;
}
// ↑ check 와 act 를 분리하면 안 됨
}
public void process(Shipment shipment) {
// 무거운 작업 병렬
BigDecimal freight = calculate(shipment);
// 짧은 임계 영역
synchronized (lock) {
recordStats(freight);
}
// 무거운 작업 병렬
repository.save(shipment);
}
private void validateShipment(Shipment s) { }
private void recordStats(BigDecimal f) { }
}
동기화 범위 최소화는?
답:
1. 의미:
효과:
한계:
균형:
// 다양한 잠금 대상
// 1. this
synchronized (this) { }
// 2. Class 객체
synchronized (MyClass.class) { }
// 3. 별도 객체
private final Object lock = new Object();
synchronized (lock) { }
// 4. 공유 자원 자체
synchronized (sharedList) { }
public class Counter {
private int count = 0;
public void increment() {
synchronized (this) { // 인스턴스 락
count++;
}
}
// synchronized 메서드와 동일한 락 (this)
}
public class Counter {
private static int count = 0;
public void increment() {
synchronized (Counter.class) { // Class 락
count++;
}
}
// static synchronized 와 동일한 락
}
public class Counter {
private int count = 0;
private final Object lock = new Object(); // 전용 락
public void increment() {
synchronized (lock) { // 전용 락
count++;
}
}
// this 와 독립적
// 외부에서 간섭 불가
}
public class MultiResource {
private int countA = 0;
private int countB = 0;
private final Object lockA = new Object();
private final Object lockB = new Object();
public void incrementA() {
synchronized (lockA) {
countA++; // A 만 (B 와 독립)
}
}
public void incrementB() {
synchronized (lockB) {
countB++; // B 만
}
}
// incrementA 와 incrementB 동시 가능 (다른 락)
}
@Service
public class ShipmentLockTargets {
private int shipmentCount = 0;
private BigDecimal totalRevenue = BigDecimal.ZERO;
// 독립적 자원 → 다른 락
private final Object countLock = new Object();
private final Object revenueLock = new Object();
public void incrementCount() {
synchronized (countLock) { // count 전용 락
shipmentCount++;
}
}
public void addRevenue(BigDecimal amount) {
synchronized (revenueLock) { // revenue 전용 락
totalRevenue = totalRevenue.add(amount);
}
}
// count 와 revenue 동시 갱신 가능 (다른 락)
// 세밀한 동기화
}
잠금 대상 명시는?
답:
1. 방법:
this:
별도 객체:
여러 락:
synchronized (this) vs synchronized (new Object()):
synchronized (this):
- 같은 객체의 this 락
- 여러 스레드 공유
- 정상 동기화
synchronized (new Object()):
- 매번 새 객체 (다른 락)
- 각 스레드 다른 락
- 동기화 효과 없음! (버그)
// ❌ 동기화 효과 없음
public void brokenSync() {
synchronized (new Object()) { // 매번 새 객체!
count++;
// 각 스레드가 다른 락
// → 서로 배타 X
// → 동기화 안 됨
}
}
// 스레드 A: new Object() → 락 A
// 스레드 B: new Object() → 락 B (다름!)
// → 동시 진입 (배타 X)
// → count++ 손실
new Object() 가 효과 없는 이유:
synchronized 는 같은 락 객체를 공유해야 배타.
new Object() 매번:
- 각 호출이 다른 객체
- 다른 락
- 서로 배타 안 됨
핵심:
- 락은 공유되어야 의미
- 매번 새 객체 = 공유 X
// ✓ 공유 락 객체
public class Correct {
private int count = 0;
private final Object lock = new Object(); // 한 번 생성, 공유
public void increment() {
synchronized (lock) { // 공유 락
count++;
}
}
// 모든 스레드가 같은 lock
// → 배타 → 동기화 O
}
synchronized (lock) — 공유:
스레드 A ─┐
스레드 B ─┼→ 같은 lock 객체 (배타)
스레드 C ─┘
synchronized (new Object()) — 매번 새로:
스레드 A → 락 A
스레드 B → 락 B (서로 다름)
스레드 C → 락 C
→ 배타 X (동기화 안 됨)
@Service
public class LockObjectExample {
private int counter = 0;
// ❌ 버그 — 매번 새 객체
public void incrementBroken() {
synchronized (new Object()) { // 효과 없음!
counter++; // 손실 발생
}
}
// ✓ 올바름 — 공유 락
private final Object lock = new Object();
public void incrementCorrect() {
synchronized (lock) { // 공유 락
counter++; // 안전
}
}
// ✓ this 도 가능
public void incrementThis() {
synchronized (this) { // this 공유
counter++;
}
}
}
synchronized (this) vs new Object()?
답:
1. this:
new Object():
이유:
올바름:
너무 넓은 임계 영역:
불필요한 코드까지 동기화.
문제:
- 직렬화 (병렬성 ↓)
- 락 점유 시간 ↑
- 처리량 ↓
- 데드락 위험 ↑
// ❌ 너무 넓음
public void processWide(Shipment shipment) {
synchronized (lock) {
// 무거운 작업 (락 불필요)
BigDecimal freight = expensiveCalc(shipment); // 1초
Tracking tracking = callApi(shipment); // 2초 (I/O)
// 실제 임계 영역
counter++;
// 무거운 작업 (락 불필요)
repository.save(shipment); // 0.5초
}
// 3.5초 동안 락 점유 → 다른 스레드 대기
}
I/O 를 락 안에 두면:
- 네트워크/DB 대기 동안 락 점유
- 다른 스레드 모두 BLOCKED
- 응답 지연
- 처리량 급감
원칙:
- I/O 는 락 밖에서
- 락 안에는 메모리 연산만
// ✓ 범위 최소화
public void processNarrow(Shipment shipment) {
// 무거운 작업 (락 밖, 병렬)
BigDecimal freight = expensiveCalc(shipment);
Tracking tracking = callApi(shipment);
// 임계 영역만 (메모리 연산)
synchronized (lock) {
counter++;
}
// 무거운 작업 (락 밖, 병렬)
repository.save(shipment);
}
넓은 범위 + 여러 락 = 데드락 위험:
// ❌ 위험
synchronized (lockA) {
doSomething();
synchronized (lockB) { // 중첩 (데드락 가능)
...
}
}
// 다른 스레드:
synchronized (lockB) {
synchronized (lockA) { // 반대 순서 (데드락!)
...
}
}
핵심:
- 락 점유 길수록 데드락 ↑
- 중첩 락 순서 주의
@Service
public class WideCriticalSectionProblem {
private int counter = 0;
private final Object lock = new Object();
// ❌ I/O 를 락 안에 (위험)
public void processBad(Shipment shipment) {
synchronized (lock) {
Tracking tracking = trackingApi.fetch(shipment.getBlNo()); // 2초 I/O
// 모든 스레드가 2초씩 대기
counter++;
repository.save(shipment); // 0.5초 I/O
}
}
// ✓ I/O 는 락 밖
public void processGood(Shipment shipment) {
Tracking tracking = trackingApi.fetch(shipment.getBlNo()); // 병렬
synchronized (lock) {
counter++; // 메모리 연산만
}
repository.save(shipment); // 병렬
}
}
너무 넓은 임계 영역의 문제는?
답:
1. 문제:
I/O 위험:
데드락:
원칙:
synchronized 메서드 vs 블록:
메서드 전체가 임계 영역:
→ synchronized 메서드
일부만 임계 영역:
→ synchronized 블록 (성능 ↑)
잠금 대상 제어 필요:
→ synchronized 블록
// 메서드 전체가 임계 영역
public synchronized void transfer(int amount) {
// 모든 코드가 동기화 필요
balance -= amount;
history.add(new Transaction(amount));
updateTimestamp();
// 전체 일관성
}
// 블록으로 나눌 이유 없음
// 일부만 임계 영역
public void process(Shipment shipment) {
BigDecimal freight = calculate(shipment); // 락 불필요
synchronized (lock) {
counter++; // 이 부분만 임계 영역
}
save(shipment); // 락 불필요
}
// 블록으로 범위 최소화
| 기준 | 메서드 | 블록 |
|---|---|---|
| 범위 | 전체 | 일부 |
| 잠금 대상 | this/Class 고정 | 자유 |
| 가독성 | 간결 | 명시적 |
| 성능 | 넓음 (느림) | 좁음 (빠름) |
| 적합 | 전체 임계 | 일부 임계 |
선택 가이드:
1. 전체가 임계 영역?
- Yes → 메서드
- No → 블록
2. 잠금 대상 제어?
- 필요 → 블록 (별도 락)
- 불필요 → 메서드 (this)
3. 성능 중요?
- Yes → 블록 (범위 최소화)
대부분:
- 블록 권장 (범위 제어)
- 또는 Atomic/동시성 컬렉션
@Service
public class MethodVsBlockChoice {
private int counter = 0;
private final Object lock = new Object();
private BigDecimal balance = BigDecimal.ZERO;
// 메서드 — 전체가 임계 영역
public synchronized void transfer(BigDecimal amount) {
balance = balance.subtract(amount);
recordTransaction(amount);
// 전체 일관성 (메서드 적합)
}
// 블록 — 일부만 임계 영역
public void processWithStats(Shipment shipment) {
BigDecimal freight = calculate(shipment); // 병렬
synchronized (lock) {
counter++; // 임계 영역만
}
repository.save(shipment); // 병렬
// 블록 적합 (범위 최소화)
}
private void recordTransaction(BigDecimal amount) { }
private BigDecimal calculate(Shipment s) { return s.getWeight(); }
}
메서드 vs 블록 선택은?
답:
1. 메서드:
블록:
기준:
권장:
public class SafeCounter {
private int count = 0;
private final Object lock = new Object(); // 전용 락
public void increment() {
synchronized (lock) {
count++;
}
}
}
private final 락 객체의 이유:
private:
- 외부에서 접근 불가
- 외부가 락 못 건드림
final:
- 락 객체 불변
- 다른 객체로 교체 X
- 일관된 락
// ❌ this 잠금의 위험
public class RiskyThis {
public synchronized void method() { // this 락
count++;
}
}
// 외부에서:
RiskyThis obj = new RiskyThis();
synchronized (obj) { // ★ 외부가 같은 락 점유!
// 오래 점유하면
// obj.method() 가 영원히 대기
}
// 외부 코드가 동기화 간섭
// ✓ 전용 락 (안전)
public class SafeLock {
private int count = 0;
private final Object lock = new Object(); // private
public void method() {
synchronized (lock) { // 외부가 모르는 락
count++;
}
}
}
// 외부에서:
SafeLock obj = new SafeLock();
// synchronized (obj.lock) // ❌ 접근 불가 (private)
// 외부가 간섭 못 함
// 락 객체 명명 패턴
public class LockPatterns {
// 단일 락
private final Object lock = new Object();
// 여러 락 (자원별)
private final Object readLock = new Object();
private final Object writeLock = new Object();
// 의미 있는 이름
private final Object stockLock = new Object();
private final Object historyLock = new Object();
// final 필수 (불변)
// private 필수 (캡슐화)
}
@Service
public class ShipmentPrivateLock {
private int counter = 0;
private final List<Shipment> history = new ArrayList<>();
// 전용 락 (private final)
private final Object counterLock = new Object();
private final Object historyLock = new Object();
public void incrementCounter() {
synchronized (counterLock) { // 외부 간섭 X
counter++;
}
}
public void addHistory(Shipment shipment) {
synchronized (historyLock) { // 독립 락
history.add(shipment);
}
}
// 외부에서 락에 접근 불가
// 안전한 동기화
// counter 와 history 독립 (다른 락)
}
private final 락 객체를 쓰는 이유는?
답:
1. private:
final:
this 위험:
전용 락:
여러 락 객체:
독립적 자원에 별도 락.
효과:
- 세밀한 동기화
- 병렬성 ↑
- 불필요한 경합 방지
// ❌ 단일 락 (불필요한 경합)
public class SingleLock {
private int countA = 0;
private int countB = 0;
private final Object lock = new Object();
public void incA() {
synchronized (lock) { // A 갱신에 lock
countA++;
}
}
public void incB() {
synchronized (lock) { // B 갱신도 같은 lock!
countB++;
}
}
// A 와 B 는 독립인데 같은 락
// → incA, incB 동시 불가 (불필요한 경합)
}
// ✓ 락 분리 (세밀)
public class SeparateLocks {
private int countA = 0;
private int countB = 0;
private final Object lockA = new Object();
private final Object lockB = new Object();
public void incA() {
synchronized (lockA) { // A 전용
countA++;
}
}
public void incB() {
synchronized (lockB) { // B 전용
countB++;
}
}
// A 와 B 독립 → incA, incB 동시 가능
// 병렬성 ↑
}
// Lock Striping — 여러 락으로 분할
public class StripedMap {
private static final int STRIPES = 16;
private final Object[] locks = new Object[STRIPES];
private final Map<String, String>[] segments;
public StripedMap() {
for (int i = 0; i < STRIPES; i++) {
locks[i] = new Object();
}
// ... segments 초기화
}
public void put(String key, String value) {
int stripe = Math.abs(key.hashCode() % STRIPES);
synchronized (locks[stripe]) { // 키별 다른 락
segments[stripe].put(key, value);
}
}
// 다른 키 → 다른 락 → 동시 가능
// ConcurrentHashMap 의 아이디어
}
// 여러 락 사용 시 데드락 주의
// ❌ 락 순서 불일치
public void transfer(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) { // 순서: from → to
// ...
}
}
}
// 스레드 A: transfer(acc1, acc2) → acc1 → acc2
// 스레드 B: transfer(acc2, acc1) → acc2 → acc1
// → 데드락! (순환 대기)
// ✓ 일관된 순서 (예: id 순)
public void transferSafe(Account from, Account to, int amount) {
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) { // 항상 id 순
// ...
}
}
}
@Service
public class ShipmentMultiLock {
private final Map<String, BigDecimal> revenueByRoute = new HashMap<>();
private final Map<String, Integer> countByRoute = new HashMap<>();
// 라우트별 락 (세밀)
private final Map<String, Object> routeLocks = new ConcurrentHashMap<>();
private Object getLock(String route) {
return routeLocks.computeIfAbsent(route, k -> new Object());
}
public void record(String route, BigDecimal revenue) {
Object lock = getLock(route);
synchronized (lock) { // 라우트별 락
revenueByRoute.merge(route, revenue, BigDecimal::add);
countByRoute.merge(route, 1, Integer::sum);
}
// 다른 라우트 → 다른 락 → 동시 가능
}
// 실무: ConcurrentHashMap + AtomicReference 권장
}
여러 락 객체로 세밀한 동기화는?
답:
1. 락 분리:
단일 락 문제:
Lock Striping:
데드락 주의:
| Q | 핵심 답변 |
|---|---|
| synchronized 블록? | 일부만 동기화 |
| 범위 최소화? | 임계 영역만, 성능 ↑ |
| 잠금 대상? | this/Class/별도 객체 |
| new Object() 매번? | 효과 없음 (다른 락) |
| 넓은 범위 문제? | 직렬화, I/O 위험 |
| 메서드 vs 블록? | 전체 vs 일부 |
| private final 락? | 외부 간섭 방지 |
| 여러 락? | 세밀한 동기화 |
| Lock Striping? | 분할 락 (ConcurrentHashMap) |
| 데드락 방지? | 락 순서 일관 |
답:
답:
답:
답:
if (instance == null) { // 1차 (락 없이)
synchronized (lock) {
if (instance == null) { // 2차 (락 안)
instance = new ...;
}
}
}
// volatile 필수 (가시성)
답:
1. synchronized 블록
2. 주의점
3. 세밀한 동기화
이번 Unit에서 synchronized 블록을 봤다면, 다음은 모니터 락의 내부 동작 (★ 마스터, 면접 단골).
🚀 Phase 4 — synchronized & volatile (★ 1차 정점)
✅ Unit 4.1 임계 영역과 동기화의 필요성
✅ Unit 4.2 synchronized 메서드
✅ Unit 4.3 synchronized 블록 ← 여기
⏭ Unit 4.4 모니터 락 (★ 마스터)
⏭ Unit 4.5 volatile (★ 마스터)
✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
✅ Phase 3 — 스레드 다루기 (5 Unit)
🚀 Phase 4 — synchronized & volatile (3/5 진행) ★ 1차 정점
총: 15/35 Unit