본문 바로가기

Programming/JAVA

java.lang.Object 메쏘드 분석 6 - wait , notify


출처 : http://egloos.zum.com/iilii/v/5565036

wait과 notify는 한 묶음으로 같이 이해하셔야 합니다.


wait은 아래와 같이 3가지 오버로딩된 메쏘드로 있습니다.

    public final void wait() throws InterruptedException               : 누군가 깨울 때까지 기다리겠음. 안 깨워주면 안 일어남.
    public final native void wait(long timeout) throws InterruptedException    : 누군가 깨워주거나 timeout 까지 안 깨워주면 알아서 일어나겠음.
    public final void wait(long timeout, int nanos) throws InterruptedException  : 누군가 깨워주거나 timeout에 nanos 까지 고려한 시간까지 안 깨워주면 일어나겠음. ( 그러나 실제 코드에서 nanos는 크게 의미가 없음.)

포인트는 가운데 놈만 native!! 즉, 첫번째 놈과 마지막 놈은 가운데 놈에 적당히 인자를 넣어서 호출합니다.( 마지막 놈의 원래 의도는 그게 아니었던 것 같지만, 어쨌거나 까보니 그렇게 생겼네요.) 

애들은 전부 기다리기 위한 애들이고, 깨우는 것은 notify()와 notifyAll()로 합니다. notify()는 하나만 깨우는 거고, notifyAll()은 전부 깨우는 겁니다. 그 외에는 비슷하다고 보면 됩니다. 이 후부터는 따로 구분하지 않는 한 notify라고 하면 notifyAll도 똑같다고 보시면 됩니다. 자세한 건 뒤에서 다시 다루겠습니다.



wait의 의미는 "기다린다" 입니다. "무엇이" "무엇을 기준으로" "언제까지" 기다리는 것인지를 명확히 이해해야 합니다.

1. 무엇이?
기다리는 주체는 wait이 호출된 Thread 입니다. wait 코드가 있는 곳에서 Thread.currentThread() 했을 때 나오는 그 Thread.

2. 무엇을 기준으로?
obj.wait() 이 호출되었다면, obj를 기준으로 기다리는 것입니다. 기준이 왜 중요한가 하면 깨우는 기준과 wait하는 기준이 같기 때문입니다.

3. 언제까지?
다음 중 하나가 만족할 때까지입니다.
* timeout 될 때까지.
* notify나 notifyAll로 깨워줄 때까지. ( notify에 대해서는 아래서 자세히 설명할 겁니다.) 여기서 중요한 것은 깨우는 기준이 wait을 가진 instance입니다. 즉, obj.wait() 이라고 호출이 되었으면, obj.notify()가 호출되어야 깨어납니다. 물론 notify()는 다른 Thread에서 호출합니다.
* Thread의 interrupt가 호출 될 때까지. 얘도 notify와 비슷하지만 기준이 다릅니다. 잠자는 Thread를 기준으로 깨웁니다. obj.wait()이 걸려있다고 하더라도 그것을 건 Thread를 기준으로 깨웁니다. 그리고 이렇게 깨우면 위의 두 가지 경우와는 다르게 InterruptedException이 발생합니다. 이건 언젠가 먼 훗날 Thread에 관련된 글을 쓰겠습니다. 다 쓰면 링크 달께요^^
* 기타 있어서는 안 될 상황. Thread.stop() 이나 jvm이 죽는 다거나.. 등등등..


obj.wait을 걸기 위해서는 반드시 그 obj를 기준으로 synchronized 블럭을 만들고 그 안에서 걸어야 합니다.

synchronized (obj) { 
while (뭔가 조건){
obj.wait();
}
}

synchronized 의 obj와 obj.wait()에서의 obj는 같다는 게 포인트죠. 이게 다르면 IllegalMonitorStateException 이 발생합니다. synchronized에 대해서 명확하게 이해하셔야 하며, 잘 모를 때는 요기 클릭!!

wait은 반드시 loop 안에서 쓰라고 합니다. 왜냐하면 여러 개의 Thread에서 접근하기 때문에 항상 조건을 따져봐야 하기 때문입니다. 이 내용은 뒤에 notify와 묶어서 다시 다루겠습니다.



wait set이라는 개념이 있는데, 어떤 object가 소유한 set 이라고 보시면 됩니다.
obj.wait이 호출되면
1. Thread.currentThread() 가 멈춘다.
2. obj의 wait set에 그 Thread가 들어간다.
3. synchronized 로 잡은 monitor의 lock을 푼다. 

로 보시면 됩니다. 왜 obj의 wait set에 들어가냐하면 나중에 notify() 깨울 기준이 되기 때문입니다.

wait이 걸리면 synchronized 블럭에서 일단 빠져나온다고 보시면 됩니다. 즉 모니터를 놔 줍니다. 그리고 notify에 의해서 깨어나면 다시 synchronized 안 으로 들어간다고 보시면 됩니다.


