[Java] 14. Thread 실행제어 및 상태 설명


Thread 실행제어 및 상태 설명에 대한 포스팅이다.



01. 쓰레드의 실행제어

쓰레드 프로그래밍이 까다로운 이유는 동기화(synchronization)와 스케줄링(scheduling)때문이다.

효율적인 멀티쓰레드 프로그램을 만들기 위해서는 보다 정교한 스케줄링을 통해
프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.


메서드설명
static void sleep(long millis)지정된 시간(천분의 일초 단위)동안 쓰레드를 일시정지 시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태가 된다.
void join()지정된 시간동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
void interrupt()sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다. 해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지상태를 벗어나게 한다.
void stop()쓰레드를 즉시 종료시킨다.
void suspend()쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행대기상태가 된다.
void resume()suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다.
static void yield()실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기상태가 된다.


!![참고] resume(), stop(), suspend()는 쓰레드를 교착상태(dead-lcok)로 만들기 쉽기 때문에 deprecated 되었다.



02. 쓰레드의 상태

상태설명
NEW쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
RUNNABLE실행 중 또는 실행 가능한 상태
BLOCKED동기화블럭에 의해서 일시정지된 상태(lock이 풀릴 떄까지 기다리는 상태)
WAITING,
TIMED_WAITING
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrunnable) 일시정지 상태.
TIMED_WAITING은 일시정지시간이 지정된 경우를 의미한다.
TERMINATED쓰레드의 작업이 종료된


쓰레드 상태(진행) 설명

1 ) 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐(queue)와 같은 구조먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.
2 ) 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.
3 ) 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.
4 ) 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다.
5 ) 지정된 일시정지시간이 다되거나(time-out), notify(), resume(), interrupt()가 호출되면 일시정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.
6 ) 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.



02-1. sleep()

  • 일정시간 동안 쓰레드를 멈추게 한다.
  • sleep()은 지정된 시간동안 쓰레드를 멈추게 한다. 밀리세컨드(millis, 1000분의 일초)와 나노세컨드(nanos, 10억분의 일초)의 시간단위로 세밀하게 값을 지정할 수 있지만 어느 정도의 오차가 발생할 수 있다는 것은 염두에 둬야 한다.
// sleep()
try{
	Thread.sleep(1, 500000); // Tmfpemfmf 0.0015초 동안 멈추게 한다.
} catch(InterruptedException e){}


sleep() 예제

  • 아래 예제에서 Thread1에 sleep을 th1.sleep(5000); 주어서 호출해도 실제로 영향을 받는건 Main 쓰레드이기 때문에 예제를 실제로 실행해보면 “«메인종료»” 문구가 제일 마지막에 늦게 출력되는 것을 볼 수 있다.
public class ExSleep {

    public static void main(String args[]) {
        SleepThread th1 = new SleepThread("1");
        SleepThread th2 = new SleepThread("2");

        th1.start();
        th2.start();

        try {
            th1.sleep(5000);
        } catch (InterruptedException e) {
        }

        System.out.print("<<메인종료>>");
    }
}

class SleepThread extends Thread {
    private String threadName = "";

    SleepThread(String threadName) {
        this.threadName = threadName;
    }

