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 |