트릭/동기화/싱크로(일단 패스)

류한선·2025년 6월 23일

실기연습-2

목록 보기
30/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의 최종 값은 확정적


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


🎯 전체 코드 다시 보기

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 일 끝나면 알려줘"라고 말하고 기다림.

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


좋아요! 🔥 이번엔 wait/notify 트릭deadlock 트릭 두 가지를 준비하고, 한 줄 한 줄 디버깅하면서 상황을 묘사해 드릴게요.


🎯 트릭 1: wait/notify 문제

이 문제는 wait()notify()의 타이밍 트릭을 보여줍니다.

🔥 코드

class Shared {
    synchronized void doWait() {
        System.out.println(Thread.currentThread().getName() + " waiting...");
        try {
            wait();  // 트릭: synchronized 블록 안에서 호출됨
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " resumed!");
    }

    synchronized void doNotify() {
        System.out.println(Thread.currentThread().getName() + " notifying...");
        notify();  // 트릭: 대기 중인 하나의 쓰레드를 깨움
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Shared shared = new Shared();

        Thread waiter = new Thread(() -> shared.doWait(), "Waiter");
        Thread notifier = new Thread(() -> {
            try {
                Thread.sleep(1000); // 트릭: Waiter가 대기할 시간을 줌
                shared.doNotify();
            } catch (InterruptedException e) { }
        }, "Notifier");

        waiter.start();
        notifier.start();

        waiter.join();
        notifier.join();
    }
}

🧠 한 줄 한 줄 디버깅

waiter.start()

  • "Waiter" 쓰레드 시작 → doWait() 호출
  • synchronized lock 잡고
  • Waiter waiting... 출력
  • wait() 호출 → 현재 쓰레드가 대기 상태로 전환 + lock 반납!

notifier.start()

  • "Notifier" 쓰레드 시작
  • 1초 쉬기 (sleep)
  • 깨어난 후 doNotify() 호출
  • synchronized lock 잡고
  • Notifier notifying... 출력
  • notify() 호출 → 대기중인 Waiter를 깨움
  • notify 끝나면 lock 반환

Waiter 쓰레드 깨어남

  • lock을 다시 잡고 wait() 이후의 코드를 계속 실행
  • Waiter resumed! 출력
  • 종료

🎯 최종 출력 (순서 고정됨)

Waiter waiting...
Notifier notifying...
Waiter resumed!

🔥 트릭 포인트

wait()는 반드시 synchronized 블록 안에서 호출
wait() 호출하면 lock 반납
notify() 호출해도 wait한 쓰레드가 바로 실행되는 건 아님. notify 종료 후 lock 반환해야 wait한 쓰레드가 깨어나 진행 가능
✅ 반드시 join()해서 main이 끝나지 않도록 대기



🎯 트릭 2: Deadlock 문제

이번엔 두 쓰레드가 서로 다른 lock을 잡고 교착 상태에 빠지는 트릭!

🔥 코드

class DeadlockDemo {
    final Object lockA = new Object();
    final Object lockB = new Object();

    public void thread1() {
        synchronized (lockA) {
            System.out.println("Thread-1 잡은 lockA, lockB 대기중...");
            try { Thread.sleep(500); } catch (InterruptedException e) {}
            synchronized (lockB) {
                System.out.println("Thread-1 잡은 lockB");
            }
        }
    }

    public void thread2() {
        synchronized (lockB) {
            System.out.println("Thread-2 잡은 lockB, lockA 대기중...");
            try { Thread.sleep(500); } catch (InterruptedException e) {}
            synchronized (lockA) {
                System.out.println("Thread-2 잡은 lockA");
            }
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        DeadlockDemo demo = new DeadlockDemo();

        Thread t1 = new Thread(() -> demo.thread1(), "Thread-1");
        Thread t2 = new Thread(() -> demo.thread2(), "Thread-2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Main 종료");
    }
}

🧠 한 줄 한 줄 디버깅

t1.start()

