좋아요! 🔥 이제 Thread/동기화 트릭을 하나 준비해 드릴게요.
이 부분은 정보처리기사 실기보다는 심화된 자바 동기화 트릭에 가깝지만, 자바의 동작 원리를 깊이 이해하는 데 도움이 돼요.
이 트릭 문제를 풀면서 synchronized 블록, 정적 메서드 동기화, 인스턴스 동기화의 차이, race condition을 모두 잡을 수 있습니다. 😄
class Counter {
static int count = 0;
synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " increment: " + count);
}
static synchronized void staticIncrement() {
count++;
System.out.println(Thread.currentThread().getName() + " staticIncrement: " + count);
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
Counter c1 = new Counter();
Counter c2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
c1.increment(); // 트릭 1: c1 인스턴스에 동기화
c2.staticIncrement(); // 트릭 2: 클래스 레벨 동기화
}
}, "T1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
c2.increment(); // 트릭 3: c2 인스턴스에 동기화
c1.staticIncrement(); // 트릭 4: 클래스 레벨 동기화
}
}, "T2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("최종 count = " + Counter.count);
}
}
✅ synchronized가 인스턴스 메서드에 있으면 인스턴스별로 락
✅ static synchronized가 있으면 클래스 단위 락
✅ 서로 다른 인스턴스에서 synchronized 호출해도 충돌 없음
✅ static synchronized 호출은 전역적으로 하나만 호출 가능
✅ 출력값의 순서가 실행 환경마다 달라질 수 있음 → 하지만 마지막 count 값 계산 트릭
✅ count를 증가시키는 메서드가 6번 호출되므로 증가량 계산 트릭
각 스레드가 3번씩 아래를 호출:
increment() → 인스턴스 락 (c1과 c2가 달라 병렬 가능)staticIncrement() → 클래스 락 (동시에 호출 불가, 반드시 교대로 호출)최종적으로 count++가 호출되는 횟수:
12번 호출됨.T1 increment: 1
T1 staticIncrement: 2
T2 increment: 3
T2 staticIncrement: 4
T1 increment: 5
T1 staticIncrement: 6
T2 increment: 7
T2 staticIncrement: 8
T1 increment: 9
T1 staticIncrement: 10
T2 increment: 11
T2 staticIncrement: 12
최종 count = 12
⚠️ 출력 순서 자체는 스케줄러 상황에 따라 바뀜
⚠️ 하지만 최종 count 값은 반드시12
✅ 인스턴스 동기화(synchronized 메서드): 같은 객체에서만 락. 다른 인스턴스는 동시 호출 가능.
✅ 정적 동기화(static synchronized): 클래스 전체를 한 스레드만 호출 가능.
✅ 호출 순서가 섞여도, 동기화 때문에 데이터 정합성 보장 → race condition 없음
✅ 출력값 순서는 동기화 범위마다 다르므로 예측 불가하지만, count의 최종 값은 확정적
이런 트릭 문제를 보며 정리:
좋아요. 😄 방금 코드를 완전히 디버깅하듯 한 줄 한 줄 상황 보고 형식으로, 처음 보는 초보자도 이해할 수 있는 수준으로 꼼꼼히 설명해 드릴게요!
class Counter {
static int count = 0;
synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " increment: " + count);
}
static synchronized void staticIncrement() {
count++;
System.out.println(Thread.currentThread().getName() + " staticIncrement: " + count);
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
Counter c1 = new Counter();
Counter c2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
c1.increment();
c2.staticIncrement();
}
}, "T1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
c2.increment();
c1.staticIncrement();
}
}, "T2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("최종 count = " + Counter.count);
}
}
Counterstatic int count = 0;
→ 공유되는 정적 변수: 클래스가 하나이므로 어디서든 이 하나의 count를 공유.
synchronized void increment()this)에 락을 건다.c1.increment() 호출 중이면 c1 객체를 쓰는 다른 스레드가 increment 호출할 수 없음.static synchronized void staticIncrement()Counter.class) 자체에 락을 건다.Counter c1 = new Counter();
Counter c2 = new Counter();
→ 이제 c1, c2 두 개의 다른 인스턴스가 있음.
Thread t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
c1.increment(); // c1 객체 락
c2.staticIncrement(); // 클래스 락
}
}, "T1");
t1이 할 일:
c1.increment() → c1 인스턴스 락을 잡고 count++ 출력c2.staticIncrement() → 클래스 락을 잡고 count++ 출력Thread t2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
c2.increment(); // c2 객체 락
c1.staticIncrement(); // 클래스 락
}
}, "T2");
t2가 할 일:
c2.increment() → c2 락 잡고 count++ 출력c1.staticIncrement() → 클래스 락 잡고 count++ 출력c1.increment()와 c2.increment()는?increment() 부분에서 동시에 돌아가는 것 가능.staticIncrement() 부분은?스레드마다 루프가 3번 돌아요. 각 루프당:
스레드 2개 → 12번 호출 → count가 12 증가.
순서가 섞이는 이유:
increment() 호출 부분은 c1과 c2 락이 달라 동시 호출 가능staticIncrement() 부분은 반드시 한 번에 한 스레드만 호출 → 교대로 출력count의 최종값 = 12T1 increment: X, T2 increment: Y, T1 staticIncrement: Z, ... 이런 형태로 교차 출력됨.increment() → 인스턴스 락 → c1, c2 동시에 호출 가능staticIncrement() → 클래스 락 → 반드시 한 번에 하나좋아요! 🎯 이번에는 t1.start(), t2.start(), t1.join(), t2.join() 부분을 정말 초보자 입장에서 디버깅하면서 자세히 설명해 드릴게요.
t1.start()와 t2.start()가 하는 일t1.start()t1이라는 쓰레드를 "실행 준비 상태(Runnable)"로 만듦.t1의 run() 코드를 실행해줌.t2.start()t2 쓰레드를 실행 준비 상태로 만듦.t1.start()와 t2.start()를 호출하면 main 쓰레드와 t1, t2가 동시에 돌아가기 시작.
즉, main()에서 start 호출한 이후:
t1.join()과 t2.join()이 하는 일이 부분이 조금 트릭인데,
**"main 쓰레드가 t1, t2가 끝날 때까지 기다리는 것"**을 말해요.
t1.join()t2.join()t1.start() → t1 시작t2.start() → t2 시작t1.join() → main 쓰레드가 t1 끝날 때까지 대기t2.join() 호출 → t2 끝날 때까지 대기System.out.println()을 호출만약 join()을 안 쓰면?
→ main 쓰레드가 기다리지 않고 바로 끝나 버릴 수 있음.
→ 즉, count를 출력하기 전에 t1이나 t2가 아직 안 끝났을 수 있음.
→ 결과가 엉망이 되겠지!
t1.start() = 일꾼1에게 일 시작하라고 함.t2.start() = 일꾼2에게 일 시작하라고 함.t1.join() = 대장(메인) 입장에서 "일꾼1 일 끝나면 알려줘"라고 말하고 기다림.t2.join() = 대장(메인) 입장에서 "일꾼2 일 끝나면 알려줘"라고 말하고 기다림.→ 두 일꾼 일 끝나야 대장(메인)이 "최종 결과!"라고 말함.
이해되시나요? 🎯
더 디버깅하거나 추가 예제 원하시면 말씀해 주세요! 🚀