본문 바로가기

Java 길찾기/이것이 자바다

[Java] 스레드 상태 제어 - join(), wait(), notify(), notifyAll()

다른 스레드의 종료를 기다림(join())

스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있다. 예를 들어 계산 작업을 하는 스레드가 모든 계산 작업을 마쳤을 때, 계산 결과값을 받아 이용하는 경우가 이에 해당한다. 이런 경우를 위해서 Thread는 join() 메소드를 제공하고 있다. 다음 그림을 보고 이해해보자. ThreadA가 ThreadB의 join() 메소드를 호출하면 ThreadA는 ThreadB가 종료할 때까지 일시 정지 상태가 된다. ThreadB의 run() 메소드가 종료되면 비로소 ThreadA는 일시 정지에서 풀려 다음 코드를 실행하게 된다.

 

 

다음 예제를 보면 메인 스레드는 sumThread가 계산 작업을 모두 마칠 때까지 일시 정지 상태에 있다가 SumThread가 최종 계산된 결과값을 산출하고 종료하면 결과값을 받아 출력한다.

// SumThread.java --  1부터 100까지 합을 계산하는 스레드
public class SumThread extends Thread {
    private long sum;
    
    public long getSume() {
        return sum;
    }
    
    public void setSum(long sum) {
        this.sum = sum;
    }
    
    public void run() {
        for(int i=0; i<=100; i++) {
            sun+=1;
        }
    }
}
// JoinExample.java -- 다른 스레드가 종료될 때까지 일시 정지 상태 유지
public class JoinExample {
    public static void main(String[] args) {
        SumThread sunThread = new SumThread();
        sumThread.start();
        
        try {
            sunThread.join();
        } catch(InterruptedException e) {}
        
        System.out.println("1~100 합: " + sumThread.getSum());
    }
}

 

JoinExample 클래스의 6~9라인을 주석 처리하고 실행하면 1~100까지의 합은 0이 나오게 된다. 그 이유는 SumThread가 계산 작업을 완료하지 않은 상태에서 합을 먼저 출력하기 때문이다.

 

스레드간 협업(wait(), notify(), notifyAll())

경우에 따라서는 두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있다. 정확한 교대 작업이 필요한 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것이다. 이 방법의 핵심은 공유 객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 구분해 놓는다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.

 

만약 wait() 대신 wait(long timeout)이나, wait(long timeout, int nanos)를 사용하면 notify()를 호출하지 않아도 지정된 시간이 지나면 스레드가 자동적으로 실행 대기 상태가 된다. notify() 메소드와 동일한 역할을 하는 notifyAll() 메소드도 있는데, notify()는 wait()에 의해 일시 정지된 스레드 중 한 개를 실행 대기 상태로 만들고, notifyAll() 메소드는 wait()에 의해 일시 정지된 모든 스레드들을 실행 대기 상태로 만든다. 이 메소드들은 Thread 클래스가 아닌 Object 클래스에 선언된 메소드이므로 모든 공유 객체에서 호출이 가능하다. 주의할 점은 이 메소드들은 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다. 다음 예제는 두 스레드의 작업을 WorkObject의 methodA()와 methodB()에 정의해 두고, 두 스레드 ThreadA와 ThreadB가 교대로 methodA()와 methodB()를 호출하도록 했다.

// WorkObject.java -- 두 스레드의 작업 내용을 동기화 메소드로 작성한 공유 객체
public class WorkObject {
    public synchronized void methodA() {
        System.out.println("ThreadA의 methodA() 작업 실행");
        notify();
        try {
            wait();
        } catch(InterruptedException e) {}
    }
    
    public synchronized void methodB() {
        System.out.println("ThreadA의 methodB() 작업 실행");
        notify();
        try {
            wait();
        } catch(InterruptedException e) {}
    }
}
// ThreadA.java -- WorkObject의 methodA()를 실행하는 스레드
public class ThreadA extends Thread {
    private WorkObject workObject;
    
