[Java] Thread - 2 스레드 상태 제어

Hyeonsu Bang·2021년 12월 18일
0

Java Basic

목록 보기
9/11
post-thumbnail

스레드 상태

스레드 실행 순서

스레드를 생성하고 start() 를 호출하면 바로 스레드가 실행될 것 같지만, 사실 실행 대기(Runnable) 상태가 된다. 실행 대기 상태란 스케줄링이 되지 않아서 실행을 기다리고 있는 상태를 말한다.

실행 대기 상태에 있는 스레드 중에서 스레드 스케줄링으로 선택된 스레드가 CPU를 점유하고 run()메소드를 실행한다. 이때를 실행(Running) 상태라고 한다.

실행 상태의 스레드는 run() 메서드를 실행 완료하기 전에 스레드 스케줄링(time slice)에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 또 다른 스레드가 스케줄링에 의해 실행 상태가 된다. 이렇게 번갈아가며 여러 스레드가 자신의 run() 를 조금씩 실행한다. 실행 상태에서 run() 가 종료되면 스레드의 실행도 멈추게 된다. 이 상태를 종료 상태(Terminated)라 한다.



실행 순서와 관련하여 아래 코드를 보자.

public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();

        Calc calc = new Calc();
        User1 user1 = new User1();
        user1.setCalc(calc);

        user1.start(); // user1 

        User2 user2 = new User2();
        user2.setCalc(calc);

        user2.start(); // user2
        System.out.println(calc.getMemory()); // main

앞선 예제에서와 같이 main, user1, user2 스레드를 실행시켰다. calc는 공유되고 있고 user2 스레드가 user1보다 더 늦게 값을 세팅하므로 `calc.getMemory()` 의 결과는 50일 것이라고 예상했다. 하지만 위 실행 결과는 다음과 같다.

User1 :100 // user1
100 // main
User2 :50 // user2 

코딩된 순서로 보면 user2가 user1 다음에 출력되고 main 스레드의 syso가 마지막에 출력될 것 같지만, 그렇지 않다. 그리고 코딩 상으로는 user2 스레드 시작 후에 calc 값을 호출했으므로 당연히 50이 출력될 것으로 예상했지만, user1의 값이었던 100이 출력되었다.


main 스레드를 잠시 멈추고 `user2.setCalc()`가 호출될 때까지 기다린 뒤 다시 출력하는 코드를 추가했다.

public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread();

        Calc calc = new Calc();
        User1 user1 = new User1();
        user1.setCalc(calc);

        user1.start(); // user1 

        User2 user2 = new User2();
        user2.setCalc(calc);

        user2.start(); // user2
        System.out.println(calc.getMemory()); // main
				**mainThread.sleep(1000);
        System.out.println(calc.getMemory());
}**

출력결과는 다음과 같다.
User1 :100 // user1
100 // main
User2 :50 // user2
50 // main


보는 것처럼 시간을 조금 두고 다시 calc.getMemory() 를 해보면 값이 변해있다는 걸 확인할 수 있다. 즉 start() 를 호출한다고 해서 바로 스레드가 실행되는 것이 아니라 스레드 스케줄링에 의해서 적절한 시점에 시작되며, 시작하는 시간까지의 차이가 있다는 것이다.


일시정지 상태

스레드의 생명주기는 생성 → 실행대기 → 실행 → 실행 종료이다. 스레드가 실행 대기 상태일 때는 위와 같이 세 가지 상태가 있다.

스레드의 상태를 코드로 확인하고 싶다면 Thread의 getState()를 호출하면 된다. enum 상수 값으로 스레드의 상태를 리턴한다. 각 값이 의미하는 바는 다음과 같다.

  • Thread.State.NEW: 스레드 객체가 생성
  • Thread.State.RUNNABLE : 실행 대기. 언제든 실행상태가 될 수 있음
  • Thread.State.WAITING : 다른 스레드가 통지할 때까지 기다리는 상태
  • Thread.State.TIMED_WAITING : 주어진 시간 동안 기다리는 상태
  • Thread.State.BLOCKED: 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
  • Thread.State.TERMINATED: 실행을 마친 상태



