쓰레드 동기화와 제어 (Synchronization)
멀티 쓰레드 환경에서는 여러 쓰레드가 프로세스의 같은 자원(메모리 등)을 공유하기 때문에 예상치 못한 문제가 발생할 수 있습니다. 이를 해결하기 위해 하나의 쓰레드가 특정 작업을 마칠 때까지 다른 쓰레드가 접근하지 못하도록 막는 것을 동기화(Synchronization) 라고 합니다.
1. synchronized 키워드
메서드 전체나 특정 영역(블록)에 임계 영역(Critical Section)을 설정하여, 한 번에 하나의 쓰레드만 이 영역을 실행하도록 보장할 수 있습니다.
메서드에 synchronized 적용
public synchronized void withdraw(int money) {
if(balance >= money) {
try { Thread.sleep(1000); } catch(Exception e) {}
balance -= money;
}
}
특정한 블록에 synchronized 적용
public void withdraw(int money) {
synchronized(this) { // 이 객체의 lock을 얻은 쓰레드만 블럭 안으로 진입
if(balance >= money) {
try { Thread.sleep(1000); } catch(Exception e) {}
balance -= money;
}
}
}
위와 같이 동기화를 사용하면 경쟁 상태(Race Condition)를 방지하여 데이터의 무결성을 지킬 수 있습니다.
2. wait()와 notify()
동기화를 통해 한 가지 문제를 해결했지만, 어떤 쓰레드가 Lock을 독점하여 오랫동안 쥐고 있게 되면 다른 쓰레드들은 작업이 진행되지 못하는 현상(기아 현상, Starvation)이 발생할 수 있습니다.
이 문제를 해결하기 위해 쓰레드를 제어하는 메서드가 wait()와 notify() 입니다.
wait(): 실행 중이던 쓰레드를 대기 상태로 만들고 객체의 락을 반납합니다.notify(): 대기 중인 쓰레드 중 하나를 깨워 실행 가능한 상태로 만듭니다.
class Table {
// 음식 리스트가 꽉 차면 요리사는 요리를 멈추고 기다려야 함
public synchronized void add(String dish) {
if(dishes.size() >= MAX_FOOD) {
try {
wait(); // COOK 쓰레드는 대기
} catch(InterruptedException e) {}
}
dishes.add(dish);
notify(); // 음식을 추가했으니 CUSTOMER 쓰레드를 깨움
}
}
3. 쓰레드의 생명주기 및 제어 메서드
쓰레드는 상태를 가지며, 다음과 같은 필수 메서드들을 사용해 실행을 제어할 수 있습니다.
sleep(long millis): 지정된 시간 동안 현재 쓰레드를 일시정지 상태로 만듭니다.join(): 다른 쓰레드의 작업이 끝날 때까지 기다립니다.interrupt(): sleep, wait, join 등으로 일시정지 상태인 쓰레드를 깨워서 실행 대기 상태로 만듭니다.yield(): 자신에게 할당된 실행 시간을 다음 쓰레드에게 양보합니다.
// join()을 사용해 두 쓰레드의 작업 시간을 측정하는 예제
Thread t1 = new Thread(new Worker());
Thread t2 = new Thread(new Worker());
t1.start();
t2.start();
long startTime = System.currentTimeMillis();
try {
t1.join(); // main 쓰레드는 t1 작업 완료를 기다림
t2.join(); // main 쓰레드는 t2 작업 완료를 기다림
} catch (InterruptedException e) {}
System.out.print("소요시간: " + (System.currentTimeMillis() - startTime));
이러한 동기화와 흐름 제어 도구들을 정확하게 이해하는 것이 자바 멀티 쓰레드 프로그래밍의 핵심입니다.