-
스레드 생명 주기와 스케줄링
스레드 상태
: 스레드는 JVM에 있어 생명체와 같다. 스레드는 태어나고, 실행하고, 잠자고, 대기하고, 종료하는 등 생명주기(life cycle)를 가진다.
스레드의 상태 변이도 그리고 생명 주기 동안 위와 같은 여러 상태의 변이를 거친다. 스레드의 상태는 다음과 같이 총 6가지이며 JVM에 의해 관리된다.
1) NEW
: 스레드가 생성되었지만 아직 실행할 준비가 되지 않은 상태이다. start() 메소드가 호출되면 RUNNABLE 상태가 된다.
2) RUNNABLE
: 스레드가 현재 실행되고 있거나, 실행 준비되어 스케줄링을 기다리는 상태이다.
3) TIMED_WAITING
: 스레드가 sleep(long n)을 호출하여 n밀리초 동안 잠을 자는 상태이다.
4) BLOCK
: 스레드가 I/O 작업을 실행하여 I/O 작업의 완료를 기다리면서 멈춘(blocked) 상태이다. 스레드가 I/O 작업을 실행하면 JVM이 자동으로 현재 스레드를 BLOCK 상태로 만들고 다른 스레드를 스케줄링한다
5) WAITING
: 스레드가 어떤 객체 a에 대해 a.wait()을 호출하여, 다른 스레드가 a.notify(), a.notifyAll()을 불러줄 때까지 무한정 기다리는 상태이다.
6) TERMINATED
: 스레드가 종료한 상태이다. 더이상 다른 상태로 변이할 수 없다
스레드의 일생
스레드의 일생에 대해 자세히 알아보도록 하자
new Thread()
: new Thread()에 의해 스레드 객체가 생성되면 JVM은 생성된 스레드 정보를 관리한다. 생성된 스레드는 NEW 상태이다. NEW 상태의 스레드는 스케줄링되지 않기 때문에 실행될 수 없는 상태이다. 스케줄링이란 JVM이 RUNNABLE(준비) 상태인 스레드 중에서 하나를 선택하여 실행시키는 과정이다.
start()
: Thread 클래스의 start() 메소드가 호출되면, 스레드는 비로소 실행될 수 있는 RUNNABLE(준비) 상태가 된다. JVM은 RUNNABLE(준비)상태에 있는 스레드 중에서 하나를 선택하고 실행시킨다. 처음으로 스케줄링되는 스레드는 run() 메소드의 첫 라인부터 실행을 시작한다. JVM은 스케줄링시 우선순위(priority)가 높은 스레드를 우선적으로 선택한다. 만일 우선순위가 동일한 스레드가 여러 개 있으면 라운드 로빈(round robin), 즉 돌아가면서 선택한다.
I/O(입출력) 작업
: 실행중인 스레드가 일시 중단되는 경우는 여러 경우가 있는데, 우선 실행 중인 스레드가 I/O(입출력) 작업을 하게 되면 JVM에 의해 즉각 중지되며 BLOCK 상태가 된다. BLOCK 상태의 스레드는 I/O 작업이 완료될 때까지 스케줄링되지 않고 대기한다. I/O 작업이 완료되면 스레드는 자동으로 RUNNABLE(준비) 상태가 된다. 화면 출력이나 키보드 입력, 프린터 출력, 파일 입출력, 네트워크 데이터 송수신 등 모든 종류의 I/O 작업시 BLOCK 상태가 된다.
yield()
: 실행 중인 스레드가 만약 yield()를 호출하면 다른 스레드가 스케줄링될 수 있도록 양보하겠다는 뜻으로, JVM은 현재 실행 중인 스레드를 즉각 RUNNABLE(준비) 상태로 변경하고 다시 스케줄링을 실시한다. 만약 다른 높은 순위의 스레드가 없거나 동일한 우선순위의 다른 스레드가 없으면 이 스레드가 다시 스케줄링된다.
sleep(millis)
: 실행 중인 스레드가 sleep(millis)을 호출하면 millis 밀리초 시간만큼 잠을 자게 되므로, JVM은 이 스레드를 TIMED_WAITING 상태로 변경하고 스케줄링을 시작한다. 이 스레드는 millis 밀리초 후에 깨어나 RUNNABLE(준비) 상태가 된다.
스레드 종료
: 실행 중인 스레드가 종료하면 TERMINATED 상태가 되며, JVM은 다른 스레드의 스케줄링을 시작한다. TERMINATED 상태의 스레드는 더이상 RUNNABLE 상태로 돌아올 수 없기 때문에 다시 실행될 수 없다.
a.wait() / a.notify() / a.notifyAll()
: 실행 중인 스레드가 어떤 객체 a의 wait() 메소드를 호출하여 다른 스레드로부터 깨워지기를 기다리는 경우이다. 이때 객체 a를 동기화 객체라고 부른다. 모든 객체는 java.lang.Object를 상속받으며 wait() 메소드는 java.lang.Object 클래스의 멤버이므로 모든 객체가 동기화 객체가 될 수 있다. 첫 번째 스레드가 객체 a의 a.wait()을 호출하여 기다리고 있을 때, 다른 스레드가 a.notify()나 a.notifyAll() 메소드를 호출하게 되면 첫 번째 스레드는 깨어나서 RUNNABLE(준비) 상태로 돌아가게 된다. notify()나 notifyAll() 메소드 역시 java.lang.Object 클래스의 멤버이다. wait-notify, wait-notifyAll의 동기화 기법은 추후에 다룬다.
스레드 우선순위와 스케줄링
: JVM은 철저히 우선순위(priority)를 기반으로 스레드를 스케줄링한다. 가장 높은 우선순위의 스레드를 먼저 실행시킨다. 동일한 우선순위인 경우에는 돌아가면서 실행시킨다. 자바 스레드의 우선순위 체계는 다음과 같다
최댓값(MAX_PRIORITY) = 10
최솟값(MIN_PRIORITY) = 1
보통값(NORMAL_PRIORITY) =5
자바 응용프로그램이 실행될 때 처음으로 생성되는 main 스레드는 보통 값(5)의 우선순위로 태어나며, 자식 스레드는 생성될 때 부모 스레드의 우선순위 값을 물려받기 때문에 main 스레드의 모든 자식 스레드는 보통 값(5)의 우선순위를 가지고 탄생된다. 하지만, 다음 메서드를 이용하면 우선순위 값을 바꿀 수 있다.
void setPriority(int newPriority) //newPriority로 스레드의 우선순위 값 변경
참고) 원래 자바 스레드는 비선점(nonpreemptive) 스케줄링을 한다. 그러므로 원칙적으로 한 번 실행된 스레드가 스스로 양보하거나 종료되는 경우가 아니라면 다른 스레드는 실행될 기회를 가질 수 없다. 따라서 개발자는 스레드 실행 도중 다른 스레드에게 양보하도록 yield() 메소드를 호출하여야 하는 것이 원칙이다. 그러나 JVM이 윈도우처럼 멀티스레딩을 지원하는 운영체제상에서 실행되는 경우, JVM은 운영체제의 스레드를 자바 스레드에 매핑시켜 선점(preemptive) 스케줄링이 가능하게 한다. 이런 경우 자바 스레드는 운영체제의 시분할 체계에 의해 자동으로 다른 스레드에게 실행을 양보당하기 때문에 굳이 yield()메소드를 호출할 필요가 없다.
main()을 실행하는 main 스레드
: JVM은 자바 응용프로그램을 실행하기 직전, 사용자 스레드를 하나 만들고, 이 스레드로 하여금 main()메소드를 실행하도록 한다. 이 스레드가 바로 메인 스레드(main 스레드)이고 실행 시작 주소는 main()메소드의 첫 코드가 된다. 자바 응용프로그램의 main() 메소드가 실행되는 순간 2개의 스레드가 존재하는 셈이다. 하나의 main 스레드이고 다른 하나는 JVM 내에 자동으로 생성된 가비지 컬렉션 스레드이다.
실행 결과값 위의 코드는 main()을 실행하는 main 스레드를 확인해보기 위한 예제이다. 이 예제는 Thread의 static 타입의 currentThread() 메소드를 호출하여 현재 실행되고 있는 스레드 정보를 담은 Thread 객체를 얻어내고, 스레드의 이름, id, 우선순위, 스레드의 상태를 알아내는 코드 샘플을 보여준다.
'Programming > JAVA' 카테고리의 다른 글
[JAVA/자바] 스레드 동기화(Thread Synchronization) (0) 2023.06.07 [JAVA/자바] 스레드 종료 (0) 2023.06.07 [JAVA/자바] Runnable인터페이스로 스레드 만들기 (1) 2023.06.06 [JAVA/자바] Thread 클래스를 상속받아 스레드 만들기 (0) 2023.06.06 [JAVA/자바] 자바의 멀티스레딩 (1) 2023.06.06 댓글