본문 바로가기

Java

wait()과 notify(), notifyAll()

728x90

멀티 쓰레드로 구성된 프로세스에서 임계 영역을 보호하기 위해, 즉 쓰레드의 동기화를 위해 synchronized 키워드를 활용합니다. 혹시 synchronized에 대해서 개념을 알고 싶으신 분들은 아래 포스팅을 참고해 주세요!

https://silver-programmer.tistory.com/entry/synchronized%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%93%B0%EB%A0%88%EB%93%9C%EC%9D%98-%EB%8F%99%EA%B8%B0%ED%99%94

 

synchronized를 이용한 쓰레드의 동기화

이번 포스팅은 쓰레드의 동기화를 위한 synchronized에 대하여 작성하였습니다. 싱글 쓰레드가 아닌 멀티 쓰레드로 프로세스를 구성할 때, 같은 자원을 공유해서 작업을 하게 됩니다. 따라서 서로

silver-programmer.tistory.com

 

이때, 한 쓰레드가 lock을 획득하여 lock을 가진 상태로 오랜 시간을 보내지 않도록 하는 것이 중요합니다. 이를 위해서 고안된 것이 wait()과 notify()/notifyAll() 메서드입니다. 이번 포스팅은 이 메서드들에 대해서 작성해 보았습니다.


wait()과 notify()/notifyAll() 메서드

lock을 획득한 쓰레드가 동기화된 임계 영역의 코드를 수행하다가 여러가지 이유로 작업을 더 이상 수행하지 못할 때, 해당 lock을 반납하여 다른 쓰레드가 lock을 획득하도록 하는 메서드 wait() 메서드입니다.

그리고, 반납한 lock을 다시 획득해야 할 때 notify() 메서드를 호출하여 lock을 얻어 작업을 진행할 수 있습니다.

마지막으로, notifyAll()이라는 메서드도 있습니다. 이 메서드는 호출된 객체의 대기 중인 쓰레드의 대기실인 waiting pool에 있는 모든 쓰레드를 깨우는 역할을 합니다. 여기서 중요한 점은 모든 객체의 waiting pool에 있는 쓰레드가 아니라 메서드가 호출된 객체의 waiting pool에 있는 쓰레드라는 점입니다!!

 

wait(), notify(), notifyAll()은 synchronized 블록 내에서만 사용할 수 있습니다.


유의해야 할 점

만약 wait()을 호출한 쓰레드가 여러개라서 대기실(waiting pool)에 여러개의 쓰레드가 대기 상태일 때, notify()를 호출하면 어떤 쓰레드가 통보를 받고 lock을 획득할 수 있을지는 알 수 없다는 점입니다. 즉, 무작위로 선정된 쓰레드가 notify()로 통보를 받게 됩니다. notifyAll()로 모든 쓰레드에게 통보를 할 수 있지만,  lock을 얻을 수 있는 쓰레드는 단 1개입니다. 이 점을 유의해야 합니다.

 

해당 메서드들은 아래와 같이 Object 클래스에 정의되어 있습니다.


void wait() // notify() 혹은 notifyAll()이 호출될 때까지 기다림
void wait(long timeout) // 해당 시간이 지나면 자동으로 notify() 호출
void wait(long timeout, int nanos) // 해당 시간이 지나면 자동으로 notify() 호출
void notify()
void notifyAll()

 

아래 예시를 통해 어떻게 wait()과 notify()가 사용되는지 알아보겠습니다. 아래 코드는 테이블 객체에 음식이 가득 찼으면 Cook이 요리를 하지 못하도록 하고, 테이블 객체에 음식을 놔둘 여유가 있으면, Cook은 요리를 할 수 있도록 합니다.  또한, table 객체에 음식이 하나도 없거나 원하는 음식이 없다면 Customer는 음식을 먹지 못하도록 하였습니다. 


public class Main {

    public static void main(String[] args) throws InterruptedException {
        Table table = new Table(); // 여러 쓰레드가 공유하는 객체

        new Thread(new Cook(table), "COOK1").start();
        new Thread(new Customer(table, "donut"), "Customer1").start();
        new Thread(new Customer(table, "burger"), "Customer2").start();

        Thread.sleep(2000);
        System.exit(0);
    }

}

class Table {
    String[] dishNames = {"donut", "donut", "burger"};
    final int MAX_FOOD_ON_TABLE = 6; // table에 놓을 수 있는 음식의 최대 개수

