elevne's Study Note

Java - Multi Thread (2) 본문

Backend/Java

Java - Multi Thread (2)

elevne 2023. 5. 9. 21:57

스레드 객체를 생성하고 start() 메서드를 호출하면 곧바로 스레드가 실행되는 것처럼 보이지만, 사실은 실행 대기 중인 상태가 된다. 실행 대기 상태란 아직 스케줄링이 되지 않아서 실행을 기다리고 있는 상태를 말한다. 실행 대기 상태에 있는 스레드 중에서 스레드 스케줄링으로 선택된 스레드가 비로소 CPU 를 점유하고 run() 메서드를 실행하며 실행 상태가 된다. 경우에 따라 실행 중인 스렐드는 실행 상태에서 (스케줄링으로 인해) 다시 실행 대기 상태로 가지 않고 일시 정지 상태로 가기도 한다. 일시 정지 상태는 스레드가 실행할 수 없는 상태로 WAITING, TIMED_WAITING, BLOCKED 세 종류의 상태로 또 나뉠 수 있다. 이러한 스레드의 상태들은 Java 에서도 getState() 메서드를 통해 확인해볼 수 있다.

 

 

 

사용자는 실행 중인 스레드의 상태를 변경할 수 있다. 이를 스레드 상태 제어라고 하는데, 멀티 스레드 프로그래밍에서는 정교한 스레드 상태 제어가 필요하다. 스레드 상태 제어를 잘못 사용하면 치명적인 프로그램 버그가 될 수 있기에 상태 변화를 가져오는 메서드를 정확하게 파악하고 있어야 한다고 한다. 

 

 

Method Description
interrupt() 일시정지 상태의 스레드에서 InterruptedException 예외를 발생시켜, 예외처리 코드에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 한다.
notify(), notifyAll() 동기화 블록 내에서 wait() 메서드에 의해 일시정지 상태에 있는 스레드를 실행 대기 상태로 만든다.
resume() suspend() 메서드에 의해 일시정지 상태에 있는 스레드를 실행 대기 상태로 만든다.  => Deprecated: 대신 notify(), notifyAll() 메서드 사용
sleep(long millis), sleep(long millis, int nanos) 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행대기 상태가 된다.
join(), join(long millis), join(long millis, int nanos) join() 메서드를 호출한 스레드는 일시정지 상태가 된다. 실행대기 상태로 가려면 join() 메서드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나야 한다.
wait(), wait(long millis), wait(long millis, int nanos) 동기화 블록 내에서 스레드를 일시 정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify(), notifyAll() 메서드에 의해 실행대기 상태로 갈 수 있다.
suspend() 스레드를 일시정지 상태로 만든다. resume() 메서드를 호출하면 다시 실행대기 상태가 된다. => Deprecated: 대신 wait() 사용
yield() 실행 중에 우선순위가 동일한 다른 스레드에 실행을 양보하고 실행대기 상태가 된다.
stop() 스레드를 즉시 종료시킨다. => Deprecated

 

 

 

sleep()

 

실행 중인 스레드를 일정 시간 멈추게 하는 코드로, 지금까지 계속 사용해왔다. 매개값으로는 얼마 동안 일시정지 상태로 있을 것인지, 밀리세컨드 (1/1000) 단위로 시간을 받는다. 일시정지 상태에서 주어진 시간이 되기 전에 interrupt() 메서드가 호출되면 InterruptedException 이 발생하기 때문에 예외처리가 필요하다.

 

 

 

yield()

 

스레드가 처리하는 작업은 반복적인 실행을 위해 반복문을 포함하는 경우가 많다. 그런데 가끔은 아래와 같이 반복문들이 무의미한 반복을 하는 경우가 있다고 한다. 

 

 

package multithreading;

public class ThreadA extends Thread {
    public boolean work = true;

    public void run() {
        while (true) {
            if (work) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
                System.out.println("ThreadA WORK");
            } 
        }
    }
}

 

 