    public void run() {
        for (int i=0; i<100; i++) {
            System.out.print(this.threadName);
        }
        System.out.print("[#쓰레드-" + this.threadName + "종료]");
    }
}
11111122222222222222222111111112222222111111111222221111112221111111111111111111111111111112211112222222222222221111111111222221111111111111112222222222222222111111111122222222112222222222222222222222[#쓰레드-2종료][#쓰레드-1종료]<<메인종료>>



02-2. interrupt()

진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야 할 때가 있다. 예를 들어 큰 파일을 다운로드받을 때 시간이 너무 오래 걸리면 중간에 다운로드를 포기하고 취소할 수 있어야 한다. interrupt()는 쓰레드에게 작업을 멈추라고 요청한다. 단지 멈추라고 요청만 하는 것일 뿐 쓰레드를 강제로 종료시키지는 못한다.

  • interrupt() : interrupted상태(인스턴스 변수)를 바꾸는 것
  • interrupted() : 쓰레드에 대해 interrupt()가 호출되었는지 알려준다. interrupt()가 호출되지 않았다면 false를, interrupt()가 호출되었다면 true를 반환한다.
  • isInterrupted() : 쓰레드의 interrupt()가 호출되었는지 확인하는데 사용할 수 있지만, interrupted()와 달리 isInterrupted()는 쓰레드의 interrupted상태를 false로 초기화하지 않는다.


interrupt() 예제 (1)

public class ExInterruptThread {

    public static void main(String... args) throws Exception {
        InterruptThreadWork t = new InterruptThreadWork();
        t.start();

        //String input = JOptionPane.showInputDialog("취소입력 입력하세요.");
        int result = JOptionPane.showConfirmDialog(null
                                                , "다운로드를 취소하겠습니까?");
        System.out.println("result " + result);
        System.out.println("전 : isInterrupt() : " + t.isInterrupted());

        if (result == 0) t.interrupt(); //interrupt()을 호출하면, interrupt 상태가 true
        System.out.println("후 : isInterrupt() : " + t.isInterrupted());
    }
}
class InterruptThreadWork extends Thread {

    @Override
    public void run() {
        int i = 10;

        while (i!=0 && !isInterrupted()) {
            System.out.println(i--);

            for(long x=0; x<2500000000L; x++);
        }

        if (isInterrupted()) {
            System.out.println("다운로드가 취소되었습니다.");
        } else {
            System.out.println("다운로드가 완료되었습니다.");
        }
    }
}



interrupt() 예제 (2) - sleep() : 실행대기상태 경우


쓰레드가 sleep(), wait(), join()에 의해 ‘일시정지 상태(WAITING)’에 있을 때, 해당 쓰레드에 대해 interrupt()를 호출하면, sleep(), wait(), join()에서 '실행대기 상태(RUNNABLE)'로 바뀐다. 해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지상태를 벗어나게 한다. 즉, 멈춰있던 쓰레드를 깨워서 실행가능한 상태로 만드는 것이다.


class InterruptThreadWork2 extends Thread {

    @Override
    public void run() {
        int i = 10;

        while (i!=0 && !isInterrupted()) {
            System.out.println(i--);

            //sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다.
            //java.lang.InterruptedException: sleep interrupted 발생!!
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                System.out.println(e);
                interrupt();
            }
        }

        if (isInterrupted()) {
            System.out.println("다운로드가 취소되었습니다.");
        } else {
            System.out.println("다운로드가 완료되었습니다.");
        }
    }
}
10
9
8
result 0
전 : isInterrupt() : false
java.lang.InterruptedException: sleep interrupted
다운로드가 취소되었습니다.
후 : isInterrupt() : true

만약 interrupt() 메서드를 실행하지 않으면 다운로드를 취소해도 계속 진행된다. 왜냐하면 sleep() 의 예외처리가 "java.lang.InterruptedException: sleep interrupted" 되면 쓰레드의 interrupted 상태는 false로 자동 초기화된다.

그래서 interrupt()를 호출해야 interrupted 상태를 true 로 변경해서 쓰레드가 중단되는 것을 구현할 수 있다.



02-3. suspend(), resume(), stop()

suspend()는 sleep()처럼 쓰레드를 멈추게 한다. suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행대기 상태가 된다. stop()은 호출되는 즉시 쓰레드가 종료된다.

suspend(), resume(), stop()은 쓰레드의 실행을 제어하는 가장 쉬운 방법이지만, suspend()와 stop()이 교착상태(deadlock)를 일으키기 쉽게 작성되어 있으므로 사용이 권장되지 않는다. 그래서 이 메서드들은 모두 'deprecated'되었다.



02-4. join()

  • 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.
void join() //작업이 모두 끝날 때까지
void join(long millis)  //천분의 1초동안
void join(long millis, int nanos)//천분의 1초동안 + 나노초 동안


join() 예제


public class ExJoin {

    public static void main(String args[]) {
        JoinThread th1 = new JoinThread("1");
        JoinThread th2 = new JoinThread("2");

        th1.start();
        th2.start();

        try {
            th1.join(); //thread-1 쓰레드가 종료 될 때까지 main 쓰레드는 대기상태
            th2.join(); //thread-2 쓰레드가 종료 될 때까지 main 쓰레드는 대기상태
        } catch (InterruptedException e) {
        }

        System.out.print("<<메인종료>>");
    }
}

