프로그래밍 언어/JAVA

스레드 생명 주기와 스케줄링

· 코딩마이데이

스레드 상태

스레드는 JVM에 있어 생명체와 같습니다. 스레드는 태어나고, 실행하고, 잠자고, 대기하고, 종료하는 등 생명 주기(life cycle)를 가집니다. 그리고 생명주기 동안 여러 상태의 변이를 거칩니다. 스레드상태는 다음과 같이 총 6가지이며 JVM에 의해 관리됩니다.

New

스레드가 생성되었지만 아직 실행할 준비가 되지 않은 상태입니다. start() 메소드가 호출되면 RUNNABLE 상태가 관리됩니다.

RUNNABLE

스레드가 현재 실행되고 있거나, 실행 준비되어 스케줄링을 기다리는 상태입니다.

스레드의 상태 변이도

 

TIMED_WAITING

스레드가 sleep(long n)을 호출하여 n밀리초 동안 점을 자는 상태입니다.

BLOCK

스레드가 I/O 작업을 실행하여 I/O 작업의 완료를 기다리면서 멈춘(blocked) 상태입니다. 스레드가 I/O 작업을 실행하면 JVM이 자동으로 현재 스레드를 BLOCK 상태로 만들고 다른 스레드를 스케줄링합니다.

WAITING

스레드가 어떤 객체 a에 대해  a.wait()을 호출하여, 다른 스레드가 a.notify(), a.notifyAll()을 불러줄 때까지 무한정 기다리는 상태입니다.

TERMINATE

스레드가 종료한 상태입니다. 더 이상 다른 상태로 변이할 수 없습니다.

 

스레드의 일생

new Thread()에 의해 스레드 객체가 생성되면 JVM은 생성된 스레드 정보를 관리합니다. 생성된 스레드는 NEW 상태입니다. NEW 상태의 스레드는 스케줄링되지 않기 때문에 실행될 수 없는 상태입니다. 스케줄링이란 JVM이 RUNNABLE(준비) 상태인 스레드 중에서 하나를 선택하여 실행시키는 과정입니다.

 

Thread 클래스의 start() 메소드가 호출되면, 스레드는 비로소 실행될 수 있는 RUNNABLE(준비) 상태가 됩니다. JVM은 RUNNABLE(준비)상태에 있는 스레드 중에서 하나를 선택하고 실행시킵니다. 처음으로 스케줄링되는 스레드는 run() 메소드의 첫 라인부터 실행을 시작합니다. JVM은 스케줄링시 우선순위(priority)가 높은 스레드를 우선적으로 선택합니다. 만일 우선순위가 동일한 스레드가 여러 개 있으면 라운드 로빈(round robin), 즉 돌아가면서 선택합니다.

 

실행중인 스레드가 일시 중단되는 경우는 여러 경우가 있는데, 우선 실행 중인 스레드가 I/O(입출력) 작업을 하게 되면 JVM에 의해 즉각 중지되며 BLOCK 상태가 됩니다. BLOCK 상태의 스레드는 I/O 작업이 완료될 때까지 스케줄링되지 않고 대기합니다. I/O 작업이 완료되면 스레드는 자동으로 RUNNABLE(준비) 상태가 됩니다. 화면 출력이나 키보드 입력, 프린터 출력, 파일 입출력, 네트워크 데이터 송수신 등 모든 종류의 I/O 작업시 BLOCK 상태가 됩니다.

 

실행 중인 스레드가 만약 yield()를 호출하면 다른 스레드가 스케줄링될 수 있도록 양보하겠다는 뜻으로, JVM은 현재 실행 중인 스레드를 즉각 RUNNABLE(준비) 상태로 변경하고 다시 스케줄링을 실시합니다. 만약 다른 높은 순위의 스레드가 없거나 동일한 우선순위의 다른 스레드가 없으면 이 스레드가 다시 스케줄링됩니다. 

 