스레드가 시작되어 run() 메서드를 실행하면 while (true) {} 블록이 무한 반복 실행된다. 만약 work 의 값이 false 라면, 그리고 work 의 값이 false 에서 true 로 변경되는 시점이 불명확하다면 while 문은 어떠한 실행문도 실행하지 않고 무의미한 반복을 한다. 이것보다는 다른 스레드에게 실행을 양보하고 자신은 실행대기 상태로 가는 것이 전체 프로그램 성능에 도움이 되는 것이다. 아래와 같이 작성해볼 수 있다. (ThreadA, ThreadB 클래스는 동일하게 작성)

 

 

package multithreading;

public class ThreadA extends Thread {
    public boolean work = true;
    public boolean stop = false;

    public void run() {
        while (!stop) {
            if (work) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
                System.out.println("ThreadA WORK");
            } else {
                Thread.yield();
            }
        }
        System.out.println("ThreadA STOPPED");
    }
}

 

 

public class Main {

    public static void main(String[] args) {

        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();
        threadA.start();
        threadB.start();

        try {Thread.sleep(3000);} catch (InterruptedException e) {}
        threadA.work = false;

        try {Thread.sleep(3000);} catch (InterruptedException e) {}
        threadA.work = true;

        try {Thread.sleep(3000);} catch (InterruptedException e) {}
        threadA.stop = true;
        threadB.stop = true;
    }
}

 

 

 

join()

 

스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있다. (e.g., 계산 작업을 하는 스레드가 모든 계산 작업을 마쳤을 때 계산 결과값을 받아 이용하는 경우) 이 때 사용할 수 있는 것이 join() 메서드이다. ThreadA, B 가 있을 때 만약 ThreadA 에서 ThreadB 의 join() 메서드를 호출하면 ThreadA 는 ThreadB 가 종료할 때까지 일시정지 상태가 된다. ThreadB 의 run() 메서드가 종료되면 ThreadA 는 일시정ㅈ에서 풀려 다음 코드를 실행할 수 있다. 

 

 

package multithreading;

public class SumThread extends Thread {
    private long sum;
    public long getSum() {
        return sum;
    }
    public void setSum(long sum) {
        this.sum = sum;
    }

    public void run() {
        for (int i = 0; i <1000; i++) {
            sum += i;
        }
    }
}

 

 

public class Main {

    public static void main(String[] args) {

        SumThread sumThread = new SumThread();
        sumThread.start();

        try {
            sumThread.join();
        } catch (InterruptedException e) {}

        System.out.println("1~1000 SUM: " + sumThread.getSum());

    }
}

 

 

 

wait(), notify(), notifyAll()

 

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

 

 

notify() 메서드는 wait() 에 의해 일시정지된 스레드 중 한 개를 실행대기 상태로 만들고, notifyAll() 메서드는 wait() 에 의해 일시정지된 모든 스레드들을 실행대기 상태로 만든다. 이 메서드들은 Thread 클래스가 아닌 Object 클래스에 선언된 메서드들로 모든 공유 객체에서 호출이 가능하다. 주의할 점으로, 이 메서드들은 동기화 메서드 혹은 동기화 블록 내에서만 사용할 수 있다. 

 

 

package multithreading;

public class WorkObjct {
    
    public synchronized void methodA() {
        System.out.println("ThreadA methodA WORK");
        notify();
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    
    public synchronized void methodB() {
        System.out.println("ThreadB methodB WORK");
        notify();
        try {
            wait();
        } catch (InterruptedException e) {} 
    }
    
}

 

 

public class ThreadA extends Thread {
    private WorkObjct workObjct;
    public ThreadA(WorkObjct workObjct) {
        this.workObjct = workObjct;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            workObjct.methodA();
        }
    }
}

 

 

public class Main {

