Files
java-examples/docs/Concurrency/02_synchronized.md

7.1 KiB

자바 스레드 동기화에 대한 설명

스레드 동기화는 멀티스레드 환경에서 여러 스레드가 공유 자원(예: 변수, 객체 등)에 동시에 접근할 때 발생할 수 있는 데이터 불일치 문제를 해결하기 위해 사용됩니다. 자바에서는 동기화를 통해 특정 코드 블록이나 메서드가 한 번에 하나의 스레드만 실행하도록 보장합니다. 이를 통해 **경쟁 조건(race condition)**을 방지하고, 데이터의 무결성을 유지할 수 있습니다.

자바에서 스레드 동기화를 구현하는 주요 방법은 synchronized 키워드와 java.util.concurrent 패키지의 도구(예: Lock, Semaphore 등)를 사용하는 것입니다. 아래에서는 synchronized 키워드를 중심으로 설명하고, 예시를 제공하겠습니다.


스레드 동기화의 필요성

예를 들어, 은행 계좌 잔액을 관리하는 프로그램에서 두 스레드가 동시에 잔액을 수정하려고 하면 문제가 발생할 수 있습니다. 동기화가 없으면 잔액이 잘못 계산될 가능성이 높습니다. 이를 경쟁 조건이라고 부르며, 동기화로 해결할 수 있습니다.


synchronized 키워드 사용 방법

자바에서 동기화는 두 가지 방식으로 적용됩니다:

  1. synchronized 메서드: 메서드 전체를 동기화하여 한 번에 하나의 스레드만 실행하도록 만듭니다.
  2. synchronized 블록: 특정 코드 블록만 동기화하여 더 세밀한 제어가 가능합니다.

예시 1: 동기화 없는 경우 (문제 발생)

아래는 동기화 없이 두 스레드가 계좌 잔액을 동시에 수정하는 예시입니다.

class BankAccount {
    private int balance = 1000; // 초기 잔액

    public void withdraw(int amount) {
        if (balance >= amount) {
            System.out.println(Thread.currentThread().getName() + " 출금 시도: " + amount);
            balance -= amount; // 잔액 감소
            System.out.println(Thread.currentThread().getName() + " 출금 후 잔액: " + balance);
        }
    }

    public int getBalance() {
        return balance;
    }
}

public class NoSyncExample {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();

        Thread t1 = new Thread(() -> {
            account.withdraw(800);
        }, "스레드1");

        Thread t2 = new Thread(() -> {
            account.withdraw(800);
        }, "스레드2");

        t1.start();
        t2.start();
    }
}
  • 출력 예시 (무작위 결과):
    스레드1 출금 시도: 800
    스레드2 출금 시도: 800
    스레드1 출금 후 잔액: 200
    스레드2 출금 후 잔액: -600
    
  • 문제: 두 스레드가 동시에 withdraw를 호출하면서 잔액이 음수가 되는 비정상적인 결과 발생. 이는 if (balance >= amount) 조건을 확인한 후 balance -= amount가 실행되기 전에 다른 스레드가 끼어들었기 때문입니다.

예시 2: synchronized 메서드로 동기화

withdraw 메서드에 synchronized를 적용하면 한 번에 하나의 스레드만 메서드를 실행할 수 있습니다.

class BankAccount {
    private int balance = 1000;

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            System.out.println(Thread.currentThread().getName() + " 출금 시도: " + amount);
            try {
                Thread.sleep(100); // 경쟁 조건 시뮬레이션
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + " 출금 후 잔액: " + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + " 잔액 부족");
        }
    }

    public int getBalance() {
        return balance;
    }
}

public class SyncMethodExample {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();

        Thread t1 = new Thread(() -> {
            account.withdraw(800);
        }, "스레드1");

        Thread t2 = new Thread(() -> {
            account.withdraw(800);
        }, "스레드2");

        t1.start();
        t2.start();
    }
}
  • 출력 예시:
    스레드1 출금 시도: 800
    스레드1 출금 후 잔액: 200
    스레드2 출금 시도: 800
    스레드2 잔액 부족
    
  • 설명: synchronized 덕분에 첫 번째 스레드가 withdraw를 완료할 때까지 두 번째 스레드가 대기하며, 잔액이 음수로 떨어지지 않음.

예시 3: synchronized 블록으로 동기화

메서드 전체가 아닌 특정 코드 블록만 동기화하려면 synchronized 블록을 사용합니다. 이때 동기화 대상 객체(락 객체)를 지정해야 합니다.

class BankAccount {
    private int balance = 1000;
    private final Object lock = new Object(); // 락 객체

    public void withdraw(int amount) {
        System.out.println(Thread.currentThread().getName() + " 출금 시도: " + amount);
        synchronized (lock) { // 동기화 블록
            if (balance >= amount) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + " 출금 후 잔액: " + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + " 잔액 부족");
            }
        }
    }

    public int getBalance() {
        return balance;
    }
}

public class SyncBlockExample {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();

        Thread t1 = new Thread(() -> {
            account.withdraw(800);
        }, "스레드1");

        Thread t2 = new Thread(() -> {
            account.withdraw(800);
        }, "스레드2");

        t1.start();
        t2.start();
    }
}
  • 출력 예시:
    스레드1 출금 시도: 800
    스레드2 출금 시도: 800
    스레드1 출금 후 잔액: 200
    스레드2 잔액 부족
    
  • 설명: synchronized (lock) 블록 내의 코드만 동기화되며, lock 객체를 통해 스레드 간 접근을 제어함. 메서드 전체를 동기화하는 것보다 유연성이 높음.

동기화의 장점과 주의점

  • 장점:
    • 공유 자원에 대한 안전한 접근 보장.
    • 데이터 무결성 유지.
  • 주의점:
    • 과도한 동기화는 성능 저하(데드락, 스레드 대기 시간 증가)를 유발할 수 있음.
    • 필요한 범위만 동기화하도록 설계해야 함.

결론

스레드 동기화는 멀티스레드 프로그래밍에서 필수적인 개념으로, 자바에서는 synchronized 키워드를 통해 간단히 구현할 수 있습니다. synchronized 메서드와 블록을 적절히 사용하면 경쟁 조건을 방지하고 안정적인 프로그램을 작성할 수 있습니다. 위 예시를 참고하여 실제 애플리케이션에서 동기화를 적용해 보세요!