자바/프로세스와 쓰레드

쓰레드의 동기화(2): wait() & notify()

백_곰 2022. 5. 7. 22:06

1. wait()와 notify()

- 동기화도 중요하지만, 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록

하는 것 또한 중요하다.

 

- 이러한 것을 구현하기 위해 wait()와 notify()가 등장하게 되었다.

 

- CS을 수행하다가 작업을 더 이상 진행할 상황이 아니면 wait() 호출을 하여 L을 반납한다.

( 이때, 대기하고 있는 쓰레드는 객체의 대기실(waiting pool)에서 통지를 기다린다. )

 

- wait() 호출 후, 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서 다시

L을 얻는다.

( notify()를 통해 통지를 waiting pool에 하면, 모든 쓰레드 중에서 임의의 쓰레드만

통지를 받는다. )

( notifiyAll()은 기다리고 있는 모든 쓰레드에게 통지를 하지만 L을 얻을 수 있는

 

- 이러한 과정을 '재진입(reentrance)'라고 부른다.

 

 

- wait()와 notify()는 Object 클래스에 정의되어 있으며 아래와 같다. 

 

void wait()

void wait(long timeout)

void wait(long timeout, int nanos)

void notify()

void notifyAll()

 

( wait()는 notify() 또는 notifyAll()이 호출될 때까지 기다리지만, 매개변수 timeout이 있는 wait()

일정 시간동안만 기다리며 자동적으로 notify()가 호출되는 것처럼 다시 작업을 재개한다. )

 

( waiting pool은 객체마다 존재하는 것이므로 notifyAll()이 호출된다고 해서 모든 객체의

waiting pool에 있는 쓰레드가 깨워지는 것이 아니다. )

( 즉, notifyAll()호출된 객체의 waiting pool에 대기중인 쓰레드만 해당되므로 주의하자. )

 

( 또한 위 메서드들은 동기화를 정의한 곳에서만 사용 가능하다. )

 

 

 

 

1-1. wait()와 notify()를 이해하기 위한 예제(1)

: 음식을 만들어서 테이블에 추가하는 요리사와 테이블의 음식을 소비하는 손님을 쓰레드로

구현한 예제이다.

 

import java.util.ArrayList;

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

		new Thread(new Cook4(Table4), "Cook41").start();
		new Thread(new Customer4(Table4, "donut"),  "CUST1").start();
		new Thread(new Customer4(Table4, "burger"), "CUST2").start();
	
		// 0.1초(100 밀리 세컨드) 후에 강제 종료시킨다.
		Thread.sleep(100);
		System.exit(0);
	}
}

class Customer4 implements Runnable {
	private Table4 Table4;
	private String food;

	Customer4(Table4 Table4, String food) {
		this.Table4 = Table4;  
		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. :(");
		} // while
	}

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

class Cook4 implements Runnable {
	private Table4 Table4;
	
	Cook4(Table4 Table4) {	this.Table4 = Table4; }

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

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

class Table4 {
	String[] dishNames = { "donut","donut","burger" }; // donut이 더 자주 나온다.
	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; }
}

 

( 위 코드를 실행하면 출력문이 제각기 다르겠지만, 두 가지 종류의 예외가 발생한다. )

 

( 첫 번째로는 요리사(Cook) 쓰레드가 테이블에 음식을 놓는 도중에, 손님(Customer) 쓰레드가 음식을

가져가려했기 때문에 발생하는 예외(ConcurrentModificationException)이 발생한다.

 

( 두 번째로는 손님(Customer) 쓰레드가 테이블의 마지막 음식을 가져가는 도중에 다른 손님 쓰레드가

저 음식을 낚아채버려서 있지도 않은 음식을 제거하려했기 떄문에 발생하는 예외(IndexOfBoundsException)

이다. )

 

( 그러므로, synchronized를 통해 적절히 동기화를 지정해주어야 한다. )

 

 

 

 

1-2. wait()와 notify()를 이해하기 위한 예제(2)

: 1-1 예제(1) 코드에 synchronized를 추가한 예제이다.

 

import java.util.*;

public class Exercise018 {
	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();
	  
		//3초 뒤에 프로그램을 종료시킨다.
		Thread.sleep(3000);
		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. :(");
		} // while
	}

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

class Cook implements Runnable {
	private Table table;
	
	Cook(Table 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) {}
		} // while
	}
}

class Table {
	String[] dishNames = { "donut","donut","burger" };
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();

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