  • Thread-1 → thread1() 호출
  • synchronized (lockA) 잡음
  • "Thread-1 잡은 lockA, lockB 대기중..." 출력
  • 500ms 잠깐 쉬기

t2.start()

  • Thread-2 → thread2() 호출
  • synchronized (lockB) 잡음
  • "Thread-2 잡은 lockB, lockA 대기중..." 출력
  • 500ms 쉬기

500ms 후:

  • Thread-1 → 이제 lockB 잡으려 함 (하지만 lockB는 Thread-2가 잡고 있음) → 대기
  • Thread-2 → 이제 lockA 잡으려 함 (하지만 lockA는 Thread-1이 잡고 있음) → 대기

🎯 Deadlock 상태

  • Thread-1은 lockB 기다림
  • Thread-2는 lockA 기다림
  • 둘 다 상대방 lock을 잡을 때까지 무한 대기
  • 프로그램 종료 안됨 → main은 join()에서 대기중

🎯 트릭 포인트

✅ 두 쓰레드가 lock 순서를 반대로 잡으면 deadlock 발생
✅ 이런 문제를 예방하기 위해선 반드시 lock 잡는 순서 고정하거나, tryLock()등을 쓰거나, 동시성 유틸 사용



💡 요약

wait() / notify() 트릭

  • 반드시 synchronized 안에서 호출
  • wait() 호출하면 lock 반납
  • notify() 호출해도 lock 반환해야 wait한 쓰레드가 진행
    ✅ Deadlock 트릭
  • lock 순서 꼬이면 교착 상태
  • 교착 상태 디버깅 시 lock 상태 파악 필수

좋아요! 🎯 지금 완전 초보자 입장에서, "synchronized(락), 대기중(wait), 깨어난다(notify)" 라는 단어 자체를 현실 비유를 섞어 하나도 안다고 가정하지 않고, 천천히 차근차근 쉽게 설명해 드릴게요. 🧡


🏠 비유: 화장실 열쇠 하나라고 생각해보자!

우리가 **동기화(synchronized)**나 **락(lock)**을 배울 때, 가장 쉬운 비유가 화장실 하나에 열쇠 하나 있는 상황이에요.

🚪 화장실(공유 자원)과 열쇠(락)

  • 화장실이 한 칸 있는데 여러 사람이 쓰려고 해요.
  • 하지만 화장실 열쇠는 딱 1개!
  • 누구든 화장실을 쓰려면 반드시 열쇠를 가져야 들어갈 수 있어요.

→ 프로그래밍에서 synchronized를 쓴다는 건,
이 화장실 열쇠를 잡고 있는 거랑 같아요.
여러 쓰레드(사람들)가 한 코드를 동시에 못 쓰도록 하는 거죠.


🧑‍🤝‍🧑 쓰레드 = 사람들