    public static void main(String[] args) {

        WorkObjct w = new WorkObjct();
        ThreadA threadA = new ThreadA(w);
        ThreadB threadB = new ThreadB(w);

        threadA.start();
        threadB.start();

    }
}

 

 

위 코드는 ThreadA 와 ThreadB 가 교대로 methodA(), methodB() 를 번갈아가며 호출하게 된다.

 

 

그 다음으로는 데이터를 저장하는 스레드가 데이터를 저장하면, 데이터를 소비하는 스레드가 데이터를 읽고 처리하는 교대 작업을 구현해본다. 생산자 스레드는 소비자 스레드가 읽기 전에 새로운 데이터를 두 번 생성하면 안 되고 소비자 스레드는 생산자 스레드가 새로운 데이터를 생성하기 전에 이전 데이터를 두 번 읽어서도 안 된다. 이는 공유 객체에 데이터를 저장할 수 있는 data 필드의 값이 null 이면 생산자 스레드를 실행대기 상태로 만들고, 소비자 스레드를 일시정지 상태로 만듦으로써 구현할 수 있다. (반대로 null 이 아니면 소비자 스레드를 실행대기 상태, 생산자 스레드를 일시정지 상태로 만든다)

 

 

package multithreading;

public class DataBox {
    private String data;
    
    public synchronized String getData() {
        if (this.data == null) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        String returnValue = this.data;
        System.out.println("CONSUMER THREAD: "+returnValue);
        this.data = null;
        notify();
        return returnValue;
    }
    
    public synchronized void setData(String data) {
        if (this.data != null) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        this.data = data;
        System.out.println("PRODUCER THREAD: "+data);
        notify();
    }
}

 

 

package multithreading;

public class ProducerThread extends Thread {

    private DataBox dataBox;

    public ProducerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            String data = "DATA-"+i;
            dataBox.setData(data);
        }
    }
}

 

 

package multithreading;

public class ConsumerThread extends Thread {
    
    private DataBox dataBox;
    
    public ConsumerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            String data = dataBox.getData();
        }
    }
    
}

 

 

public class Main {

    public static void main(String[] args) {

        DataBox dataBox = new DataBox();

        ProducerThread producerThread = new ProducerThread(dataBox);
        ConsumerThread consumerThread = new ConsumerThread(dataBox);
        producerThread.start();
        consumerThread.start();

    }
}

 

 

result

 

 

 

interrupt()

 

스레드는 자신의 run() 메서드가 모두 실행되면 자동적으로 종료되지만, 경우에 따라 실행 중인 스레드가 즉시 종료되어야 할 때도 있다. 기존에는 이를 위해 stop() 메서드가 있었지만 deprecated 되었다. (stop() 메서드로 스레드를 갑자기 종료할 경우 스레드가 사용 중이던 자원들이 불안전한 상태로 남겨졌기 때문)  

 

 

stop() 을 대체할 방법의 최선은 run() 메서드가 정상적으로 종료되도록 유도하는 것이다. 위 코드에서 적용해보았던 것처럼 while 문 내에 조건을 외부에서 false 로 맞춰주는 방법이 하나일 것이다. 그 다음 방법으로는 interrupt() 메서드를 사용하는 것이 있다. 이는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다. 이를 이용하면 run() 메서드를 종료시킬 수 있다. 

 

 

interrupt() 메서드는 실행대기 또는 실행 상태에 있는 스레드에 바로 예외를 발생시키는 것이 아니라, 스레드가 미래에 일시정지 상태가 되면 InterruptedException 을 발생시킨다. 

 

 

 

 

 

 

 

Reference:

이것이 자바다

'Backend > Java' 카테고리의 다른 글

Java - Multi Thread (3)  (0) 2023.05.13
Java - Map Compute, To JSON  (0) 2023.05.10
Java - Multi Thread (1)  (0) 2023.05.07
Java 중첩클래스/인터페이스, 익명클래스  (0) 2023.05.04
HttpServletRequest, HttpServletResponse  (0) 2023.04.30