자바 쓰레드 (5) - 쓰레드 제어 (1)

자바

2020. 4. 19. 21:43

가장 핵심인 쓰레드 제어이다. 멀티쓰레드 프로그래밍이 어려운 이유는 쓰레드 간의 스케줄링을 다룰 때 매우 세심하고 조심스럽게 다뤄야 하기 때문이다.

 

0. Thread Life Cycle

가장 먼저 쓰레드의 라이프사이클을 살펴보자. 쓰레드의 라이프사이클은 OS 수업에서 배운 프로세스의 라이프사이클과 굉장히 흡사하다.

 

public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
}

public State getState() {
    return sun.misc.VM.toThreadState(threadStatus);
}
상태 설명
NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
RUNNABLE 실행 중 또는 실행 가능한 상태
BLOCKED 동기화 블럭에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태)
WAITING, TIMED_WAITING 쓰레드의 작업이 종료되지는 않았지만, 실행가능하지 않은 일시정지 상태
TERMINATED 쓰레드의 작업이 종료된 상태

 

  1. 가장 먼저 쓰레드를 생성하면 NEW 상태가 된다.
  2. 그리고 start()를 호출하면 RUNNABLE 상태가 된다.
  3. 그리고 바로 실행되는 것이 아니라 Queue에 들어가서 자신의 차례가 될 때 까지 기다린다
  4. 그러다가 자신의 차례가 되면 실행 상태가 된다.
  5. 주어진 시간이 다 되거나 yield()를 만나면 다시 대기상태가 되고 다음 차례의 쓰레드가 자신의 차례가 된다.
  6. 또한, 실행 중 suspend(), wait(), sleep(), join()에 의해서도 일시정지 상태가 될 수 있다
  7. 자신의 일시정지 기간이 끝나거나, notify(), resume() 등이 호출되면 다시 대기열에 저장돼 차례를 기다린다
  8. 실행을 모두 마치거나 stop()을 호출하면 쓰레드는 소멸한다.

 

1. sleep()

일정 시간동안 쓰레드를 멈추게 한다. 또한, TimeUnit을 사용하면 조금 더 명시적으로 쓰레드를 sleep할 수 있다.

 

1) 주의할점 : InterruptedException

sleep에 의해 일시정지된 쓰레드가 인터럽트를 받으면 InterruptedException이 걸린다

class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println(i);
                    Thread.sleep(1000);
                }

            } catch (InterruptedException e) {
                System.out.println("interrupt service routine");
                System.out.println("인터럽트 걸릴 때, 대체 작업 수행");
            }
        });


        t.start();
        Thread.sleep(5000);
        t.interrupt();
    }
}
0
1
2
3
4
interrupt service routine
인터럽트 걸릴 때, 대체 작업 수행

Process finished with exit code 0

 

2) 주의할 점 2 : sleep은 인스턴스 메소드가 아니라 클래스 메소드이다.

class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread aThread = new ThreadA();
        Thread bThread = new ThreadB();

        aThread.start();
        bThread.start();

        aThread.sleep(2000);
        for (int i = 0; i < 100; i++) {
            System.out.print("M");
        }
        System.out.print("<MAIN 종료>");
    }
}

class ThreadA extends Thread {
    @Override public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("A");
        }
        System.out.print("<A 종료>");
    }
}

class ThreadB extends Thread {
    @Override public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("B");
        }
        System.out.print("<B 종료>");
    }
}
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB<A 종료><B 종료>MMMMMMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMM<MAIN 종료>

이렇게 짜면 Thread A가 B보다 2초 정도 늦게 끝날 것 같지만 거의 동시에 끝나고 MAIN이 2초 끝난다. Thread.sleep은 클래스 메소드이기 때문에 인스턴스에서 호출하면 안된다. 이 경우, sleep이 실행된 MAIN쪽의 쓰레드가 2초 멈추게 된다.

 

2. interrupt() / interrupted()

  • interrupt() : 현재 쓰레드에서 작업중인 업무를 종료하고, 즉시 다른 작업을 시킬 수 있음 (interrupt = true)
  • isInterrupted() : 현재 쓰레드의 인터럽트 상태 반환
  • interrupted() : 인터럽트에 걸렸었는지 출력하고, 다시 interrupt = false로 만듬

 