obj.nofiy() 는 obj.wait을 호출해서 obj의 wait set에 박혀서 잠자고 있는 Thread를 깨워줍니다. 여러 놈이 있을 때 notify()는 그 중 아무 놈이나 한 놈을 깨우고, notifyAll()은 전부 깨웁니다.

notify도 마찬가지로 synchronized 블럭 안 에 있어야 합니다.

synchronized (obj) { 
obj.notify();
}

wait과 마찬가지로 같은 monitor!! 입니다.

obj.notify()가 호출되는 시점에는 아직 synchronized 블럭 안이기 때문에 그에 의해서 깨어난 쓰레드는 다시 시작을 못합니다. wait()도 같은 모니터를 가지기 때문에 wait()이 호출된 이후의 코드를 호출하려면 다시 synchronized 구문 안에 들어가야 하는데 notify()가 아직 synchronized 구문을 빠져나오지 않았으므로 아직 실행이 안 되는 겁니다. notify를 실행한 Thread가 synchronized 블럭을 빠져나가는 순간부터 monitor 쟁탈전이 벌어지지만 notify에 의해 깨어난 Thread가 그 mornitor를 쟁취할 수 있을 지는 모르는 일입니다. 아래 코드를 실행시켜 보시면 프로그램이 안 끝나는 사태를 볼 수 있습니다. (여러 번 하다 보면 한 번쯤은 일어날 겁니다.)

