쓰레드의 동기화(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

 

저작자표시 (새창열림)

'자바 > 프로세스와 쓰레드' 카테고리의 다른 글

volatile  (0) 2022.05.10
쓰레드의 동기화(3): Lock과 Condition을 이용한 동기화  (0) 2022.05.08
쓰레드의 동기화(1): Critical Section & Lock, snychronized  (0) 2022.05.07
쓰레드의 실행제어  (0) 2022.02.20
데몬 쓰레드  (0) 2022.02.14
'자바/프로세스와 쓰레드' 카테고리의 다른 글
  • volatile
  • 쓰레드의 동기화(3): Lock과 Condition을 이용한 동기화
  • 쓰레드의 동기화(1): Critical Section & Lock, snychronized
  • 쓰레드의 실행제어
백_곰
백_곰
  • 백_곰
    친절한 코딩
    백_곰
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 알고리즘 (with JAVA)
        • 기본 알고리즘
        • 완전 탐색
        • 분할 정복 알고리즘
        • 동적 계획법
        • 탐욕법
        • 코딩 테스트 기출 문제
        • 코드트리 조별과제
      • 백준 (with JAVA)
        • 완전 탐색
        • 분할 정복
        • 그 외
      • 자바
        • 개발 환경 구축하기
        • 팁
        • 기본적인 개념
        • 컬렉션 프레임워크
        • 프로세스와 쓰레드
        • 지네릭스
        • 람다식
        • 스트림
        • 입출력 IO
        • 네트워킹
        • 열거형(enums)
        • java.lang 패키지
        • java.time 패키지
        • 유용한 클래스들
        • 형식화 클래스들
      • 안드로이드 with 자바
        • 응용 문제들
        • 자잘한 문제들
        • 오류 보고서
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      스트림
      제자리 정렬
      java.time 패키지
      serializable
      코딩테스트
      InputStream
      중간연산
      Collections Framework
      안정 정렬
      Arrays
      코딩트리조별과제
      역직렬화
      코드트리
      불안정 정렬
      알고스팟
      문자 기반 스트림
      java.lang패키지
      소켓 프로그래밍
      람다식
      자바 개념
      다형성
      선택 정렬
      snail
      유용한 클래스
      TCP 소켓 프로그래밍
      outputstream
      ServerSocket
      안드로이드 스튜디오
      file
      map()
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    백_곰
    쓰레드의 동기화(2): wait() & notify()
    상단으로

    티스토리툴바