쓰레드가 interrupted에 걸리게 되면, sleep(), join(), wait() 등의 메소드에 의해 Waiting하고 있다고 하더라도, 다시 살아나서 Runnable 상태가 된다. 즉, 기다리고 있는 쓰레드를 바로 깨워서 쓸 수 있다.

class Main {
    public static void main(String[] args) throws InterruptedException {
        InterruptServiceRoutine routine = new InterruptServiceRoutine();
        routine.start();

        Thread.sleep(5000);
        routine.setMsg("mouse");
        routine.interrupt();

        Thread.sleep(5000);
        routine.setMsg("hdd");
        routine.interrupt();

        Thread.sleep(5000);
        routine.setMsg("exit");
        routine.interrupt();

    }
}

class InterruptServiceRoutine implements Runnable {

    private String msg = "";
    private int process = 0;
    private Thread thread = new Thread(this);

    public void start() {
        thread.start();
    }

    public void interrupt() {
        thread.interrupt();
    }

    public void setMsg(String msg) {
        this.msg = msg.toLowerCase();
    }

    @Override public void run() {
        try {
            for (; process < 100; process++) {
                System.out.println("작업중 : " + process + "%");
                Thread.sleep(1000);
            }

        } catch (InterruptedException e) {
            switch (msg) {
                case "mouse":
                    System.out.println("마우스 움직임");
                    break;
                case "hdd":
                    System.out.println("HDD접근");
                    break;
                case "exit":
                    System.out.println("프로그램 종료");
                    System.exit(0);
                default:
                    System.out.println("잘못된 인터럽트");
                    break;
            }

            thread = new Thread(this);
            thread.start();
        }
    }
}
작업중 : 0%
작업중 : 1%
작업중 : 2%
작업중 : 3%
작업중 : 4%
마우스 움직임
작업중 : 4%
작업중 : 5%
작업중 : 6%
작업중 : 7%
작업중 : 8%
HDD접근
작업중 : 8%
작업중 : 9%
작업중 : 10%
작업중 : 11%
작업중 : 12%
프로그램 종료

Process finished with exit code 0

 

만약 sleep과 같이 Interrupt Exception을 지원하지 않는 작업(for long x : 0 -> 50...0L)을 진행하다가 인터럽트를 걸고 싶다면 아래처럼 if 분기로 검사해야줘야한다.

class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        counter.start();

        Thread.sleep(3000);
        counter.interrupt();

        Thread.sleep(3000);
        counter.interrupt();
    }
}

class Counter extends Thread {

    public void run() {
        int i = 10;
        while (i != 0) {
            System.out.println(i--);
            for (long x = 0; x < 5000000000L; x++) ;

            if(Thread.interrupted()){
                System.out.println("인터럽트 발생");
            }
        }
    }
}
10
9
8
7
6
인터럽트 발생
5
4
3
2
1

Process finished with exit code 0

 

 

interrupted()를 사용하면 interrupt 상태가 false로 바뀌어서 "인터럽트 발생" 부분이 다시 실행되지 않지만 만약 interrupted가 아닌, isInterrupted()를 사용한다면 아래와 같이 출력된다.

10
9
8
7
6
5
4
인터럽트 발생
3
인터럽트 발생
2
인터럽트 발생
1
인터럽트 발생

Process finished with exit code 0

 

while문의 조건에 인터럽트 여부를 걸어버리는 방법도 꽤 유용하다

class Main {
    public static void main(String[] args) throws InterruptedException {
        Locker locker = new Locker();
        locker.start();

        Thread.sleep(5000);
        locker.interrupt();
    }
}

class Locker extends Thread {

    public void run() {
        while (!isInterrupted()) {
            System.out.println("lock");
            for (long x = 0; x < 5000000000L; x++) ;
        }

        System.out.println("unlock");
    }
}
lock
lock
lock
lock
lock
unlock

Process finished with exit code 0