class JoinThread extends Thread {
    private String threadName = "";

    JoinThread(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run() {
        for (int i=0; i<100; i++) {
            System.out.print(this.threadName);
        }
        System.out.print("[#쓰레드-" + this.threadName + "종료]");
    }
}
22222222222222222222222222211111111111122211122222222222222222222222111111111111111111222222222111111222221111111111122222222222222221111111112222222111111111111111111111111111111111111111112222222222[#쓰레드-2종료][#쓰레드-1종료]<<메인종료>>



코드로 join() 이해를 하기 위해서 만약 아래 코드가 주석처리하면 main 쓰레드는 쓰레드-1, 쓰레드-2 가 작업이 끝날 떄까지 기다리지 않기에 먼저 종료되고 이후에 쓰레드1, 2가 종료되는 것을 볼 수 있다.

    // try {
    //     th1.join(); //thread-1 쓰레드가 종료 될 때까지 main 쓰레드는 대기상태
    //     th2.join(); //thread-2 쓰레드가 종료 될 때까지 main 쓰레드는 대기상태
    // } catch (InterruptedException e) {
    // }
<<메인종료>>22222222222222222222222222211111111122222222222211111111111111111111111112222222222222222221111111111111111111111111111111111111111111111111112222222111111112222222222222222222111222222221112222222221[#쓰레드-1종료][#쓰레드-2종료]



예시로

  • 1초마다 에너지를 소모를 하는 메인쓰레드와
  • 3초마다 에너지를 충전을 하는 에너지쓰레드가 있다면
  • 에너지가 20% 이하이면 자고 있는 에너지쓰레드를 깨워서, join() 으로 메인 메서드가 지정된 시간동안 특정 쓰레드가 작업(에너지 충전)하는 것을 기다린다.


sleep()을 이용해서 주기적으로 실행되도록 하다가 필요할 때마다 interrupt()를 호출해서 즉시 특정 쓰레드를 즉시 처리하도록 하는 것이 좋다.
상황에 따라 필요하다면 join()도 함께 사용해야한다.



02-5. yield()

  • 남은 시간을 다음 쓰레드에게 양보하고, 자신(현재 쓰레드)는 실행대기한다.
  • yield()와 interrupt()를 적절히 사용하면 응답성과 효율을 높일 수 있다.

예를 들어 하나의 쓰레드가 프로세스를 너무 오랫동안 점유할때(쓰레드가 특정 점유시간을 초과시) 강제로 양보를 해서 실행대기상태로 변경되서 여러 쓰레드가 자원을 같이 쓸때 필요할 듯 하다.



yield() 사용 예제

public class ExYield {

    public static void main(String args[]) {
        YieldThread th1 = new YieldThread("쓰레드-1");
        YieldThread th2 = new YieldThread("쓰레드-2");

        th1.start();
        th2.start();


        try { Thread.sleep(5000); } catch (InterruptedException e) { }
        //쓰레드-2 만 작업 진행
        th1.pauseThread();

        try { Thread.sleep(5000); } catch (InterruptedException e) { }
        //쓰레드-1 다시 작업 진행
        th1.restartThread();

        try { Thread.sleep(5000); } catch (InterruptedException e) { }
        //둘다 작업 종료
        th1.stopThread();
        th2.stopThread();
    }
}

class YieldThread extends Thread {
    private String threadName = "";
    private boolean stop = false;
    private boolean work = true;

    public boolean isStop() {
        return stop;
    }

    public boolean isWork() {
        return work;
    }

    public void pauseThread() {
        this.interrupt();
        this.work = false;
        System.out.println( this.threadName + " 작업 중지");
    }

    public void restartThread() {
        this.interrupt();
        this.work = true;
    }

    public void stopThread() {
        stop = false;
    }
    
    YieldThread(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run() {

        while (!isStop()) {
            try { Thread.sleep(1000); } catch (InterruptedException e) { }

            if (isWork()) {
                System.out.println(this.threadName + " 작업 중");
            } else {
                //System.out.println("다른 쓰레드에게 양보");
                Thread.yield();//다른 스레드에 양보
            }
        }
    }
}




[출처]

  • 자바의정석 (저자: 남궁성)