Daily log

Java 멀티스레드 환경에서 발생하는 데이터 경쟁 조건과 동기화 문제를 해결하기 위한 방법들을 알아봅니다. synchronized, volatile, Lock 인터페이스 등을 사용하여 안전하게 공유 자원에 접근하는 방법을 살펴봅니다.

왜 필요한가?

멀티스레드 프로그래밍은 여러 스레드가 동시에 실행되어 애플리케이션의 성능을 향상시키는 데 유용하지만, 동시에 여러 스레드가 공유 자원에 접근할 때 데이터 불일치, 경쟁 조건, 데드락과 같은 심각한 문제가 발생할 수 있습니다. 이러한 문제를 해결하기 위해 Java는 다양한 동기화 메커니즘을 제공합니다.

① 데이터 경쟁 (Race Condition)

두 개 이상의 스레드가 동시에 동일한 공유 자원에 접근하여 하나 이상의 스레드가 데이터를 변경하려고 할 때 데이터 경쟁이 발생합니다. 이로 인해 예상치 못한 결과가 발생할 수 있습니다. 예를 들어, 은행 계좌 잔액을 업데이트하는 상황을 생각해 봅시다.

java class Account { private int balance = 1000;

public int getBalance() {
    return balance;
}

public void withdraw(int amount) {
    balance -= amount;
}

}

public class RaceConditionExample { public static void main(String[] args) throws InterruptedException { Account account = new Account();

    Runnable task = () -> {
        for (int i = 0; i < 1000; i++) {
            int currentBalance = account.getBalance();
            account.withdraw(1);
            int newBalance = account.getBalance();
            if (currentBalance - 1 != newBalance) {
                System.out.println("Race condition detected!");
            }
        }
    };

    Thread thread1 = new Thread(task);
    Thread thread2 = new Thread(task);

    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();

    System.out.println("Final balance: " + account.getBalance());
}

}

위 코드에서 withdraw 메서드가 동기화되지 않았기 때문에 두 스레드가 동시에 잔액을 변경하려고 시도하여 데이터 경쟁이 발생할 수 있습니다.


② 데드락 (Deadlock)

두 개 이상의 스레드가 서로가 점유하고 있는 자원을 기다리면서 무한정 멈춰있는 상태를 데드락이라고 합니다. 데드락은 멀티스레드 환경에서 발생할 수 있는 심각한 문제 중 하나입니다.

java public class DeadlockExample { private static final Object lock1 = new Object(); private static final Object lock2 = new Object();

public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
        synchronized (lock1) {
            System.out.println("Thread 1: Holding lock1...");
            try { Thread.sleep(10); } catch (InterruptedException e) {}
            System.out.println("Thread 1: Waiting for lock2...");
            synchronized (lock2) {
                System.out.println("Thread 1: Acquired lock2.");
            }
        }
    });

    Thread thread2 = new Thread(() -> {
        synchronized (lock2) {
            System.out.println("Thread 2: Holding lock2...");
            try { Thread.sleep(10); } catch (InterruptedException e) {}
            System.out.println("Thread 2: Waiting for lock1...");
            synchronized (lock1) {
                System.out.println("Thread 2: Acquired lock1.");
            }
        }
    });

    thread1.start();
    thread2.start();
}

}

위 코드에서 thread1lock1을 획득하고 lock2를 기다리고, thread2lock2를 획득하고 lock1을 기다리기 때문에 데드락이 발생할 수 있습니다.


핵심 개념