	public boolean remove(String dishName) {
		synchronized(this) {	// synchronized를 추가
			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;
				}
		} // synchronized

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

 

( Table 클래스의 add()와 remove()에 synchronized를 설정하였다. )

 

( 그러나 설정했음에도 불구하고 손님 한명이 계속 기다리는 상태에 빠졌다. )

( 그 이유는 손님 쓰레드가 Table 객체의 lock을 얻을 수 없어서 불가능하기 때문이다. )

( 그렇기 때문에, 요리사는 Table 객체에 대한 lock을 얻을 수가 없다. )

 

( 이러한 현상을 해결하기 위해서는 wait()notify()을 적절하게 사용해야 한다. )

( 즉, 더 이상 작업할 게 없다면 wait()을 사용하여 요리사에게 notify()을 기다려야 한다. )

 

 

 

 

1-3. wait()와 notify()를 이해하기 위한 예제(3)

: 위 1-2 예제(2) 코드에 wait()와 notify()를 추가한 예제이다.

 

import java.util.ArrayList;

public class Exercise019 {
	public static void main(String[] args) throws Exception{
		Table2 table2 = new Table2();
		new Thread(new Cook2(table2), "요리사").start();
		new Thread(new Customer2(table2, "donut"),  "손님 1").start();
		new Thread(new Customer2(table2, "burger"), "손님 2").start();
		
		//3초 뒤에 프로그램을 종료시킨다.
		Thread.sleep(3000);
		System.exit(0);
	}
}

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

	Customer2(Table2 table, String food) {
		this.table = table;  
		this.food  = food;
	}
	
	//손님 쓰레드 1,2
	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 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(10);} catch(InterruptedException e) {}
		}
	}
}

class Table2 {
	String[] dishNames = { "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 {
					wait(); // COOK쓰레드를 기다리게 한다.
					Thread.sleep(300);
				} catch(InterruptedException e) {}	
		}
		dishes.add(dish);
		
		// 기다리고 있는 손님 쓰레드를 꺠우기 위한 notify() 메서드
		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 {
						wait(); 
						Thread.sleep(300);
					} catch(InterruptedException e) {}	
			}
			
			//손님 1,2가 먹는 음식이 있다면 삭제 후, 기다리고 있는 요리사를 깨운다.
			while(true) {
				for(int i=0; i<dishes.size();i++) {
					if(dishName.equals(dishes.get(i))) {
						dishes.remove(i);
						
						//잠자고 있는 요리사 쓰레드를 꺠우기 위한 notify() 메서드
						notify();
						
						return;
					}
				} // for문의 끝

				try {
					System.out.println(name+" is waiting.");
					wait(); // 원하는 음식이 없는 CUST쓰레드를 기다리게 한다.
					Thread.sleep(300);
				} catch(InterruptedException e) {}	
			} 
		}
	}
	
	public int dishNum() { return dishNames.length; }
}

( 이전 예제보다 우리가 원하는 출력대로 나왔다. )

( 그러나, 여기서 문제가 있는데, 요리사 쓰레드와 손님 쓰레드가 같은 waiting pool에 있다는 것이다. )

( 그래서 notify() 통지를 받았을 때, 누가 통지를 받을지 알 수 없다. )

 

( 운이 안좋다면 음식이 줄어들어 요리사 쓰레드를 호출해야되는 상황에서 받지 못하고 오랫동안 기다리게

되는데, 이러한 현상을 '기아(starvation)'이라고 부른다. )

( 이러한 현상을 막을려면 notifyAll()을 사용해야 한다. )

 

( 그렇게 되면, 손님 쓰레드가 다시 waiting pool에 들어가더라도 요리사 쓰레드는 결국 L을 얻을 것이다. )

( 하지만, 손님 쓰레드가 불필요하게 요리사 쓰레드와 L을 얻기 위해 경쟁하게 된다. )

( 이처럼 여러 쓰레드가 L을 얻기위해 경쟁하는 것을 '경쟁 상태(race condition')라고 한다. )

 

( 이러한 것을 완벽히 처리하기 위해서는 다음장에서 Lock&Condition을 이용하면 해결할 수 있다. )

( 아래의 링크에서 1-2-1 예제(1)처럼 해결할 수 있다. )

쓰레드의 동기화(3): Lock과 Condition을 이용한 동기화 (tistory.com)

 

쓰레드의 동기화(3): Lock과 Condition을 이용한 동기화

1.

kind-coding.tistory.com

 

 

 

 

다음장

쓰레드의 동기화(3): Lock과 Condition을 이용한 동기화 (tistory.com)

 

쓰레드의 동기화(3): Lock과 Condition을 이용한 동기화

1.

kind-coding.tistory.com