실행 중인 스레드가 sleep(millis)을 호출하면 millis 밀리초 시간만큼 잠을 자게 되므로, JVM은 이 스레드를 TIMED_WAITING 상태로 변경하고 스케줄링을 시작합니다 이 스레드는 millis 밀리초 후에 깨어나 RUNNABLE(준비) 상태가 됩니다. 

 

실행 중인 스레드종료하면 TERMINATED 상태가 되며, JVM은 다른 스레드의 스케줄링을 시작합니다. TERMINATED 상태의 스레드는 더이상 RUNNABLE 상태로 돌아올 수 없기 때문에 다시 실행될 수 없습니다.

 

실행 중인 스레드가 어떤 객체 a의 wait() 메소드를 호출하여 다른 스레드로부터 깨워지기를 기다리는 경우입니다. 이때 객체 a를 동기화 객체라고 부릅니다. 모든 객체는 java.lang.Object를 상속받으며 wait() 메소드는 java.lang.Object 클래스의 멤버이므로 모든 객체가 동기화 객체가 될 수 있습니다. 첫 번째 스레드가 객체 a의 a.wait()을 호출하여 기다리고 있을 때, 다른 스레드가 a.notify()a.notifyAll() 메소드를 호출하게 되면 첫 번째 스레드는 깨어나서 RUNNABLE(준비) 상태로 돌아가게 됩니다. notify()나 notifyAll() 메소드 역시 java.lang.Object 클래스의 멤버입니다.

 

스레드 우선순위와 스케줄링

JVM은 철저히 우선순위(priority)를 기반으로 스레드를 스케줄링합니다. 가장 높은 우선순위의 스레드를 먼저 실행시킨다. 동일한 우선순위인 경우에는 돌아가면서 실행시킨다. 자바 스레드의 우선순위 체계는 다음과 같습니다.

 

최댓값(MAX_PRIORITY) = 10

최솟값(MIN_PRIORITY) = 1

보통값(NORMAL_PRIORITY) =5

 

자바 응용프로그램이 실행될 때 처음으로 생성되는 main 스레드보통 값(5)의 우선순위로 태어나며, 자식 스레드는 생성될 때 부모 스레드의 우선순위 값을 물려받기 때문에 main 스레드의 모든 자식 스레드는 보통 값(5)의 우선순위를 가지고 탄생됩니다. 하지만, 다음 메서드를 이용하면 우선순위 값을 바꿀 수 있습니다.

void setPriority(int newPriority) //newPriority로 스레드의 우선순위 값 변경

 

main()을 실행하는 main 스레드

JVM은 자바 응용프로그램을 실행하기 직전, 사용자 스레드를 하나 만들고, 이 스레드로 하여금 main()메소드를 실행하도록 합니다. 이 스레드가 바로 메인 스레드(main 스레드)이고 실행 시작 주소는 main()메소드의 첫 코드가 됩니다. 자바 응용프로그램의 main() 메소드가 실행되는 순간 2개의 스레드가 존재하는 셈입니다. 하나의 main 스레드이고 다른 하나는 JVM 내에 자동으로 생성된 가비지 컬렉션 스레드입니다.

 

main 스레드의 정보 출력

public class ThreadMainEx {
    public static void main(String[] args) {
        long id = Thread.currentThread().getId(); // 스레드 ID 얻기
        String name = Thread.currentThread().getName(); // 스레드 이름 얻기
        int priority = Thread.currentThread().getPriority(); // 스레드 우선순위 값 얻기

        Thread.State s = Thread.currentThread().getState(); // 스레드 상태 값 얻기

        System.out.println("현재 스레드 이름 = " + name);
        System.out.println("현재 스레드 ID = " + id);
        System.out.println("현재 스레드 우선순위 값 = " + priority);
        System.out.println("현재 스레드 상태 = " + s);
    }
}

 

[실행결과]

현재 스레드 이름 = main
현재 스레드 ID = 1
현재 스레드 우선순위 값 = 5
현재 스레드 상태 = RUNNABLE