wait()과 notify()
이전 시간에, synchronized 키워드를 이용해서 동기화 하는 방법에 대해서 알아보았었다.
동기화는 하면 데이터 보호라는 장점이 있다. 하지만 비효율적이라는 단점도 있다. 한번에 한 쓰레드만 임계영역에 들어갈 수 있으니 말이다. 그래서 동기화를 하면 프로그램의 효율이 떨어진다.
이 효율을 높일 방법을 생각해서 만들어낸 것이, wait()과 notify()이다.
wait()과 notify()
- 동기화의 효율을 높이기 위해 wait(), notify()를 사용
- Object클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
- wait() - 객체의 lock을 풀고 쓰레드를 해당 객체의 wating pool에 넣는다.
- notify() - waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.
- notifyAll() - waiting pool에서 대기중인 모든 쓰레드를 깨운다.
wait()는 기다리는 것이고, notify()는 통보,알려주는 것이다.
wait()와 ntify()는 Object클래스에 저으이되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
위코드를 보자. 출금을 하는코드인데,
출금할 돈이 없으면, 어떻게 해야할까? 출금에 실패하면 될까?
위 코드에서는 출금할 돈이 없으면 기다리게 되어있다. 그러면 쓰레드가 여기서 멈춰있으면 어떻게 될까?
해당 임계영역에 해당 쓰레드가 자물쇠(Lock)를 가지고 들어갈텐데, 그러면 다른 쓰레드가 여기에 들어오지 못한다.
동기화해야하는 객체를 읽고,쓰는 메서드들도 전부 동기화해야한다는 것을 공부했었다.
그래서 출금메서드와 입금메서드 모두 sychronized 처리를 해준 것이다.
그런데, 출금액이 부족해서 기다리는 것 까진 좋은데,
입금을 하려고해도,
예를 들어 어떤 다른 쓰레드B가 입금하려고 해도, 락을 쓰레드A가 가지고 있어버리면 입금도 하지 못한다.그래서 전혀 입금과 출금이 이루어지지 않는다.
이때 방법이 뭐가있냐면, wait()를 호출하는 것이다.
"고객님 지금 잔고가 부족하니까, 잠시 기다려주세요."라고 하는 것이다.
그러면 Account객체에 waiting pool(대기실)이 있는데, 여기서 쓰레드 A가 락을 풀고 기다리는 것이다.
그렇게 되면, 입금하려는 쓰레드B가 락을 가질 수 있게 되고, 입금을 할 수 있게 된다.
그러면 입금을 마치면서 notify()해준다. 대기중인 쓰레드 중 하나에게 알리는 것이다.
그러면 대기하고 있던 쓰레드A가 다시 락을 얻어서 출금을 진행할때 wait()가 호출되었던 곳으로 돌아가고,
만약에 잔고가 충분하다면 출금이 진행 될 것이고, 아직도 잔고가 부족하다면 다시 wait()를 또 만나서 또 대기실을 가는 것이다.
이것이 바로 wait(), notify()이다.
어떤 쓰레드가 작업을 하다가, 작업을 수행할 상황이 안되면, 락을 풀어서 반납하고, 대기실로 간다. (객체마다 대기실이 있다.)
그러다가 notify()하면 해당 객체의 대기실에 있는 쓰레드에게 알려준다.
그러면 그때 다시 해당 쓰레드가 wait()위치로 가서 락을 얻어서 다시 작업을 시도한다.
notify()와 notifyAll()의 차이는
notify()는 waiting pool에서 대기중인 쓰레드 중의 하나를 깨우고,
notifyAll()은 waiting pool에서 대기중인 모든 쓰레드를 깨운다는 차이가 있다.
왜 notifyAll()이 있냐면, waiting pool에 여러쓰레드가 있을 때, notify()의 경우 하나의 쓰레드를 깨우는데, 이때 들어온순서대로 깨우는게 아니라 랜덤으로 깨운다. 그래서 운이안좋으면 특정쓰레드(손님)는 계속 waiting상태일 수 있다.
그래서 공평하게 notifyAll()로 전부 깨우고, 전부깨운다해도 그중에 1명만 락을 얻고 나머지는 다시 기다리게 될 것이기 떄문에 notifyAll()이 있는 것이다.
wait()과 notify() - 예제 1
- 요리사는 Table에 음식을 추가. 손님은 Table의 음식을 소비
- 요리사와 손님이 같은 객체(Table)을 공유하므로 동기화가 필요
테이블위의 접시들을 ArrayList에 저장할 수 있도록 했다.
그리고 요리사는, 테이블에 요리들을 가져다 놓을 것이다.
손님은, 테이블에 있는 요리를 소비할 것이다.
그래서 테이블에는 음식(dish)를 추가하는 메서드가 있고,
음식(dish)를 제거할 수도 있게 되어있다.
ArrayList는 동기화 되어있지 않는다.(vector는 되어있고)
요리사가 하는 일은, 요리중에 하나를 선택해서 table에 추가한다.
그리고 손님은 음식을 먹는다(소비). 즉,테이블에서 dish를 제거한다.
Cook이라는 쓰레드와 Customer라는 쓰레드는 Table을 같이 공유한다.
그래서 동기화가 필요하다.
그런데 일단은 동기화가 되어있지 않다.
그래서 동기화가 안된상태에서 일단 실행을 한다고 가정해보자.
손님이 2명, 요리사1명일 때, 실행하면 어떻게 되냐면,
wait()과 notify() - 예제 1 실행결과(동기화x)
이런식으로 에러가 난다. (예외 발생)
항상 에러나는 것은 아니고, 돌리다보면 에러가 난다.
예외1은 ArrayList를 읽기 수행중에 add(), remove() 즉 변경이 발생하면 나는 에러다.
예외 2는 요리가 1개 남았을 때, 손님A가 이것을 먹으려고하는데, 자기 차례가 끝나서 못먹었다.
그런데 다른 손님B가 와서 그것을 먹으면, 손님A는 있지도 않은 요리를 먹게된다.
그래서 발생하는 에러다.
- [문제점] Table을 여러 쓰레드가 공유하기 때문에 작업 중에 끼어들기 발생
- [해결책] Table의 add()와 remove()를 synchronized로 동기화
Table의 add()와 synchronized 메서드를 동기화 해야하는 것이다.
sychronized하는 방법에는 2가지가 있는데, 이번 코드의 remove의 경우에는 어떤 방법을 써도 가능하다.
this(이 테이블)이라는 객체를 동기화 한 것이다.
wait()과 notify() - 예제1 실행결과(동기화O)
[문제] 예외는 발생하지 않지만, 손님(CUST2)이 Table에 lock건 상태를 지속, 요리사가 Table의 lock을 얻을 수 없어서 음식을 추가하지 못함.
Table을 동기화하면 예외는 발생하지 않지만, 작업이 진행되지 않는다.
처음에 burger가 나왔는데, 손님이 먹었다. 첫번쨰 손님은 donut이 먹고싶었는데 없어서 못먹었다.
그런데 그 다음손님이 음식이 없어서 테이블에 lock을 건채로 계속 기다리고 있다.
동기화하는 것은 좋지만, 작업진행이 비효율적인 상태가 되어버렸다.
이것을 해결하려면, wait(), notify()를 사용하면 된다.
예제를 살펴보자.
import java.util.ArrayList;
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) {
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;}
}
public class Ex13_14 {
public static void main(String[] args) throws Exception {
Table table = new Table(); //
new Thread(new Cook(table), "COOK").start();
new Thread(new Customer(table, "donut"), "CUST1").start();
new Thread(new Customer(table, "burger"), "CUST2").start();
Thread.sleep(5000);
System.exit(0);
}
}
이것의 결과는,
이렇게 작업이 실행되지 않고 대기만 하게 된다.
[문제점] 음식이 없을 때, 손님이 Table의 lock을 쥐고 안놓는다. 요리사가 lock을 얻지 못해서 Table에 음식을 추가할 수 없다.
[해결책] 음식이 없을 때, wait()으로 손님이 lock을 풀고 기다리게 하자.
요리사가 음식을 추가하면, notify()로 손님에게 알리자. (손님이 lock을 재획득)
음식을 소비하는 쪽에서는 테이블에서 음식을 제거하는 코드에 wait()을 넣었다.
table에 있는 음식이 0면 기다리도록 했다. 그때 wait을 호출한다.
그러면, 쓰레드가 lock을 가지고 음식먹으러 왔다가, 음식이 없으면 wiat()하며 lock을 반납하고 waiting pool로 간다.
만약에 테이블에 음식이 0이 아니면 음식을 하나 먹는다. 그다음 notify()해주어야 한다.
요리사에게 통보해준다. 왜냐하면, 테이블에 음식이 꽉차면 기다리고 있을 수 있기 때문이다.
remove()의 맨밑의 try-catch문에서는, 테이블에 음식이 있어도 고객에 원하는 음식이 없는 경우 wait()하도록 해준다.
손님과 요리사가 기다리는 이유가 다르다.
add()메서드, 음식을 추가하는 곳에서는
테이블이 꽉 찻을 경우, cook을 wait()한다.
그리고 음식을 추가한 경우에는, 손님에게 통보해준다.
손님은 테이블에 음식이 없어서 기다리고 있을 수 있기 때문이다.
정리를 해보면,
요리사는, 테이블에 음식이 가득 찼을 때, wait()한다.
음식을 추가하고나면 대기실에서 기다리는 손님에게 통보(notify()) 한다.
손님은, 음식이 없으면 wait()한다. 음식을 먹고나면 요리사에게 notify()한다.
그리고 만약에 음식이 있다고 해도, 원하는 음식이 없을 경우 table lock을 붙잡고 계속 있으면 진행이 안되므로,
원하는 음식이 없는 경우에도 손님은 wait()한다.
언제 wait()과 notify()가 사용되는지 잘 이해해야 한다.
wait()과 notify() 예제2 실행결과
- 전과 달리 한 쓰레드가 lock을 오래 쥐는 일이 없어짐. 효율적이 됨!!
작업을 한글로 잘 설명해 놓았다.
여기서 제일 문제가 뭐냐면, wait()과 notify()가 호출되는 대상이 불분명하다.
왜냐하면, 대기실에는 손님과 요리사가 동시에 대기하고있기 때문이다.
그런데 notify()는 특정한 쓰레드를 깨우는 것이 아니기 때문에 손님이 깨워진 것인지 요리사가 깨워진 것인지 불분명하다.
그래서 이렇게 구분되지 않는 단점을 개선하려고 나온 것이, Lock & Condition이다.
Lock&Condition에 대한 것은 자바의 정석 3판에 자세히 설명한다.
똑같은데, 구별할 수 있다는 것만 다르기 때문에, 검색해서 찾아봐도 충분하다.
이 예제에서 중요한 것은, 동기화가 왜 필요한지, 동기화를 어떻게 하는지, 동기화를 보다 효율적으로 하려면 wait(), notify()를 써야 한다는 것이다.
[예제]
import java.util.ArrayList;
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(100);} catch(InterruptedException e) {}
String name = Thread.currentThread().getName();
table.remove(food);
System.out.println(name + " ate a " + food);
} // while
}
}
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) {}
} // while
}
}
class Table2 {
String[] dishNames = { "donut","donut","burger" }; // donut의 확률을 높인다.
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(500);
} catch(InterruptedException e) {}
}
dishes.add(dish);
notify(); // 기다리고 있는 CUST를 깨우기 위함.
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(); // CUST쓰레드를 기다리게 한다.
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);
notify(); // 잠자고 있는 COOK을 깨우기 위함
return;
}
} // for문의 끝
try {
System.out.println(name+" is waiting.");
wait(); // 원하는 음식이 없는 CUST 쓰레드를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
} // while(true)
} // synchronized
}
public int dishNum() { return dishNames.length; }
}
class Ex13_15 {
public static void main(String[] args) throws Exception {
Table2 table = new Table2();
new Thread(new Cook2(table), "COOK").start();
new Thread(new Customer2(table, "donut"), "CUST1").start();
new Thread(new Customer2(table, "burger"), "CUST2").start();
Thread.sleep(2000);
System.exit(0);
}
}
'JAVA' 카테고리의 다른 글
함수형 인터페이스 (0) | 2022.07.30 |
---|---|
람다식, 람다식 작성하기 (0) | 2022.07.28 |
쓰레드의 동기화 (0) | 2022.07.09 |
join(), yield() (0) | 2022.07.07 |
suspend(), resume(), stop() (0) | 2022.07.07 |
댓글
이 글 공유하기
다른 글
-
함수형 인터페이스
함수형 인터페이스
2022.07.30 -
람다식, 람다식 작성하기
람다식, 람다식 작성하기
2022.07.28 -
쓰레드의 동기화
쓰레드의 동기화
2022.07.09 -
join(), yield()
join(), yield()
2022.07.07