F-LAB JAVA · 4주차 · Phase 4 · 동기화: synchronized와 메모리 가시성
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
synchronized 메서드는 메서드 시그니처에
synchronized키워드를 붙여, 같은 객체에 대해 한 번에 하나의 스레드만 진입하도록 보장하는 동기화 방식이다.
인스턴스 메서드의 잠금 대상은this(해당 인스턴스) 이고, static synchronized 메서드의 잠금 대상은 그 클래스의Class객체 다 (인스턴스 무관).
한 스레드가 객체의 synchronized 메서드에 진입하면 그 객체의 모니터 락을 획득하고, 같은 객체의 다른 synchronized 메서드도 다른 스레드가 진입할 수 없다 (같은 락을 공유하기 때문).
synchronized 는 상호 배제뿐 아니라 가시성 도 보장하며 (락 해제 시 변경을 메인 메모리에 flush), 재진입 가능 (Reentrant) 하다 (같은 스레드는 이미 가진 락을 다시 획득 가능).
단점은 메서드 전체가 잠겨 동기화 범위가 넓고 (성능 저하), 잠금 대상을 세밀하게 제어할 수 없다는 점이다 (다음 Unit 의 synchronized 블록이 대안).
synchronized 메서드 = 화장실 열쇠 1개:
인스턴스 락 (this):
- 각 화장실 (객체) 에 열쇠 1개
- 들어가려면 열쇠 필요
- 한 명만 사용 (나머지 대기)
- 나오면 열쇠 반납
같은 객체의 여러 synchronized 메서드:
- 같은 화장실 열쇠 공유
- 메서드 A 사용 중이면
- 메서드 B 도 못 들어감 (같은 열쇠)
static 락 (Class):
- 건물 전체 마스터 열쇠
- 모든 인스턴스 무관하게 1개
- static 메서드는 마스터 열쇠
재진입:
- 열쇠 가진 사람은
- 안에서 다른 방도 (같은 열쇠로)
→ synchronized = 객체별 열쇠 1개, static 은 클래스 마스터 열쇠.
1. synchronized 메서드의 정의
2. 잠금 대상 (this)
3. 한 번에 하나의 스레드
4. static synchronized (Class 락)
5. 인스턴스 락 vs 클래스 락
6. 같은 클래스 다른 메서드 동시 호출
7. 가시성과 재진입성
8. synchronized 메서드의 단점
9. 면접 + 자기 점검
class Counter {
private int count = 0;
public synchronized void increment() { // synchronized 키워드
count++; // 임계 영역 보호
}
public synchronized int getCount() {
return count;
}
}
synchronized 메서드 동작:
메서드 진입 시:
- 객체의 모니터 락 획득 시도
- 성공 → 진입 (RUNNABLE)
- 실패 → 대기 (BLOCKED)
메서드 종료 시:
- 락 자동 반납
→ 한 번에 하나의 스레드만
synchronized 의 효과:
상호 배제:
- 한 번에 하나만 진입
- 경쟁 조건 방지
가시성:
- 변경이 다른 스레드에 보임
- 락 해제 시 flush
원자성:
- 메서드 전체가 원자적
- 중간 개입 X
class SafeCounter {
private int count = 0;
// synchronized — 안전
public synchronized void increment() {
count++; // read-modify-write 가 원자적
}
public synchronized int get() {
return count;
}
}
// 100 스레드 × 1000 증가 = 정확히 100,000
// ❌ 동기화 X
class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 손실 가능
}
}
// ✓ 동기화
class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++; // 안전
}
}
@Service
public class ShipmentStatistics {
private int totalProcessed = 0;
private BigDecimal totalRevenue = BigDecimal.ZERO;
// synchronized — 통계 안전
public synchronized void record(Shipment shipment) {
totalProcessed++;
totalRevenue = totalRevenue.add(shipment.getRevenue());
// 두 변수 일관성 보장
}
public synchronized Statistics getStatistics() {
return new Statistics(totalProcessed, totalRevenue);
// 일관된 스냅샷
}
record Statistics(int count, BigDecimal revenue) {}
}
synchronized 메서드의 정의는?
답:
1. 정의:
동작:
효과:
결과:
인스턴스 synchronized 메서드:
잠금 대상 = this (해당 인스턴스)
public synchronized void method() { }
≡
public void method() {
synchronized (this) { // this 락
...
}
}
this 락:
- 객체마다 모니터 락 1개
- synchronized 메서드 = this 락
- 같은 객체의 synchronized 들이 락 공유
따라서:
- 다른 객체는 다른 락 (독립)
- 같은 객체는 같은 락 (배타)
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
// 객체 2개
Counter c1 = new Counter();
Counter c2 = new Counter();
// 스레드 A: c1.increment() (c1 락)
// 스레드 B: c2.increment() (c2 락)
// → 다른 락, 동시 실행 가능 (독립)
// 스레드 C: c1.increment() (c1 락)
// → A 와 같은 락, 대기 (BLOCKED)
this 락:
객체 c1: 객체 c2:
┌─────────────┐ ┌─────────────┐
│ 모니터 락 c1 │ │ 모니터 락 c2 │
└─────────────┘ └─────────────┘
↑ ↑
c1.increment() c2.increment()
(c1 락) (c2 락)
c1 의 메서드끼리: 배타 (같은 락)
c1 vs c2: 독립 (다른 락)
// synchronized 메서드
public synchronized void method() {
count++;
}
// 동일한 의미 (명시적 블록)
public void method() {
synchronized (this) {
count++;
}
}
// 둘 다 this 락
public class ShipmentProcessor {
private int processedCount = 0;
// this 락
public synchronized void process(Shipment shipment) {
// 이 인스턴스의 락
processedCount++;
doProcess(shipment);
}
}
// 사용
ShipmentProcessor p1 = new ShipmentProcessor();
ShipmentProcessor p2 = new ShipmentProcessor();
// p1.process() 와 p2.process()
// → 다른 락 (독립, 동시 가능)
// p1.process() × 2 스레드
// → 같은 락 (배타, 하나씩)
// 주의: Spring 싱글톤이면 인스턴스 1개
// → 모든 요청이 같은 락 (배타)
synchronized의 잠금 대상은?
답:
1. 인스턴스 메서드:
this 락:
다른 객체:
같은 객체:
상호 배제 (Mutual Exclusion):
한 스레드가 synchronized 메서드 진입 시
같은 객체의 다른 스레드는 진입 불가.
원리:
- 모니터 락 1개
- 획득한 스레드만 진입
- 나머지 BLOCKED
synchronized 시나리오:
스레드 A: increment() 호출
→ c 의 락 획득
→ 진입 (RUNNABLE)
→ count++ 실행
스레드 B: increment() 호출 (동시)
→ c 의 락 없음 (A 보유)
→ BLOCKED (대기)
스레드 A: 메서드 종료
→ 락 반납
스레드 B:
→ 락 획득
→ 진입
한 번에 하나:
스레드 A: [락 획득][increment 실행][락 반납]
스레드 B: [BLOCKED 대기────────][락 획득][실행]
↑ A 반납 후
동시 실행 X (직렬화)
안전하지만 병렬성 ↓
// synchronized 대기 = BLOCKED
class SharedCounter {
private int count = 0;
public synchronized void increment() {
count++;
sleep(100); // 오래 점유
}
}
// 스레드 A: increment() 실행 중 (락 보유)
// 스레드 B: increment() 시도 → BLOCKED
// jstack 에서:
// "Thread-B" BLOCKED (on object monitor)
// waiting to lock <0x...>
synchronized 의 직렬화:
여러 스레드가 같은 객체 메서드:
- 한 번에 하나
- 사실상 순차 실행
- 병렬성 상실
성능 영향:
- 임계 영역 길수록 ↓
- 경합 심할수록 ↓
→ 범위 최소화 (다음 Unit)
@Service
public class ShipmentSequential {
private int counter = 0;
// synchronized — 직렬화
public synchronized void process(Shipment shipment) {
counter++;
// ❌ 무거운 작업도 락 안에 (나쁨)
callExternalApi(shipment); // 다른 스레드 모두 대기
repository.save(shipment); // 직렬화
// 모든 스레드가 하나씩 대기
// 병렬성 X
}
// 개선 — 범위 최소화 (Unit 4.3 에서)
public void processBetter(Shipment shipment) {
// 무거운 작업은 밖에서 (병렬)
callExternalApi(shipment);
repository.save(shipment);
// 임계 영역만 동기화
synchronized (this) {
counter++; // 짧게
}
}
private void callExternalApi(Shipment s) { }
}
한 번에 하나의 스레드 원리는?
답:
1. 상호 배제:
시나리오:
상태:
영향:
class Counter {
private static int staticCount = 0;
public static synchronized void incrementStatic() {
staticCount++;
}
}
static synchronized 잠금 대상:
= 그 클래스의 Class 객체
(Counter.class)
public static synchronized void method() { }
≡
public static void method() {
synchronized (Counter.class) {
...
}
}
Class 락:
- 클래스당 Class 객체 1개
- 모든 인스턴스 무관
- static synchronized 들이 공유
따라서:
- 인스턴스가 몇 개든 Class 락 1개
- 모든 static synchronized 가 배타
class Counter {
private static int count = 0;
public static synchronized void increment() {
count++; // Class 락
}
}
// 인스턴스 무관
Counter c1 = new Counter();
Counter c2 = new Counter();
// 스레드 A: Counter.increment() (또는 c1.increment())
// 스레드 B: Counter.increment() (또는 c2.increment())
// → 같은 Class 락 (배타)
// → 인스턴스 달라도 직렬화
Class 락:
Counter.class (Class 객체)
┌──────────────┐
│ Class 모니터 │ ← 클래스당 1개
└──────────────┘
↑
┌────────┼────────┐
│ │ │
c1.static c2.static Counter.static
(모두 같은 Class 락)
인스턴스 무관, 모두 배타
public class ShipmentIdGenerator {
private static long lastId = 0;
// static synchronized — Class 락
public static synchronized long nextId() {
return ++lastId; // 모든 인스턴스/스레드 공유
}
// 클래스 레벨 동기화
// 어떤 인스턴스에서 호출해도 같은 락
}
// 사용
long id1 = ShipmentIdGenerator.nextId();
long id2 = ShipmentIdGenerator.nextId();
// 고유 ID 보장 (Class 락)
// 실무: AtomicLong 권장
public class BetterIdGenerator {
private static final AtomicLong counter = new AtomicLong();
public static long nextId() {
return counter.incrementAndGet(); // 락 없이 원자적
}
}
static synchronized의 잠금 대상은?
답:
1. 잠금 대상:
Class 락:
배타:
동등:
| 항목 | 인스턴스 락 (this) | 클래스 락 (Class) |
|---|---|---|
| 대상 | 인스턴스 (this) | Class 객체 |
| 메서드 | synchronized | static synchronized |
| 범위 | 객체별 | 클래스 전체 |
| 인스턴스 | 객체마다 락 | 무관 (1개) |
인스턴스 락 ↔ 클래스 락:
서로 다른 락!
인스턴스 synchronized: this 락
static synchronized: Class 락
→ 동시 실행 가능 (다른 락)
class Counter {
private int instanceCount = 0;
private static int staticCount = 0;
public synchronized void incInstance() { // this 락
instanceCount++;
}
public static synchronized void incStatic() { // Class 락
staticCount++;
}
}
Counter c = new Counter();
// 스레드 A: c.incInstance() (this 락)
// 스레드 B: Counter.incStatic() (Class 락)
// → 다른 락, 동시 실행 가능!
인스턴스 락 vs 클래스 락:
객체 c: Counter.class:
┌──────────┐ ┌──────────┐
│ this 락 │ │ Class 락 │
└──────────┘ └──────────┘
↑ ↑
incInstance() incStatic()
(this 락) (Class 락)
서로 다른 락 → 동시 가능
// ❌ 혼동 — 인스턴스 + static 같은 변수?
class Confused {
private static int count = 0;
// 인스턴스 락으로 static 보호 (위험!)
public synchronized void increment() {
count++; // static 인데 this 락
}
// 여러 인스턴스가 각자 this 락
// → static count 보호 안 됨!
}
// ✓ static 은 static synchronized (또는 Class 락)
class Correct {
private static int count = 0;
public static synchronized void increment() {
count++; // Class 락으로 보호
}
}
public class LockComparison {
private int instanceData = 0;
private static int sharedData = 0;
// 인스턴스 데이터 — this 락
public synchronized void updateInstance() {
instanceData++; // this 락 (객체별)
}
// static 데이터 — Class 락
public static synchronized void updateShared() {
sharedData++; // Class 락 (전역)
}
// 주의: 둘은 독립
// updateInstance() 와 updateShared() 동시 가능
// ❌ 잘못 — static 을 this 락으로
public synchronized void wrongUpdate() {
sharedData++; // ★ this 락은 static 보호 못 함
// 여러 인스턴스 → 각자 락 → 경쟁
}
}
인스턴스 락 vs 클래스 락은?
답:
1. 인스턴스 락:
클래스 락:
독립:
주의:
질문:
같은 클래스의 다른 synchronized 메서드
둘이 동시 호출 가능한가?
답:
같은 객체면 불가 (같은 락 공유).
다른 객체면 가능.
class Account {
private int balance = 1000;
public synchronized void deposit(int amount) { // this 락
balance += amount;
}
public synchronized void withdraw(int amount) { // this 락 (같음!)
balance -= amount;
}
}
Account account = new Account();
// 스레드 A: account.deposit(100) (this 락)
// 스레드 B: account.withdraw(50) (this 락, 같음!)
// → 같은 락, 동시 불가
// → B 는 BLOCKED (A 끝날 때까지)
같은 객체의 synchronized 메서드들:
모두 this 락 사용.
→ 락 1개 공유.
deposit() 진입 시 this 락 획득.
withdraw() 도 this 락 필요.
→ 이미 deposit 이 보유.
→ withdraw 대기 (BLOCKED).
핵심:
- 메서드가 달라도 같은 락
- 하나만 진입
같은 객체의 다른 메서드:
account 의 this 락 (1개):
스레드 A: deposit() ─── this 락 획득
스레드 B: withdraw() ── this 락 대기 (BLOCKED)
↑ A 가 보유 중
메서드 다르지만 같은 락
→ 동시 불가
Account a1 = new Account();
Account a2 = new Account();
// 스레드 A: a1.deposit(100) (a1 의 this 락)
// 스레드 B: a2.withdraw(50) (a2 의 this 락, 다름!)
// → 다른 락, 동시 가능
class Mixed {
private int count = 0;
public synchronized void syncMethod() { // this 락
count++;
}
public void normalMethod() { // 락 없음
// 동기화 X
readSomething();
}
}
Mixed m = new Mixed();
// 스레드 A: m.syncMethod() (this 락)
// 스레드 B: m.normalMethod() (락 X)
// → 동시 가능 (normalMethod 는 락 안 씀)
// 단, normalMethod 가 count 접근 시 위험
@Service
public class ShipmentAccount {
private BigDecimal balance = BigDecimal.ZERO;
// 모두 this 락 (같은 객체면 배타)
public synchronized void charge(BigDecimal amount) {
balance = balance.add(amount);
}
public synchronized void refund(BigDecimal amount) {
balance = balance.subtract(amount);
}
public synchronized BigDecimal getBalance() {
return balance;
}
// charge, refund, getBalance 모두 같은 객체 this 락
// → 동시 호출 불가 (하나씩)
// → balance 일관성 보장
// Spring 싱글톤이면 모든 요청이 같은 인스턴스
// → 모든 메서드 직렬화 (안전하지만 병목 가능)
}
같은 클래스의 다른 synchronized 메서드 둘이 동시 호출 가능한가?
답:
1. 같은 객체:
이유:
다른 객체:
비동기화 메서드:
synchronized 의 가시성 보장:
락 획득 시:
- 메인 메모리에서 최신 값 읽기
락 해제 시:
- 변경을 메인 메모리에 flush
→ 다른 스레드가 변경 봄
→ happens-before 보장
class VisibilityExample {
private boolean flag = false;
private int data = 0;
public synchronized void write() {
data = 42;
flag = true;
// 락 해제 시 flush
}
public synchronized boolean read() {
// 락 획득 시 최신 읽기
if (flag) {
return data == 42; // true 보장
}
return false;
}
// synchronized 가 가시성 보장
}
재진입성 (Reentrant):
같은 스레드는 이미 가진 락을
다시 획득 가능.
→ 데드락 방지
→ 중첩 synchronized OK
class Reentrant {
public synchronized void outer() {
inner(); // 같은 객체의 다른 synchronized 호출
// 이미 this 락 보유 → inner 진입 OK
}
public synchronized void inner() {
// outer 가 이미 this 락 보유
// 같은 스레드라 재진입 가능
}
}
// outer() 호출:
// 1. this 락 획득
// 2. inner() 호출
// 3. this 락 이미 보유 → 재진입 (OK)
// 재진입 없으면 데드락 (자기 자신 대기)
재진입 메커니즘:
락에 카운트 + 소유 스레드.
획득: 카운트++
반납: 카운트--
같은 스레드 재획득: 카운트++
카운트 0 시 완전 반납
예:
outer 진입: 카운트 1
inner 진입: 카운트 2
inner 종료: 카운트 1
outer 종료: 카운트 0 (반납)
@Service
public class ReentrantExample {
private int count = 0;
// 재진입 — 중첩 synchronized
public synchronized void processAll(List<Shipment> shipments) {
// this 락 획득
for (Shipment s : shipments) {
processOne(s); // 같은 객체 synchronized 호출
// 이미 this 락 보유 → 재진입 OK
}
}
public synchronized void processOne(Shipment shipment) {
// processAll 에서 호출 시 재진입
count++;
doProcess(shipment);
}
// 재진입 없으면 데드락 (자기 락 대기)
// synchronized 는 재진입 가능 → 안전
private void doProcess(Shipment s) { }
}
가시성과 재진입성은?
답:
1. 가시성:
재진입성:
재진입 메커니즘:
효과:
synchronized 메서드의 단점:
1. 범위가 넓음
- 메서드 전체 잠금
- 불필요한 부분도 직렬화
2. 잠금 대상 고정
- this 또는 Class
- 세밀한 제어 X
3. 성능
- 직렬화 (병렬성 ↓)
- 락 오버헤드
4. 유연성 부족
- 타임아웃 X
- 인터럽트 X
- tryLock X (Phase 5)
// ❌ 메서드 전체 잠금 (넓음)
public synchronized void process(Shipment shipment) {
// 무거운 작업 (락 불필요)
BigDecimal freight = expensiveCalculation(shipment);
Tracking tracking = callExternalApi(shipment);
// 실제 임계 영역 (락 필요)
count++;
// 더 무거운 작업 (락 불필요)
repository.save(shipment);
// 전체가 동기화 → 다른 스레드 모두 대기
}
// ✓ 블록으로 범위 최소화 (Unit 4.3)
public void processBetter(Shipment shipment) {
BigDecimal freight = expensiveCalculation(shipment); // 병렬
Tracking tracking = callExternalApi(shipment); // 병렬
synchronized (this) {
count++; // 임계 영역만
}
repository.save(shipment); // 병렬
}
// synchronized 메서드 — this 또는 Class 고정
public synchronized void method() {
// this 락만
}
// 다른 락 객체 쓰고 싶으면?
// → synchronized 블록 (Unit 4.3)
private final Object lock = new Object();
public void method2() {
synchronized (lock) { // 별도 락
// ...
}
}
synchronized 의 유연성 부족:
- 타임아웃 불가 (무한 대기)
- 인터럽트 불가 (대기 중 깰 수 없음)
- tryLock 불가 (시도 후 포기 X)
- 공정성 보장 X
해결:
- ReentrantLock (Phase 5)
- lock(), tryLock(), lockInterruptibly()
synchronized vs Lock (Phase 5 예고):
synchronized:
+ 간단 (키워드)
+ 자동 반납
+ 재진입
- 범위 (메서드 전체)
- 유연성 부족
ReentrantLock:
+ 유연 (타임아웃, 인터럽트, tryLock)
+ 공정성 옵션
- try-finally 필수
- 복잡
@Service
public class SynchronizedDisadvantage {
private int counter = 0;
// ❌ 단점 — 넓은 범위
public synchronized void processWide(Shipment shipment) {
// 무거운 작업도 락 안에
callExternalApi(shipment); // 5초 (모든 스레드 대기)
counter++;
repository.save(shipment); // 1초 (직렬화)
}
// ✓ 개선 — 범위 최소화
public void processNarrow(Shipment shipment) {
callExternalApi(shipment); // 병렬
synchronized (this) {
counter++; // 짧은 임계 영역만
}
repository.save(shipment); // 병렬
}
// ✓✓ Atomic (락 없이)
private final AtomicInteger atomicCounter = new AtomicInteger();
public void processAtomic(Shipment shipment) {
callExternalApi(shipment);
atomicCounter.incrementAndGet(); // 락 없이 원자적
repository.save(shipment);
}
private void callExternalApi(Shipment s) { }
}
synchronized 메서드의 단점은?
답:
1. 넓은 범위:
잠금 대상 고정:
성능:
유연성 부족:
| Q | 핵심 답변 |
|---|---|
| synchronized 메서드? | 키워드, 한 번에 하나 |
| 인스턴스 락? | this |
| static 락? | Class 객체 |
| 같은 객체 다른 메서드? | 불가 (같은 락) |
| 다른 객체? | 가능 (다른 락) |
| 인스턴스 vs Class 락? | 독립 (동시 가능) |
| 가시성 보장? | 락 해제 시 flush |
| 재진입성? | 같은 스레드 재획득 |
| 대기 상태? | BLOCKED |
| 단점? | 범위 넓음, 유연성 부족 |
답:
답:
답:
답:
답:
1. synchronized 메서드
2. 락 공유
3. 특성과 단점
이번 Unit에서 synchronized 메서드를 봤다면, 다음은 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 (2/5 진행) ★ 1차 정점
총: 14/35 Unit