wait()과 notify(), notifyAll()

반응형
728x90
반응형

wait(), notify(), notifyAll()

synchronized로 동기화해서 공유 데이터를 보호할때 특정 스레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는것도 중요하다. 락을 오랜시간 보유하게되면, 다른 스레드들은 모두 해당 객체의 락을 기다리느라 다른 작업들도 원할히 진행되지 않는다.

이러한 상황을 위해 고안된 것이 wait), notify()다.

 

wait()

동기화된 임계 영역의 코드를 수행하다가 더이상 작업을 진행할 상황이 아니라면, 일단 wait()을 호출하여 스레드가 락을 반납하고 기다리게한다.

그러면 다른 스레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있게된다. 

 

notify()

나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서, 작업을 중단했던 스레드가 다시 락을 얻어 작업을 진행할 수 있게한다.

 

재진입 (reentrance)

wait()에 의해 lock을 반납했다가, 다시 lock을 얻어서 임계 영역이 들어오는 것 

 

wiat()이 호출되면, 실행중이던 스레드는 해당 객체의 대기실(waitting pool)에서 통지를 기다린다. notify()가 호출되면, 해당 객체의 대기실에 있던 모든 스레드 중에서 임의의 스레드만 통지를 받는다.

 

notifyAll()

기다리고있는 모든 스레드에게 통보를 한다. 

그럼에도 lock을 얻을 수 있는 것은 하나의 스레드이고, 나머지 스레드는 통보를 받더라도 lock을 얻지 못하면 다시 lock을 기다리게된다.

 

wait(), notify(), notifyAll() 특징

  • Object에 정의되어있다.
  • 동기화 블록(synchronized 블록)내에서만 사용할 수 있다.
  • 보다 효율적인 동기화를 가능하게 한다.

 

Object.java

Object Method  
void wait() wait()은 notify() 또는 notifyAll()이 호출될때까지 기다린다.
void wait(long timeout) 매개변수가 있는 wait()은 지정된 시간동안만 기다린다. 지정된 시간이 지난후 자동적으로 notify()가 호출되는 것과 같다.
void wait(long timeout, int nanos)
void notify()  
void notifyAll() 모든 객체의 waiting pool에 있는 스레드가 깨워지는 것이 아닌, notifyAll()이 호출된 객체의 waitting pool에 대기중인 스레드만 해당된다.
package java.lang;

import jdk.internal.HotSpotIntrinsicCandidate;

public class Object {

    private static native void registerNatives();
    static {
        registerNatives();
    }
    
    ...

    @HotSpotIntrinsicCandidate
    public final native void notify();
    
    @HotSpotIntrinsicCandidate
    public final native void notifyAll();
   
    public final void wait() throws InterruptedException {
        wait(0L);
    }
    
    public final native void wait(long timeoutMillis) throws InterruptedException;

    public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
        if (timeoutMillis < 0) {
            throw new IllegalArgumentException("timeoutMillis value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeoutMillis++;
        }

        wait(timeoutMillis);
    }

}

 

 

예제

▶ 스레드 목록

  • 식당에서 음식(Dish)을 만들어서 테이블(Table)에 추가(add)하는 요리사(Cook)
  • 테이블의 음식을 소비(remove)하는 손님(Customer)
package com.java.effective.item81.threadexam;

import java.util.ArrayList;

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

        new Thread(new Cook(table), "COOK1").start();
        new Thread(new Customer(table, "donut"), "CUST1").start();
        new Thread(new Customer(table, "burger"), "CUST2").start();

        Thread.sleep(100); // 0.1초 후 강제 종료시킨다.
        System.exit(0); // 프로그램 전체를 종료. (모든 쓰레드가 종료됨)
    }
}


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

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

    public void run() {
        while (true) {
            try { Thread.sleep(10); } catch (InterruptedException e){}
            String name = Thread.currentThread().getName();

            if (eatFood()) {
                System.out.println(name +" ate a " + food);
            }else{
                System.out.println(name +" failed to eat. :(");
            }
        }
    }

    boolean eatFood() { return table.remove(food); }
}

class Cook implements Runnable {
    private Table table;

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