  • Thread-1, Thread-2 = 화장실 쓰려는 사람들

🧑‍🦰 synchronized(락 잡기)

이제 코드 한 줄:

synchronized (lockA) {
    // 중요한 작업
}
  • 이건 lockA라는 열쇠를 잡고 화장실 들어가는 것!
  • 안에 들어간 쓰레드는 작업 다 끝날 때까지 열쇠를 안 놓아요.
  • 작업 끝나면 나올 때 열쇠 놓아요.

🧑‍🦰 wait() (대기 중)

자, 화장실 안에 있는데 갑자기:

wait();

라고 하면?

→ 안에 있던 사람이 문 열고 나와서 열쇠 걸어두고 복도로 나가 대기하는 거예요.
(wait() 호출 = 화장실 열쇠 놓고 복도로 나가 기다리는 상태)

이 상태에서 사람(쓰레드)은 대기중(기다리는 중) 상태가 돼요.


🧑‍🦰 notify() (깨우기)

화장실 바깥 다른 사람이:

notify();

라고 호출해주면?

→ "대기중인 사람들 중 하나에게 화장실 쓰라고 알려주기"
(notify() = "야 이제 화장실 열쇠 잡고 들어가서 계속해!" 신호)


🧑‍🦰 대기중에서 깨어나다

wait() 상태였다가 notify() 호출받으면:
→ 기다리던 사람이 화장실 열쇠 잡고 다시 작업 계속!
→ 이 상태를 깨어난다(resumed) 라고 해요.


🎬 한 줄 정리

동작비유프로그래밍 동작
synchronized화장실 열쇠 잡고 안에 들어감공유 자원 lock 잡고 작업
wait()화장실 안에서 열쇠 놓고 나가 복도로 나가 기다림lock 놓고 대기 상태
notify()화장실 밖에서 대기중인 사람에게 "이제 써!"라고 알려줌대기중인 쓰레드를 깨움
resumed대기중이던 사람이 화장실 열쇠 잡고 다시 작업 시작wait() 끝나고 이후 코드 실행

🧠 예제와 함께 이해하기

짧은 코드로:

synchronized (shared) {
    System.out.println("Thread-1: 작업중");
    shared.wait();       // 열쇠 놓고 대기
    System.out.println("Thread-1: 깨어나 작업 이어가기");
}

다른 쓰레드:

synchronized (shared) {
    System.out.println("Thread-2: 대기중인 Thread-1 깨우기");
    shared.notify();     // 대기중인 애 하나 깨우기
}

🎯 정리

synchronized = 공유 자원(화장실) 하나를 한 번에 한 쓰레드만 쓸 수 있게 함
wait() = 일 하다가 lock 놓고 쉬기(대기 상태)
notify() = 대기중인 쓰레드에게 "이제 계속해"라고 알리기
✅ 깨어나면(wait가 끝나면) → 다시 lock 잡고 작업 재개


좋아요! 🔥 아주 천천히 하나씩 설명해 드릴게요.
지금 보여주신 이 코드 부분을 한 줄 한 줄, 초보자 입장에서 완전히 뜯어서 설명해 드릴게요. 😄


🎯 코드 다시 보기

class Shared {
    synchronized void doWait() {
        System.out.println(Thread.currentThread().getName() + " waiting...");
        try {
            wait();  // 트릭: synchronized 블록 안에서 호출됨
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " resumed!");
    }

    synchronized void doNotify() {
        System.out.println(Thread.currentThread().getName() + " notifying...");
        notify();  // 트릭: 대기 중인 하나의 쓰레드를 깨움
    }
}

이 코드에서 처음 보는 부분들 하나씩 알려드릴게요.


🧠 Thread.currentThread().getName()

🔍 Thread.currentThread() 란?

  • 현재 실행 중인 쓰레드를 가져오는 거예요.
  • 프로그램은 한 번에 여러 쓰레드가 일할 수 있는데, 각 쓰레드마다 자기 이름이 있거든요.
  • Thread.currentThread()라고 쓰면 **"지금 나(이 코드를 실행 중인 쓰레드) 자신"**을 나타냄.

🔍 getName()이란?

  • 현재 쓰레드의 이름을 알려줘요.
  • 쓰레드를 만들 때 이름을 지정하거나 지정 안 하면 JVM이 Thread-0, Thread-1처럼 이름을 붙여줘요.
  • 그래서 Thread.currentThread().getName()지금 이 코드를 실행 중인 쓰레드의 이름을 반환.

🎯 예:

만약 메인에서 쓰레드를 이렇게 만들었으면:

Thread t1 = new Thread(() -> shared.doWait(), "Worker-1");

→ 이 쓰레드 안에서 Thread.currentThread().getName() 하면 "Worker-1" 반환!

→ 출력 예:

Worker-1 waiting...

🧠 try { ... } catch (InterruptedException e) { ... }

🔍 try-catch란?

  • try 블록 안의 코드에서 **예외(exception)**가 발생할 수 있는 경우, 프로그램이 터지지 않도록 잡아서(catch) 처리해주는 구조예요.

🔍 InterruptedException이란?

