[Java] 16. Thread wait() 와 notify()


쓰레드의 동기화 처리시에 특정 쓰레드가 프로세스 자원을 너무 오랜 시간동안 점유할 경우 발생하는 문제에 대한 포스팅이다.



01. 동기화시에 lock의 오랜 시간 점유 문제

synchronized 로 동기화해서 쓰레드간 lock과 임계영역으로 공유 데이터를 동시처리를 예방하지만, 특정 쓰레드가 객체의 락을 오랜 시간을 점유하게 되면 다른 쓰레드들은 모두 해당 객체의 락을 기다리느라 다른 작업들이 모두 중단되는 문제가 발생할 수가 있다.

이런한 문제를 예방하기 위해 나온 것이 wait(), notify() 이다. 동기화 된 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호츌하여 쓰레드가 락을 반납하고 기다리게 한다.

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



02. wait()notify()

한번에 한개의 스레드만 들어올 수 있기 때문에 비효율적이다. 그래서 동기화의 효율을 높이기 위해 wait(), notify()를 사용한다. 즉 동기화를 효율적으로 하기 위함으로 사용한다. Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.

  • wait() : 객체의 lock을 풀고 쓰레드를 해당 객체의 Waiting Pool에 넣는다.
  • notify()
    waiting pool에서 대기중인 쓰레드 중의 하나를 깨워서 실행대기(RUNNABLE) 상태로 보낸다
  • notifyAll() : waiting pool에서 대기중인 모든 쓰레드를 깨워서 실행대기(RUNNABLE) 상태로 보낸다.



03. wait()notify() 예제

package com.example.thread.sample;


import java.util.ArrayList;

class Customer implements Runnable {
    String[] sushiKind = {"광어초밥", "연어초밥", "새우초밥", "계란초밥", "참치초밥"};

    private ConveyorBeltSushi conveyorBeltSushi;
    private String sushi;

    Customer(ConveyorBeltSushi conveyorBeltSushi) {
        this.conveyorBeltSushi = conveyorBeltSushi;
    }

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

            //컨베이트 벨트에 있는 초반 중에 선택
            String sushi = conveyorBeltSushi.getOneSushi();
            conveyorBeltSushi.getSushiDish(sushi);
            System.out.println(name + " ate a " + sushi);
        }
    }
}

class SushiChef implements Runnable {
    private ConveyorBeltSushi conveyorBeltSushi;

    SushiChef(ConveyorBeltSushi conveyorBeltSushi) {
        this.conveyorBeltSushi = conveyorBeltSushi;
    }

    public void run() {
        while (true) {
            int idx = (int)(Math.random()*conveyorBeltSushi.getSushiKindCnt());
            conveyorBeltSushi.putSushiDish(conveyorBeltSushi.sushiKind[idx]);

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

class ConveyorBeltSushi {
    String[] sushiKind = {"광어초밥", "연어초밥", "새우초밥", "계란초밥", "참치초밥"};
    final int MAX_FOOD = 10;
    private ArrayList<String> sushiDishes = new ArrayList<>();


    //초밥을 만들어서 회전벨트에 놓음
    public synchronized void putSushiDish(String sushi) {
        //회전벨트에 초밥갯수가 10개 이상이되면 초밥을 만들지 않고 기다림
        while(sushiDishes.size() >= MAX_FOOD) {
            String name = Thread.currentThread().getName();
            System.out.println(name + " is waiting");

            try {
                //대기 - Lock을 풀고 대기하다, 통지를 받으면 Lock을 재획득(RUNNABLE(실행대기) 상태)
                wait();
                Thread.sleep(500);
            } catch (InterruptedException e) {}
        }
        //초밥 생성함
        sushiDishes.add(sushi);

        notify();//대기 중인 쓰레드를 깨움(RUNNABLE(실행대기) 상태)
        System.out.println("Dishes : " + sushiDishes.toString());
    }

    //초밥을 회전벨트에서 가져감
    public void getSushiDish(String sushi) {
        synchronized (this) {
            String name = Thread.currentThread().getName();

            while(sushiDishes.size() == 0) {
                System.out.println(name + " is waiting");

                try {
                    wait();//Custom 쓰레드를 기다리게 한다.(Waiting Pool로 보냄)
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                }
            }

            while(true) {
                for (int i=0; i<sushiDishes.size(); i++) {
                    if (sushi.equals(sushiDishes.get(i))) {
                        sushiDishes.remove(i);
                        notify();//잠자고 있는 Cook 을 깨우기 위함(RUNNABLE(실행대기) 상태)
                        return;
                    }
                }

                //음식이 없는 경우 Custom 쓰레드를 기다리게 한다.
                try {
                    wait();//COOK 쓰레드를 기다리게 한다.(Waiting Pool로 보냄)
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                }
            }
        }
    }

    public int getSushiKindCnt() {
        return sushiKind.length;
    }

    /**
     * 컨베이트 벨트에 있는 초반 중에 선택
     */
    public String getOneSushi() {
        double random=Math.random();
        int num = (int)Math.round(random * (sushiDishes.size()-1));
        return sushiDishes.get(num);
    }
}

/**
 * wait(), notify()
 * - 동기화의 효율을 높이기 위해 wait(), notify() 사용
 * - wait() : 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
 * - notify() : waiting pool 에서 대기중인 쓰레드 중의 하나를 꺠운다.
 * - notifyAll() : waiting pool 에서 대기중인 모든 쓰레드를 깨운다.
 */
public class ExWaitNotify {

    public static void main(String... args) throws Exception {
        ConveyorBeltSushi conveyorBeltSushi = new ConveyorBeltSushi();

        new Thread(new SushiChef(conveyorBeltSushi), "To make sushi").start();

        //고객1과 고객2는 "광어초밥", "연어초밥"을 먹는 쓰레드
        new Thread(new Customer(conveyorBeltSushi), "CUSTOMER1").start();
        new Thread(new Customer(conveyorBeltSushi), "CUSTOMER2").start();
        Thread.sleep(5000);
        //System.exit(0);
    }
}

[출처]

  • 자바의정석 (저자: 남궁성)