    public void run(){
        while (true) {
            //임의의 요리를 하나 선택해서 table에 추가한다.
            int idx = (int) (Math.random() * table.dishNum());
            table.add(table.dishNames[idx]);

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

class Table {
    String[] dishNames ={ "donut", "donut", "burger"};
    final int MAX_FOOD = 6;

    private ArrayList<String> dishes = new ArrayList<>();

    public void add(String dish) {
        // 테이블에 음식이 가득찼으면, 테이블에 음식을 추가하지 않는다.
        if (dishes.size() >= MAX_FOOD) return;
        dishes.add(dish);
        System.out.println("Dishes : " + dishes.toString());
    }

    public boolean remove(String dishName) {
        // 저장된 요리와 일치하는 요리를 테이블에서 제거한다.
        for (int i = 0; i < dishes.size(); i++) {
            if (dishName.equals(dishes.get(i))) {
                dishes.remove(i);
                return true;
            }
        }
        
        return false;
    }

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

 

실행결과
Dishes : [burger]
CUST1 failed to eat. :(
Dishes : [burger]
Dishes : [burger, burger]
Dishes : [burger, burger, burger]
Dishes : [burger, burger, burger, donut]
Dishes : [burger, burger, burger, donut, donut]
Dishes : [burger, burger, burger, donut, donut, donut]
CUST2 ate a burger
CUST1 ate a donut
Dishes : [burger, burger, burger, donut, donut, donut]
CUST2 ate a burger
Dishes : [burger, burger, donut, donut, donut, donut]
CUST1 ate a donut
Dishes : [burger, burger, donut, donut, donut, donut]
CUST2 ate a burger
Dishes : [burger, donut, donut, donut, donut, donut]
CUST1 ate a donut
Dishes : [burger, donut, donut, donut, donut, donut]
CUST2 ate a burger
CUST1 ate a donut
Dishes : [donut, donut, donut, donut, donut]
Dishes : [donut, donut, donut, donut, donut, burger]
CUST2 ate a burger
CUST1 ate a donut
Dishes : [donut, donut, donut, donut, donut]
Dishes : [donut, donut, donut, donut, donut, burger]
CUST1 ate a donut
Dishes : [donut, donut, donut, donut, burger, donut]
CUST2 ate a burger
Dishes : [donut, donut, donut, donut, donut, donut]
CUST1 ate a donut
Dishes : [donut, donut, donut, donut, donut, donut]
CUST2 failed to eat. :(

 

실행 결과는 실행할 때마다 다르다. 예외가 발생할 수도 있고 발생하지 않을 수도 있다.

이 예제를 반복해서 실행해보면 2가지 종류의 예외가 발생할 수 있다.

예외 설명
ConcurrentModificationException 요리사(Cook) 스레드가 테이블에 음식을 놓는 도중에, 손님(Customer) 스레드가 음식을 가져가려고했다.
IndexOutOfBoundsException 손님 스레드가 테이블의 마지막 남은 음식을 가져가는 도중에,
다른 손님 스레드가 먼저 음식을 낚아채버려서 있지도 않은 음식을 테이블에서 제거하려고했다.

이런 예외들이 발생하는 이유는 여러 스레드가 테이블을 공유하는데도 동기화하지 않았기 때문이다. 

동기화를 추가하자.

 

 

동기화 synchronized 키워드 추가

package com.java.effective.item81.threadexam;

import java.util.ArrayList;

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

        new Thread(new Cook2(table), "COOK1").start();
        new Thread(new Customer2(table, "donut"), "CUST1").start();
        new Thread(new Customer2(table, "burger"), "CUST2").start();

        Thread.sleep(5000);
        System.exit(0); // 프로그램 전체를 종료. (모든 쓰레드가 종료됨)
    }
}


class Customer2 implements Runnable {
    private Table2 table;
    private String food;

    Customer2(Table2 table, String food) {
        this.table = table;
        this.food = food;
    }

    public void run() {
        while (true) {
            try { Thread.sleep(10); } catch (InterruptedException e){}
            String name = Thread.currentThread().getName();

            if (eatFood()) {
                System.out.println(name +" ate a " + food);
            } else {
                System.out.println(name +" failed to eat. :(");
            }
        }
    }

    boolean eatFood(){ return table.remove(food); }
}

class Cook2 implements Runnable{
    private Table2 table;
    Cook2(Table2 table) { this.table = table; }
    public void run(){
        while (true) {
            int idx = (int) (Math.random() * table.dishNum());
            table.add(table.dishNames[idx]);
            try { Thread.sleep(100); } catch (InterruptedException e) { }
        }
    }
}

class Table2{
    String[] dishNames ={ "donut", "donut", "burger"};
    final int MAX_FOOD = 6;

    private ArrayList<String> dishes = new ArrayList<>();

    /**
     * 동기화 추가 
     * @param dish
     */
    public synchronized void add(String dish) {
        if(dishes.size() >= MAX_FOOD) return;
        dishes.add(dish);
        System.out.println("Dishes : " + dishes.toString());
    }

    /**
     * 동기화 추가 
     * @param dishName
     * @return
     */
    public boolean remove(String dishName) {
        synchronized(this) {
            while (dishes.size() ==0) {
                String name = Thread.currentThread().getName();
                System.out.println(name+" is waiting");
                try { Thread.sleep(500); } catch (InterruptedException e) { }
            }

            for (int i = 0; i < dishes.size(); i++) {
                if (dishName.equals(dishes.get(i))) {
                    dishes.remove(i);
                    return true;
                }
            }
        }
        
        return false;
    }
    public int dishNum(){ return dishNames.length; }
}

 

실행결과
Dishes : [burger]
CUST1 failed to eat. :( <- donut이 없어서 먹지 못했다.
CUST2 ate a burger 
CUST1 is waiting
CUST1 is waiting
CUST1 is waiting
CUST1 is waiting
CUST1 is waiting
CUST1 is waiting
CUST1 is waiting
CUST1 is waiting
CUST1 is waiting
CUST1 is waiting

계속해서 waiting 이 출력되는데, 이는 음식이 없어서 테이블에 lock을 건 채로 계속 기다리고 있기 때문이다.

 

손님 스레드가 원하는 음식이 테이블에 없으면, 'failed to eat'을 출력하고, 테이블에 음식이 하나도 없으면, 0.5초마다 음식이 추가되었는지 확인하면서 기다리도록 작성되어있다.

요리사 스레드는 왜 음식을 추가하지 않고 손님 스레드를 계속 기다리게 하는 걸까?

synchronized(this) {
    while (dishes.size() ==0) {
        String name = Thread.currentThread().getName();
        System.out.println(name+" is waiting");
        try { Thread.sleep(500); } catch (InterruptedException e) { }
    }

    for (int i = 0; i < dishes.size(); i++) {
        if (dishName.equals(dishes.get(i))) {
            dishes.remove(i);
            return true;
        }
    }
}

그 이유는 손님 스레드가 테이블 객체의 lock을 쥐고 기다리기 때문이다.

요리사 스레드가 음식을 새로 추가하려고 해도 테이블의 객체의 lock을 얻을 수 없어서 불가능하다.

이럴때 사용하는 것이 wait() & notify()이다. 

손님 스레드가 lock을 쥐고 기다리는게 아니라, wait()으로 lock을 풀고 기다리다가 음식이 추가되면 notify()로 통보를 받고 다시 lock을 얻어서 나머지 작업을 진행하게 할 수 있다.

 

 

wait() & notify() 사용 버전

package com.java.effective.item81.threadexam;

import java.util.ArrayList;

// wait() & notify()
public class ThreadWaitEx3 {
    public static void main(String[] args) throws Exception {
        Table3 table = new Table3(); // 여러 쓰레드가 공유하는 객체

        new Thread(new Cook3(table), "COOK1").start();
        new Thread(new Customer3(table, "donut"), "CUST1").start();
        new Thread(new Customer3(table, "burger"), "CUST2").start();
        
        Thread.sleep(2000);
        System.exit(0); // 프로그램 전체를 종료. (모든 쓰레드가 종료됨)
    }
}


class Customer3 implements Runnable {
    private Table3 table;
    private String food;

    Customer3(Table3 table, String food) {
        this.table = table;
        this.food = food;
    }

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

class Cook3 implements Runnable {
    private Table3 table;
    Cook3(Table3 table) { this.table = table; }
    public void run() {
        while (true) {
            int idx = (int) (Math.random() * table.dishNum());
            table.add(table.dishNames[idx]);
            try { Thread.sleep(10); } catch (InterruptedException e) { }
        }
    }
}

class Table3 {
    String[] dishNames ={ "donut", "donut", "burger"};
    final int MAX_FOOD = 6;

    private ArrayList<String> dishes = new ArrayList<>();
    
    public synchronized void add(String dish) {
        while(dishes.size() >= MAX_FOOD){
            String name = Thread.currentThread().getName();
            System.out.println(name+" is waiting");
            try {
                /** 음식이 가득찼으므로 COOK 쓰레드를 기다리게 한다. */
                wait();
                Thread.sleep(500);
            } catch (InterruptedException e){}
        }
        
        dishes.add(dish);
        /** 음식이 추가되면 기다리고 있는 CUST를 깨운다. */
        notify();
        
        System.out.println("Dishes : " + dishes.toString());
    }

    public void remove(String dishName) {
        synchronized(this) {
            String name = Thread.currentThread().getName();
            while (dishes.size() == 0) {
                System.out.println(name+" is waiting");
                
                try {
                    /** 원하는 음식이 없는 CUST 스레드를 기다리게한다. */
                    wait();
                    Thread.sleep(500);
                }
                catch (InterruptedException e) { }
            }

            while (true) {
                for(int i = 0; i < dishes.size(); i++) {
                    if (dishName.equals(dishes.get(i))) {
                        dishes.remove(i);
                        
                        /** 음식을 하나라도 비우면 잠자고 있는 COOK을 깨운다. */
                        notify();
                        
                        return; // 음식먹었으면 return
                    }
                }
                
                try {
                    System.out.println(name+" is waiting");
                    
                    /** 원하는 음식이 없는 CUST 쓰레드를 기다리게 한다. */
                    wait();
                    
                    Thread.sleep(500);
                } catch (InterruptedException e){}
            } 
        }
    }
    
    public int dishNum(){ return dishNames.length; }
}

 

실행결과
Dishes : [donut]
Dishes : [donut, burger]
Dishes : [donut, burger, burger]
Dishes : [donut, burger, burger, donut]
Dishes : [donut, burger, burger, donut, donut]
Dishes : [donut, burger, burger, donut, donut, donut]
COOK1 is waiting
CUST1 ate a donut
Dishes : [burger, burger, donut, donut, donut, burger]
CUST1 ate a donut
CUST2 ate a burger
Dishes : [burger, donut, donut, burger, donut]
Dishes : [burger, donut, donut, burger, donut, burger]
COOK1 is waiting
CUST1 ate a donut
Dishes : [burger, donut, burger, donut, burger, burger]
CUST1 ate a donut
CUST2 ate a burger
Dishes : [burger, donut, burger, burger, burger]
Dishes : [burger, donut, burger, burger, burger, donut]
COOK1 is waiting
CUST1 ate a donut
Dishes : [burger, burger, burger, burger, donut, donut]
CUST1 ate a donut
CUST2 ate a burger
Dishes : [burger, burger, burger, donut, donut]
Dishes : [burger, burger, burger, donut, donut, burger]
COOK1 is waiting
CUST1 ate a donut
CUST2 ate a burger

한가지 문제가 있다.

 

테이블 객체의 waiting pool에 요리사 스레드와 손님 스레드가 같이 기다린다는 것이다.

그래서 notify()가 호출되었을때 요리사 스레드와 손님 스레드 중에서 누가 통지를 받을지 알 수 없다. 

만약 테이블의 음식이 줄어들어서 notify()가 호출되었다면, 요리사 스레드가 통지를 받아야한다.

notify()는 그저 waiting pool에 대기중인 스레드 중에서 하나를 임의로 선택해서 통지할 뿐, 요리사 스레드를 선택해서 통지할 수 없다.

 

기아현상 (starvation)

지독히 운이 나쁘면 요리사 스레드는 계속 통지를 받지 못하고 오랫동안 기다리게된다.

 

이 현상을 막으려면 notify() 대신 notifyAll()을 써야한다.

일단 모든 스레드에 통지를 하면, 손님 스레드는 다시 waiting pool에 들어가더라도 요리사 스레드는 결국 lock을 얻어서 작업을 진행할 수 있기 때문이다.

notifyAll()로 요리사 스레드의 기아 현상을 막았지만, 손님 스레드까지 통지를 받아서 불필요하게 요리사 스레드와 lock을 얻기 위해 경쟁하게된다. 

 

 

 

자바의 정석 - 쓰레드(thread)에서 wait(), notify(), notifyAll()

 

반응형

Designed by JB FACTORY