trede는 나올지 안나올지 모름 일단 패스

류한선·2025년 6월 23일

실기연습-2

목록 보기
29/95

좋아요! 🔥 이제 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++가 호출되는 횟수:

  • T1: increment(3번) + staticIncrement(3번) = 6번
  • T2: increment(3번) + staticIncrement(3번) = 6번
    → 총 12번 호출됨.

🎯 최종 출력 (예시, 실행마다 Thread 이름 출력 순서는 섞임)

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의 최종 값은 확정적


💡 시험 대비 팁

이런 트릭 문제를 보며 정리:

  • 인스턴스 동기화 vs 클래스 동기화 차이
  • 동기화 안 된 메서드를 호출할 때의 문제점
  • 정적 메서드 동기화 트릭
  • 교착 상태(deadlock) 트릭 연습

좋아요. 😄 방금 코드를 완전히 디버깅하듯 한 줄 한 줄 상황 보고 형식으로, 처음 보는 초보자도 이해할 수 있는 수준으로 꼼꼼히 설명해 드릴게요!


🎯 전체 코드 다시 보기

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);
    }
}

🧠 코드 디버깅 단계별 설명

✅ 클래스 Counter

static int count = 0;

공유되는 정적 변수: 클래스가 하나이므로 어디서든 이 하나의 count를 공유.


synchronized void increment()

  • 인스턴스 메서드 동기화
  • 이 메서드를 호출할 때, 호출한 인스턴스(this)에 락을 건다.
  • c1.increment() 호출 중이면 c1 객체를 쓰는 다른 스레드가 increment 호출할 수 없음.
  • 하지만 c2.increment()는 다른 락을 쓰므로 동시 호출 가능.

static synchronized void staticIncrement()

  • 정적 메서드 동기화
  • 이건 클래스(Counter.class) 자체에 락을 건다.
  • 어떤 인스턴스(c1, c2)로 호출하든 이 정적 메서드 호출은 오직 한 번에 한 스레드만 가능.

✅ 메인에서 쓰레드 생성 부분

Counter c1 = new Counter();
Counter c2 = new Counter();

→ 이제 c1, c2 두 개의 다른 인스턴스가 있음.


✅ t1 쓰레드

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++ 출력
  • 이 두 작업을 3번 반복

✅ t2 쓰레드

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++ 출력
  • 이 두 작업을 3번 반복

🧠 동기화 트릭 이해하기

1) c1.increment()c2.increment()는?

  • 서로 다른 인스턴스의 락을 잡으므로 t1의 c1.increment()와 t2의 c2.increment()는 동시에 호출될 수 있다.
  • 즉 두 스레드가 increment() 부분에서 동시에 돌아가는 것 가능.

2) staticIncrement() 부분은?

  • 클래스 단위의 락이므로 반드시 한 스레드만 호출 가능.
  • 다른 스레드는 반드시 끝날 때까지 기다려야 함.

🧠 호출 횟수 계산하기

스레드마다 루프가 3번 돌아요. 각 루프당:

  • increment() 1번 + staticIncrement() 1번 호출
    → 한 쓰레드당 6번 호출됨.

스레드 2개 → 12번 호출 → count가 12 증가.


🧠 출력 순서 트릭

순서가 섞이는 이유:

  • increment() 호출 부분은 c1과 c2 락이 달라 동시 호출 가능
  • staticIncrement() 부분은 반드시 한 번에 한 스레드만 호출 → 교대로 출력
    → 따라서 출력이 섞일 수 있음!

🎯 최종적으로 보장되는 건:

  • count의 최종값 = 12
  • 출력 순서는 섞이지만, 위처럼 T1 increment: X, T2 increment: Y, T1 staticIncrement: Z, ... 이런 형태로 교차 출력됨.

🔥 한 줄 한 줄 요약:

  1. increment() → 인스턴스 락 → c1, c2 동시에 호출 가능
  2. staticIncrement() → 클래스 락 → 반드시 한 번에 하나
  3. 두 쓰레드가 각각 6번 호출 → count는 12까지 증가
  4. 출력 메시지는 쓰레드 이름과 현재 count 값 출력

좋아요! 🎯 이번에는 t1.start(), t2.start(), t1.join(), t2.join() 부분을 정말 초보자 입장에서 디버깅하면서 자세히 설명해 드릴게요.


🧠 t1.start()t2.start()가 하는 일

t1.start()

  • t1이라는 쓰레드를 "실행 준비 상태(Runnable)"로 만듦.
  • 이 말은 운영체제에게 **"이 쓰레드를 시작해 줘"**라고 요청하는 거야.
  • 운영체제 스케줄러가 정해진 시점에 t1run() 코드를 실행해줌.

t2.start()

  • t2 쓰레드를 실행 준비 상태로 만듦.
  • 이 쓰레드도 이제 별도로 돌아가기 시작!

💭 중요한 점

  • t1.start()t2.start()를 호출하면 main 쓰레드와 t1, t2가 동시에 돌아가기 시작.

  • 즉, main()에서 start 호출한 이후:

    • main 쓰레드
    • t1 쓰레드
    • t2 쓰레드
      세 개가 병렬(동시에) 돌아가는 거야!

🧠 t1.join()t2.join()이 하는 일

이 부분이 조금 트릭인데,
**"main 쓰레드가 t1, t2가 끝날 때까지 기다리는 것"**을 말해요.


t1.join()

  • main 쓰레드 입장에서:
    → "t1 쓰레드 끝날 때까지 나(main 쓰레드)는 여기서 멈춰!"
    → t1 쓰레드가 종료(run() 끝남)될 때까지 기다림.

t2.join()

  • main 쓰레드 입장에서:
    → "t2 쓰레드 끝날 때까지 나(main 쓰레드)는 여기서 멈춰!"
    → t2 쓰레드가 종료될 때까지 기다림.

🧠 정리: 실행 순서

  1. t1.start() → t1 시작
  2. t2.start() → t2 시작
  3. t1.join() → main 쓰레드가 t1 끝날 때까지 대기
  4. t1 끝나면 이제 t2.join() 호출 → t2 끝날 때까지 대기
  5. 두 쓰레드가 끝나면 main 쓰레드가 아래 System.out.println()을 호출

🎯 왜 이렇게 쓰나?

만약 join()을 안 쓰면?
→ main 쓰레드가 기다리지 않고 바로 끝나 버릴 수 있음.
→ 즉, count를 출력하기 전에 t1이나 t2가 아직 안 끝났을 수 있음.
→ 결과가 엉망이 되겠지!


🧠 간단 비유

  • t1.start() = 일꾼1에게 일 시작하라고 함.
  • t2.start() = 일꾼2에게 일 시작하라고 함.
  • t1.join() = 대장(메인) 입장에서 "일꾼1 일 끝나면 알려줘"라고 말하고 기다림.
  • t2.join() = 대장(메인) 입장에서 "일꾼2 일 끝나면 알려줘"라고 말하고 기다림.

→ 두 일꾼 일 끝나야 대장(메인)이 "최종 결과!"라고 말함.


이해되시나요? 🎯
더 디버깅하거나 추가 예제 원하시면 말씀해 주세요! 🚀

0개의 댓글