public class ThreadTest {
private volatile boolean active = false;
private volatile int i = 0;

public synchronized void mWait() throws InterruptedException {         // 모니터 쟁탈 포인트 1. synchronized method라서.
while (!active) {
wait();   //모니터 쟁탈 포인트 2. 깨어나는 순간 모니터를 다시 획득해야 Thread가 진행됨.
}
active = false;
System.out.println("print\t" + i);
}

public synchronized void mNotify() { // 모니터 쟁탈 포인트 3. 마찬가지로 synchronized method라서.
active = true;
notifyAll();
i++;
System.out.println("after notify\t" + i);
}

private static class WaitCaller extends Thread{
private final ThreadTest a;
public WaitCaller(ThreadTest a) {
this.a = a;
}
public void run() {
try {
a.mWait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class NotifyCaller extends Thread{
private final ThreadTest a;
public NotifyCaller(ThreadTest a) {
this.a = a;
}
public void run() {
a.mNotify();
}
}
public static void main(String[] args)  {
final ThreadTest a = new ThreadTest();
final int cnt = 2;
for (int i = 0; i < cnt; i++) {
new WaitCaller(a).start();
}
for (int i = 0; i < cnt; i++) {
new NotifyCaller(a).start();
}
}
}

ThreadTest라는 놈은 wait에서 loop를 위한 active라는 멤버 변수를 가지고 있습니다. 그리고 notify가 호출될 때 마다 값이 증가하는 int로 i 를 가지고 있습니다.
이 ThreadTest에 대한 인스턴스 1개를 만들어서 wait을 호출하는 WaitThread 가 2개 (WaitCaller 1 , 2 라고 가정) 뜨고, notify를 호출하는 NotifyCaller가 2개 (NotifyCaller 1,2)가 뜹니다. 

monitor 쟁탈전이 일어나는 포인트는 총 3개 입니다. 4개의 Thread 중 어떤 놈이 모니터를 쟁취하느냐에 따라 결과가 달라집니다.

        WaitThread 1: 포인트 1에서 모니터 획득.
        WaitThread 1: 포인트 2에서 모니터 상실.(wait에 빠지니까..)
        WaitThread 2: 포인트 1에서 모니터 획득.
        WaitThread 2: 포인트 2에서 모니터 상실.(wait에 빠지니까..)
NotifyThread 1: 포인트 3에서 모니터 획득.
        NotifyThread 2: 포인트 3에서 모니터 획득 실패.

NotifyThread 1: notify 호출하고 i 증가 시키고 할 꺼 다하고 죽음. 당연히 모니터 상실.


여기서 보면 notifyAll로 호출되었기 때문에 WaitThread 1,2 가 모두 포인트 2에서 모니터를 얻으려고 하며, NotifyThread 2도 모니터를 획득하려고 하고 있으므로, NotifyThread 1을 제외한 3개의 Thread가 모니터를 노림.

시나리오 1.
WaitThread 1: 포인트 2에서 모니터 획득. 일 하고 죽고 모니터 놔주고.
WaitThread 2: 포인트 2에서 모니터 획득. 그러나 while 문 때문에 active가 false이므로 모니터 다시 상실하고 다시 wait 상태로.
NotifyThread 2: 포인트 3에서 모니터 획득. 할거 다하고 모니터 놓고 죽음.
WaitThread 2: NotifyThread2 덕에 깨어나서 할 거 하고 죽음.
아래와 같은 결과

after notify 1
print 1
after notify 2
print 2


시나리오 2.
NotifyThread 2: 포인트 3에서 모니터 획득. 할거 다하고 모니터 놓고 죽음.
WaitThread 1: 포인트 2에서 모니터 획득. 일 하고 죽고 모니터 놔주고.
WaitThread 2: 포인트 2에서 모니터 획득. 그러나 while 문 때문에 active가 false이므로 모니터 다시 상실하고 다시 wait 상태로.
아래와 같은 결과 - 안 끝남.. WaitThread 2가 wait 상태에 빠졌는데 더 이상 아무도 안 깨워줌.

after notify 1
after notify 2
print 2


지금 모든 가능한 시나리오를 설명한 것도 아니고 쓰레드도 꼴랑 4개 뿐인데도 마구 복잡해질 기미가 보이고 있습니다.

위 프로그램에서의 문제는 갯수에 관련된 게 없다는 것입니다. wait이 기다리는 조건이 boolean 값이기 때문에 두번 true로 설정해 봤자 1번 true로 설정한 것과 별반 다르지 않지요.

뭔가 좀 비슷하지만 아래 프로그램을 실행시켜보면 멈추지 않습니다. 아래 프로그램은 notify에서 list에 값을 넣고, wait에서 그 list에서 값을 빼내는 프로그램입니다.  위에서 얘기한 시나리오 2가 다음과 같이 달라집니다.

NotifyThread 2: 포인트 3에서 모니터 획득. 할거 다하고 모니터 놓고 죽음.
WaitThread 1: 포인트 2에서 모니터 획득. 일 하고 죽고 모니터 놔주고.
WaitThread 2: 포인트 2에서 모니터 획득. while 문에서 보니 아직 list에 뭔가 남아있으므로 while문 빠져나오고 실행하고 죽고.
데드락 같은 거 안 생김!



import java.util.LinkedList;


public class ThreadTest{
private volatile LinkedList<Integer> list = new LinkedList<Integer>();

public synchronized void mWait() throws InterruptedException {
while (list.isEmpty()) {
wait(1000);
}
Integer i = list.pop();
System.out.println("print\t" + i);
}

public synchronized void mNotify(int i) {
notifyAll();
list.add(i);
System.out.println("after notify\t" + i);
}

private static class WaitCaller extends Thread{
private final ThreadTest threadTest;
public WaitCaller(ThreadTest threadTest) {
this.threadTest = threadTest;
}
public void run() {
try {
threadTest.mWait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class NotifyCaller extends Thread{
private final ThreadTest threadTest;
private final int i;
public NotifyCaller(ThreadTest threadTest, int i) {
this.threadTest = threadTest;
this.i = i;
}
public void run() {
threadTest.mNotify(i);
}
}
public static void main(String[] args) throws InterruptedException {
final ThreadTest threadTest = new ThreadTest();
final int cnt = 2;
for (int i = 0; i < cnt; i++) {
new WaitCaller(threadTest).start();
}
for (int i = 0; i < cnt; i++) {
new NotifyCaller(threadTest,i).start();
}
}
}


loop 안에서 wait을 걸어야 하는 이유?
여러 개의 쓰레드 중 wait에 관련된 thread가 있고, notify에 관련된 thread가 있는데, 모두 하나의 모니터 안에서 놀게 됩니다. 따라서 wait에 관련된 thread가 어떤 일을 처리하고 나서 다음에 모니터를 가지는 놈이 또 다른 wait에 관련된 놈이면 또 어떤 일을 처리하려고 할 겁니다.

위의 예제 중 두번째 예제를 보면 while문이 없을 경우에 문제가 생길 수 있습니다.
list에 1개가 있을 때 , WaitCalller 1이 list.pop()을 실행하고, 끝나려는데, WaitCaller 2도 list.pop()을 실행하려고 합니다. 이미 WaitCalller 1이 가져갔기 때문에 WaitCaller 2에서는 에러가 날 수 있습니다.



notify는 가급적 자제 하고 notifyAll을 쓰기를 권장합니다.

notify는 하나의 Thread만 깨웁니다. WaitThread 1,2가 잠자고 있을 때 notifyAll은 한 번만 호출 되어도 두 쓰레드가 모두 깨어나고,  모니터 쟁탈전을 하더라도 어쨌건 둘이 순차적으로 실행은 될 겁니다. 하지만, notify로 깨울 경우 둘 중 하나만 깨어나기 때문에 좀비 Thread가 생길 가능성이 생깁니다. 따라서 가급적 notifyAll()을 쓰는 게 바람직 합니다.


'Programming > JAVA' 카테고리의 다른 글

JUnit Thread Testing  (0) 2015.07.17
Test는 JUnit으로!  (0) 2015.07.17
enum의 뿌리를 찾아서  (0) 2015.07.02
[Java] 자신을 호출한 이전 메소드 혹은 클래스 정보 보기  (0) 2015.06.27
Make a Java Daemon with Jsvc on Linux  (0) 2015.06.05