    private ArrayList<String> dishesOnTable = new ArrayList<>(); // table 위에 있는 음식 리스트
    public synchronized void addDishToTable(String dish) { // table에 음식 추가하는 메서드
        while (dishesOnTable.size() >= MAX_FOOD_ON_TABLE) { // table에 음식이 가득찼으면 추가하지 않는다.
            String name = Thread.currentThread().getName();
            System.out.println(name + " is waiting."); // Cook 쓰레드가 기다릴 것이다.
            try {
                wait(); // COOK 쓰레드를 기다리게 한다.
                Thread.sleep(500);
            } catch (InterruptedException e) {}
        }
        dishesOnTable.add(dish); // 음식 추가
        notify(); // 기다리고 있는 Customer 쓰레드를 깨운다.
        System.out.printf("Table has %s. Total %d/%d dishes.\n", dishesOnTable.toString(), dishesOnTable.size(),MAX_FOOD_ON_TABLE);
    }

    public void removeDishFromTable(String dish) { // 해당 dish를 table에서 제거하는 메서드
        synchronized (this) {
            String name = Thread.currentThread().getName();

            while (dishesOnTable.size() == 0) {
                System.out.println(name + " is waiting.");
                try {
                    wait(); // Customer 쓰레드를 기다리게 한다.
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
            }

            while (true) {
                for (String s : dishesOnTable) {
                    if (dish.equals(s)) {
                        dishesOnTable.remove(s);
                        notify(); // COOK 쓰레드를 깨운다.
                        return; // 원하는 음식을 찾았으므로 먹으러 가야한다.
                    }
                }

                try { // 원하는 음식이 없는 Customer를 기다리게 한다.
                    System.out.println(name + " is waiting.");
                    wait(); // Customer를 기다리게 한다.
                    Thread.sleep(500);
                } catch (InterruptedException e) {}

            } // while(true)
        } // synchronized

    }

    public int dishNum() {
        return dishNames.length;
    }
}

class Customer implements Runnable {
    private Table table;
    private String food;

    Customer(Table table, String food) {
        this.table = table;
        this.food = food;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}
            String name = Thread.currentThread().getName();
            table.removeDishFromTable(food);
            System.out.println(name+" ate a "+food);
        }
    }
}

class Cook implements Runnable {
    private Table table;

    public Cook(Table table) {
        this.table = table;
    }

    @Override
    public void run() {
        while (true) {
            // 임의의 음식을 가지고 온 후, 요리사가 요리하여 table에 추가하는 메서드
            int idx = (int) (Math.random() * table.dishNum());
            table.addDishToTable(table.dishNames[idx]);

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {}
        }
    }
}

 

위에서와 같이 wait()과 notify()를 통해 해당 시뮬레이션을 구현하였습니다. 하지만 문제가 있습니다. table 객체의 waiting pool에 Cook 쓰레드와 Customer 쓰레드가 같이 기다린다는 것은, notify()가 호출되었을 때, 둘 중 누가 통지를 받을지 알 수 없다는 것입니다. 따라서 정상적이라면 Customer와 Cook이 적절히 번갈아가며 호출되어야 하지만 어떤 쓰레드가 통지를 받아 아 깨어날지는 알 수 없습니다. 

해당 코드에서는 운이 나쁘면 Cook 쓰레드가 계속해서 lock을 점유할 수 없어 Customer가 음식을 먹지 못하고 계속 waiting pool에 들어가는 상황이 발생할 수도 있습니다. 


기아 현상과 경쟁 상태

이렇게 특정 쓰레드(여기서는 Cook 쓰레드)가 운이 나쁘게도 계속 통지받지 못하고 오랫동안 기다리게 되는 현상기아 현상 (startvation)이라고 합니다.  이를 방지하기 위해서는 notifyAll() 메서드를 통해 모든 쓰레드에게 통지함으로써 Customer 쓰레드가 다시 waiting pool에 들어가더라도 Cook 쓰레드가 결국 lock을 얻어 작업을 진행할 수도 있습니다.

하지만, notifyAll()로 기아현상은 막을 수 있을지언정 Customer 쓰레드까지 깨움으로써 불필요한 lock을 얻기 위한 경쟁 상태(race condition)가 발생합니다. 이를 개선하기 위해서는 Customer 쓰레드와 Cook 쓰레드를 구별해서 통지하는 방법이 있으면 좋을 것 같네요. 

 

이를 해결하는 방법은 lock과 condition을 이용하는 것입니다. 이 부분에 관해서는 다음 포스팅에서 작성해 보겠습니다.

 

감사합니다!!


 

[참고자료]

남궁 성, [Java의 정석 3rd Edition], 도우출판, 2016

728x90
반응형

'Java' 카테고리의 다른 글

volatile 키워드  (2) 2023.10.29
Lock과 Condition (await()과 signal(),signalAll())  (2) 2023.10.29
synchronized를 이용한 쓰레드의 동기화  (2) 2023.10.29
제네릭(Generic) 타입의 형변환  (0) 2023.10.29
Thread 실행 제어  (0) 2023.10.27