스레드의 상태와 관련해서, 아래의 코드를 보자.

public class StatePrintThread extends Thread {

    private Thread target;
    private Thread target2;

    public StatePrintThread(Thread target, Thread target2){
        this.target = target;
        this.target2 = target2;
    }

    @Override
    public void run() {
        while(true){
            Thread.State state = target.getState();
            Thread.State state2 = target2.getState();

            System.out.println("target Thread Name: " +target.getName());
            System.out.println("target2 Thread Name: " +target2.getName());
            System.out.println("current Thread Name: " +Thread.currentThread().getName());

            System.out.println("thread status : " + state);
            System.out.println("thread2 status : " + state2);

            if(state == Thread.State.NEW){
                target.start();
            }

            if(state == Thread.State.TERMINATED){
                break;
            }

            try{
                Thread.sleep(500);
            } catch (Exception e){}
        }
    }
}
public class Target extends Thread {

    @Override
    public void run() {

        System.out.println("here we go");
        Thread thread = Thread.currentThread();
        thread.setName("target");
        System.out.println("thread name: " +thread.getName());
        System.out.println("thread group: " +this.getThreadGroup());
        for(int i = 0; i<100000000; i++){}

        try{
            Thread.sleep(1500);
        } catch (Exception e){}

        for(int i = 0; i<100000000; i++){}

    }
}
public class ThreadStatusApp {
    public static void main(String[] args) {
        StatePrintThread printThread = new StatePrintThread(new Target(), new TestThread());

        printThread.start();

    }
}
target Thread Name: Thread-0
target2 Thread Name: Thread-1
current Thread Name: Thread-2
thread status : NEW
thread2 status : NEW
here we go
thread name: target
thread group: java.lang.ThreadGroup[name=main,maxpri=10]
target Thread Name: target
target2 Thread Name: Thread-1
current Thread Name: Thread-2
thread status : TIMED_WAITING
thread2 status : NEW
target Thread Name: target
target2 Thread Name: Thread-1
current Thread Name: Thread-2
thread status : TIMED_WAITING
thread2 status : NEW
target Thread Name: target
target2 Thread Name: Thread-1
current Thread Name: Thread-2
thread status : TERMINATED
thread2 status : NEW

StatePrintThread가 while문을 돌리는 동안 Target 에서는 1억번 for 루핑, 1.5초 멈추었다가 다시 1억번 for 루핑이 일어난다. 처음 target 스레드가 만들어졌을 때에는 상태가 NEW 이고, 루핑을 통과하면서 일시정지 상태가 되어서 이를 StatePrintThread가 출력하고, 다시 target이 모든 루핑을 통과하여 run() 실행이 끝나 TERMINATED 상태가 되고, 이를 printThread가 출력하고 while 루핑에서 break하고 있다.


Runnable.run()vs. Thread.start()

두 메서드의 가장 큰 차이점은 run()은 메서드가 호출되고 있는 현재 스레드에서 코드를 수행한다는 점이다. 반면 start()가 호출되면 새 스레드가 생기고 그 스레드에서 run()이 수행된다. start() 같은 경우 해당 스레드가 실행되고 있는지 상태를 체크한 뒤에 스레드를 생성하기 때문에 같은 스레드에서 두 번 start()를 호출하면 IllegalThreadStateException()이 발생한다. 반면 run()은 코드만 수행하기 때문에 여러 번 호출해도 상관은 없지만, 새로운 스레드가 생성되지 않기 때문에 run()을 직접 사용할 이유가 없다.




reference:

「이것이 자바다」, 신용권

profile
chop chop. mish mash. 재밌게 개발하고 있습니다.

0개의 댓글