  • wait()sleep() 같은 메서드는 "중간에 다른 쓰레드가 너를 깨울 수 있다"는 걸 컴파일러에게 알려주려고 반드시 InterruptedExceptionthrow 하도록 되어 있어요.
  • 그래서 wait()를 호출할 때 무조건 try-catchthrows로 감싸야 함.

🧠 e.printStackTrace()란?

  • 예외가 터지면 어디서 무슨 문제가 났는지 빨간 글씨로 보여주는 도우미.
  • 프로그램 디버깅할 때 "이 줄에서 에러 터졌어요" 하고 콘솔에 출력.

🎯 한 줄 요약

try { wait(); } catch (InterruptedException e) { e.printStackTrace(); }
wait() 호출할 때 혹시 중간에 끊기면(인터럽트되면) 프로그램 안 죽고 에러 메시지만 출력.


🧠 요약 정리

  1. Thread.currentThread().getName()
    → 현재 일하는 쓰레드 이름 출력 (예: "Thread-1")

  2. try { wait(); } catch (InterruptedException e) { ... }
    wait() 하려면 반드시 try-catch로 감싸야 함
    → 대기중에 다른 쓰레드가 "그만 기다려!"라고 인터럽트 날릴 수 있는데, 그때 InterruptedException 발생

  3. e.printStackTrace()
    → 인터럽트 예외가 터졌을 때 무슨 일이 있었는지 콘솔에 보여주기


좋아요! 🎯 아주 기초부터 **synchronized(동기화)**의 역할을 천천히 쉽게 설명해 줄게요. 😄


🔍 synchronized란 무엇인가?

synchronized 키워드는 한마디로 말하면:

"한 번에 한 쓰레드만 이 코드(또는 이 블록)에 들어올 수 있게 해 주는 열쇠"
라고 생각하면 돼요. 🔑


🧠 왜 synchronized를 쓰는가?

여러 쓰레드가 같은 데이터를 동시에 바꾸려고 하면 문제가 생길 수 있어요.

예를 들어:

int count = 0;

Thread t1 = new Thread(() -> count++);
Thread t2 = new Thread(() -> count++);

→ 이렇게 하면 두 쓰레드가 count++를 거의 동시에 호출하면서 count를 꼬이게 바꿀 수 있어요.
(이걸 레이스 컨디션(Race Condition) 이라고 함)


🧠 synchronized의 역할

이 문제를 막기 위해 synchronized를 쓰면:

  • 한 쓰레드가 synchronized 블록 안에서 일하는 동안
  • 다른 쓰레드들은 기다리게 됨
  • 따라서 한 번에 한 쓰레드만 안전하게 데이터를 수정하게 돼요.

🎨 비유

생각해 보자:

  • 화장실 한 칸(synchronized)
  • 화장실 열쇠 하나()
  • 사람이 들어가면(쓰레드가 블록 안에 들어감)
  • 열쇠를 잠그고(락 잡음)
  • 다른 사람은 열쇠가 비어 있을 때까지 밖에서 기다림(다른 쓰레드 대기)
  • 다 끝나면 나와서 열쇠 돌려줌(락 반환)
  • 다음 사람이 들어감(다른 쓰레드 진행)

🧠 synchronized 사용하는 방법 2가지

✅ 1) 메서드 앞에 붙이기

synchronized void increment() {
    count++;
}

→ 이 경우 this 객체를 락으로 사용.
→ 한 쓰레드가 increment() 호출 중이면 다른 쓰레드는 기다려야 함.


✅ 2) 특정 블록만 묶기

void increment() {
    synchronized(this) {
        count++;
    }
}

→ 이렇게 하면 synchronized(this) 블록 안쪽만 lock 걸고,
→ 다른 부분은 동기화 없이 자유롭게 동작.


🎯 정리

synchronized = 동시 접속 금지 도구
✅ 여러 쓰레드가 한꺼번에 공유 데이터를 만지면 안 될 때 씀
✅ 한 쓰레드만 들어가고 다른 쓰레드는 기다리도록 함


0개의 댓글