    public ThreadA(WorkObject workObject) {
        this.workObejct = workObejct;
    }
    
    @Override
    public void run() {
        for(int i=0; i<10; i++) {
            workObject.methodA();
        }
    }
}
// ThreadB.java -- WorkObject의 methodB()를 실행하는 스레드
public class ThreadB extends Thread {
    private WorkObject workObject;
    
    public ThreadB(WorkObject workObject) {
        this.workObejct = workObejct;
    }
    
    @Override
    public void run() {
        for(int i=0; i<10; i++) {
            workObject.methodB();
        }
    }
}
// WaitNotifyExample.java -- 두 스레드를 생성하고 실행하는 메인 스레드
public class WaitNotifyExample {
    public static void main(String[] args) {
        WorkObject sharedObject = new WorkObject();
        
        ThreadA threadA = new ThreadA(sharedObject);
        ThreadB threadB = new ThreadB(sharedObject);
        
        ThreadA.start();
        ThreadB.start();
    }
}

 

다음 예제는 데이터를 저장하는 스레드(생산자 스레드)가 데이터를 저장하면, 데이터를 소비하는 스레드(소비자 스레드)가 데이터를 읽고 처리하는 교대 작업을 구현한 것이다.

생산자 스레드는 소비자 스레드가 읽기 전에 새로운 데이터를 두 번 생성하면 안 되고(setData() 메소드를 두 번 실행하면 안됨), 소비자 스레드는 생산자 스레드가 새로운 데이터를 생성하기 전에 이전 데이터를 두 번 읽어서도 안 된다(getData() 메소드를 두 번 실행하면 안 됨). 구현 방법은 공유 객체(DataBox)에 데이터를 저장할 수 있는 data 필드의 값이 null 이면 생산자 스레드를 실행 대기 상태로 만들고, 소비자 스레드를 일시 정지 상태로 만드는 것이다. 반대로 data 필드의 값이 null 이 아니면 소비자 스레드를 실행 대기 상태로 만들고, 생산자 스레드를 일시 정지 상태로 만들면 된다.

// DataBox.java -- 두 스레드의 작업 내용을 동기화 메소드로 작성한 공유 객체
public class DataBox {
    private String data;
    
    public synchronized String getData() {
        if(this.data == null) {
            try {
                wait();
            } catch(InterruptedException e) {}
        }
        String returnValue = data;
        System.out.println("ConsummerThread가 읽은 데이터: " + returnValue);
        data = null;
        notify();
        return returnValue;
    }
    
    public synchronized void setDate(String data) {
        if(this.data != null) {
            try {
                wait();
            } catch(InterruptedException e) {}
        }
        this.data = data;
        System.out.println("ProducerThread가 생성한 데이터: " + data);
        notify();
    }
}
// ProducerThread.java -- 데이터를 생산(저장)하는 스레드
public class ProducerThread extends Thread {
    private DataBox dataBox;
    
    public ProdcerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }
    
    @Override
    public void run() {
        for(int i=1; i<=3; i++) {
            String data = "Data-" + i;
            dataBoc.setData(data);
        }
    }
}
// ConsumerThread.java -- 데이터를 소비(읽는)하는 스레드
public class ConsumerThread extends Thread {
    private DataBox dataBox;
    
    public ConsumerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }
    
    @Override
    public void run() {
        for(int i=1; i<=3; i++) {
            String data = dataBox.getData();
        }
    }
}
// WaitNotifyExample .java -- 두 스레드를 생성하고 실행하는 메인 스레드
public class WaitNotifyExample {
    public static void main(String[] args) {
        DataBox dataBox = new DataBox();
        
        ProducerThread() producerThread = new ProducerThread(dataBox);
        ConsumerThread() consumerThread = new ConsumerThread(dataBox);
        
        producerThread.start();
        consumerThread.start();
    }
}