개념 설명 사용법
synchronized 메서드 또는 코드 블록을 동기화하여 한 번에 하나의 스레드만 접근하도록 합니다. synchronized (object) { // 코드 }
volatile 변수의 값을 항상 메인 메모리에서 읽고 쓰도록 강제하여 가시성을 확보합니다. private volatile int count;
Lock 인터페이스 ReentrantLock 등의 구현체를 통해 명시적인 잠금 및 해제를 제공합니다. Lock lock = new ReentrantLock(); lock.lock(); try { // 코드 } finally { lock.unlock(); }

[Thread 1] ── synchronized ──→ [Shared Resource] ←── synchronized ────┘ [Thread 2] ── Waiting ──────→ [Shared Resource]


주요 동기화 방법

synchronized 키워드

synchronized 키워드는 Java에서 가장 기본적인 동기화 방법입니다. 메서드 또는 코드 블록에 적용하여 한 번에 하나의 스레드만 해당 영역에 접근할 수 있도록 합니다.

java public class SynchronizedExample { private int count = 0;

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

public int getCount() {
    return count;
}

public static void main(String[] args) throws InterruptedException {
    SynchronizedExample example = new SynchronizedExample();

    Runnable task = () -> {
        for (int i = 0; i < 1000; i++) {
            example.increment();
        }
    };

    Thread thread1 = new Thread(task);
    Thread thread2 = new Thread(task);

    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();

    System.out.println("Count: " + example.getCount());
}

}

increment 메서드에 synchronized 키워드를 사용하여 스레드 안전성을 보장합니다.


volatile 키워드

volatile 키워드는 변수의 가시성을 보장하는 데 사용됩니다. volatile로 선언된 변수는 항상 메인 메모리에서 읽고 쓰기 때문에 스레드 간에 최신 값을 공유할 수 있습니다.

java public class VolatileExample { private volatile boolean running = true;

public void stop() {
    running = false;
}

public void run() {
    while (running) {
        System.out.println("Running...");
        try { Thread.sleep(100); } catch (InterruptedException e) {}
    }
    System.out.println("Stopped.");
}

public static void main(String[] args) throws InterruptedException {
    VolatileExample example = new VolatileExample();
    Thread thread = new Thread(example::run);
    thread.start();

    Thread.sleep(1000);
    example.stop();
}

}

running 변수를 volatile로 선언하여 메인 스레드에서 stop() 메서드를 호출했을 때 작업 스레드가 변경된 값을 즉시 확인할 수 있도록 합니다.


Lock 인터페이스

Lock 인터페이스는 synchronized 키워드보다 더 유연하고 강력한 동기화 메커니즘을 제공합니다. ReentrantLockLock 인터페이스의 대표적인 구현체입니다.

java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;

public class LockExample { private int count = 0; private Lock lock = new ReentrantLock();

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

public int getCount() {
    return count;
}

public static void main(String[] args) throws InterruptedException {
    LockExample example = new LockExample();

    Runnable task = () -> {
        for (int i = 0; i < 1000; i++) {
            example.increment();
        }
    };

    Thread thread1 = new Thread(task);
    Thread thread2 = new Thread(task);

    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();

    System.out.println("Count: " + example.getCount());
}

}

ReentrantLock을 사용하여 increment 메서드를 동기화하고, try-finally 블록에서 unlock() 메서드를 호출하여 잠금을 해제합니다.


마무리

Java 멀티스레드 환경에서 동기화는 데이터의 일관성과 안정성을 유지하는 데 필수적입니다. synchronized, volatile, Lock 인터페이스 등 다양한 동기화 메커니즘을 이해하고 적절하게 활용하여 안전하고 효율적인 멀티스레드 애플리케이션을 개발할 수 있습니다. 개인적으로 멀티스레드 프로그래밍은 어렵지만, 꼼꼼하게 동기화 처리를 하면 성능 향상에 큰 도움이 되는 것 같아요. 앞으로 더 다양한 동기화 기법을 공부해야겠어요.

'개발 > Java' 카테고리의 다른 글

Java record 클래스 사용법  (0) 2026.02.22
Java 컬렉션 프레임워크 비교  (0) 2026.02.22
Java 제네릭 완벽 가이드  (0) 2026.02.22
Java 람다 표현식 정리  (0) 2026.02.22
Java Optional 사용법  (0) 2026.02.22

공유하기

facebook twitter kakaoTalk kakaostory naver band