반응형
Java Thread
- 자바 스레드는 JVM에서 User Thread를 생성할 때 시스템 콜을 통해서 커널에서 생성된 커널 스레드와 1:1로 매핑이 되어 최종적으로 커널에서 관리된다.
- JVM에서 스레드를 생성할 때마다 커널에서 자바 스레드와 대응하는 커널 스레드를 생성한다.
- 자바에서는 플랫폼 스레드로 정의되어 있다. 즉 OS 플랫폼에 따라 JVM이 사용자 스레드를 매핑하게 된다.
- 스레드 생성은 Thread 클래스를 상속하는 방법과 Runnable 인터페이스(함수형 인터페이스)를 구현하는 방법이 있다.
Thread 실행 및 종료
자바 스레드는 OS 스케줄러에 의해 실행 순서가 결정되며 스레드 실행 시점을 JVM에서 제어할 수 없다.
새로운 스레드는 현재 스레드와 독립적으로 실행되고 최대 한번 시작할 수 있고 스레드가 종료된 후에는 다시 시작할 수 없다.
start()
스레드를 실행시키는 메서드로 시스템 콜을 통해 커널에 커널 스레드 생성을 요청한다.
- 메인 스레드가 새로운 스레드를 생성한다.
- 메인 스레드가 start() 메서드를 호출해서 스레드 실행을 시작한다.
- 내부적으로 네이티브 메서드인 start0()를 호출해서 커널에게 커널 스레드를 생성하도록 시스템 콜을 요청한다.
- 커널 스레드가 생성되고 자바 스레드와 커널스레드가 1:1 매핑이 된다.
- 커널 스레드는 OS 스케줄러로부터 CPU 할당을 받기까지 실행대기 상태에 있다.
- 커널 스레드가 스케줄러에 의해 실행상태가 되면 JVM에서 매핑 된 자바 스레드의 run()을 호출한다.
run()
- 스레드가 실행이 되면 해당 스레드에 의해 자동으로 호출되는 메서드
- Thread의 run()이 자동 호출되고 만약 Runnable 구현체가 존재할 경우 Runnable의 run()을 실행하게 된다.
- public static void main(String[] args)가 메인 스레드에 의해 자동으로 호출되는 것과 비슷한 원리다.
- 만약 start()가 아닌 run() 메서드를 직접 호출하면 새로운 스레드가 생성되지 않고 직접 호출한 스레드의 실행 스택안에서 run()이 실행된다.
Thread Stack
- 스레드가 생성되면 해당 스레드를 위한 스택이 만들어진다.
- 스택은 각 스레드마다 독립적으로 할당되어 작동하기 때문에 스레드 간 접근하거나 공유할 수 있고 스레드 세이프하다.
- 스택은 OS에 따라 크기가 주어지고 주어진 크기를 넘기게 되면 java.lang.StackOverFlowError가 발생한다.
- 스택에 대한 메모리 접근은 Push & Pop (LIFO) 순서로 이루어지며 스택은 프레임으로 구성되어 있다.
- 프레임은 새 메서드를 호출할 때마다 로컬 변수(지역변수, 파라미터) 및 객체 참조 변수와 함께 스택의 맨 위에 생성(push)되고 메서드 실행이 완료되면 해당 스택 프레임이 제거(pop)되고 흐름이 호출한 메서드로 돌아가며 공간이 다음 메서드에 사용 가능해진다.
- 스택 내부의 변수는 변수를 생성한 메서드가 실행되는 동안에만 존재한다.
- 스택 메모리에 대한 액세스는 Heap 메모리와 비교할 때 빠르다.
스레드 종료
- 스레드는 run() 메서드의 코드가 모두 실행되면 스레드는 자동으로 종료한다.
- 스레드는 예외가 발생할 경우 종료되며 다른 스레드에 영향을 미치지 않는다.
- 애플리케이션은 싱글 스레드인 경우와 멀티 스레드인 경우 종료 기준이 다르다.
싱글 스레드 애플리케이션
- 싱글 스레드는 사용자 스레드가 없는 기본 main thread만 있는 상태다.
- main thread만 종료되면 애플리케이션이 종료된다.
멀티스레드 애플리케이션
- 멀티스레드인 경우 JVM에서 실행하고 있는 모든 스레드가 종료되어야 애플리케이션이 종료된다.
- 동일한 코드를 실행하는 각 스레드의 종료 시점은 처리 시간 및 OS의 스케줄링에 의해 결정되므로 매번 다르게 나올 수 있다.
- 데몬 스레드인 경우는 유저 스레드가 모두 종료되면 데몬 스레드는 강제로 종료한다.
스레드 생명주기와 상태
- 자바 스레드는 생성, 실행, 종료에 따른 상태를 가지고 있으며 JVM에서는 6가지의 스레드 상태가 존재한다. OS 스레드 상태 X
- 자바 스레드는 어떤 시점이든 6가지 상태 중 오직 하나의 상태를 가질 수 있다.
- 자바 스레드의 현재 상태를 가져오려면 Thread의 getState() 메서드를 사용해 가져올 수 있다
- Thread 클래스에는 스레드 상태에 대한 ENUM 상수를 정의하는 Thread State 클래스를 제공한다.
스레드 상태
- 객체 상태 (NEW) - 스레드 객체가 생성됨, 아직 시작되지 않은 스레드 상태
- 실행 대기 (RUNNABLE) - 실행 중이거나 실행 가능한 스레드 상태
- 일시 정지 (WAITING) - 대기 중인 스레드 상태로서 다른 스레드가 특정 작업을 수행하기를 기다림
- 일시 정지 (TIMED_WAITING) - 대기 시간이 지정된 스레드 상태로서 다른 스레드가 특정 작업을 수행하기를 기다림
- 일시 정지 (BLOCKED) - 모니터 락(lock)이 해제될 때까지 기다리며 차단된 스레드 상태
- 종료 (TERMINATED) - 실행이 완료된 스레드 상태
Runnbale은 실행 중, 실행 가능한 스레드 상태다.
아래 그림은 실행 중(Running)이라는 개념을 도입해 작성한 그림이다.
Thread 메서드
Thread 클래스의 기본 메서드 몇 가지만 살펴보겠다.
sleep
- sleep() 메서드는 지정된 시간 동안 현재 스레드의 실행을 일시 정지하고 대기 상태로 빠졌다가 시간이 지나면 실행대기 상태로 전환된다.
- 네이티브 메서드로 연결되며 시스템 콜을 통해 커널모드에서 수행 후 유저모드로 전환한다.
- 지정한 시간 동안 스레드를 수면 상태로 만든다.
- 스레드가 수면 중에 인터럽트 될 경우 InterruptedException을 발생시킨다.
- 다른 스레드는 잠자고 있는 스레드에게 인터럽트, 즉 중단(멈춤) 신호를 보낼 수 있다.
- InterruptedException 예외가 발생하면 스레드는 수면상태에서 깨어나고 실행 대기 상태로 전환되어 실행상태를 기다린다.
작동방식
- sleep()이 되면 OS 스케줄러는 현재 스레드를 지정된 시간 동안 대기 상태로 전환하고 다른 스레드 혹은 프로세스에게 CPU 를 사용하도록 한다.
- 대기 시간이 끝나면 스레드 상태는 바로 실행상태가 아닌 실행 대기 상태로 전환 되고 CPU 가 실행을 재개할 때 까지 기다린다.
- 실행 상태가 되면 스레드는 남은 지점부터 실행을 다시 시작한다
- 동기화 메서드 영역에서 수면 중인 스레드는 획득한 모니터나 락을 잃지 않고 계속 유지한다.
- sleep() 중인 스레드에게 인터럽트가 발생할 경우 현재 스레드는 대기에서 해제되고 실행상태로 전환되어 예외를 처리하게 된다
- 스레드의 수면 시간은 OS 스케줄러 및 시스템 기능에 따라 제한되기 때문에 정확성이 보장되지 않으며 시스템의 부하가 많고 적음에 따라 지정한 수면 시간과 차이가 날 수 있다
sleep(0)과 sleep(1)의 의미
sleep(millis) 메서드는 네이티브 메서드이기 때문에 sleep(millis) 을 실행하게 되면 시스템 콜을 호출하게 되어 유저모드에서 커널모드로 전환된다.
다른 스레드에게 명확하게 실행을 양보하기 위함이라면 sleep(0) 보다는 sleep(1) 을 사용해야 한다.
sleep(0)
- 스레드가 커널 모드로 전환 후 스케줄러는 현재 스레드와 동일한 우선순위(Priority)의 스레드가 있을 경우 실행대기상태 스레드에게 CPU 를 할당함으로 컨텍스트 스위칭이 발생한다
- 만약 우선순위가 동일한 실행대기 상태의 다른 스레드가 없으면 스케줄러는 현재 스레드에게 계속 CPU 를 할당해서 컨텍스트 스위칭이 없고 모드 전환만 일어난다
sleep(n)
- 스레드가 커널 모드로 전환 후 스케줄러는 조건에 상관없이 현재 스레드를 대기상태에 두고 다른 스레드에게 CPU 를 할당함으로 모든 전환과 함께 컨텍스트 스위칭이 발생한다
join
- join() 메서드는 한 스레드가 다른 스레드가 종료될 때까지 실행을 중지하고 대기상태에 들어갔다가 스레드가 종료되면 실행대기 상태로 전환된다.
- 스레드의 순서를 제어하거나 다른 스레드의 작업을 기다려야 하거나 순차적인 흐름을 구성하고자 할 때 사용할 수 있다.
- Object 클래스의 wait() 네이티브 메서드로 연결되며 시스템 콜을 통해 커널모드로 수행한다. 내부적으로 wait() & notify() 흐름을 가지고 제어한다.
작동 방식
- join() 을 실행하면 OS 스케줄러는 현재 스레드를 대기 상태로 전환하고 join() 을 수행중인 스레드에게 CPU 를 사용하도록 한다.
- join() 을 수행중인 스레드의 작업이 종료되면 현재 스레드는 실행 대기 상태로 전환 되고 CPU 가 실행을 재개할 때 까지 기다린다.
- 실행 상태가 되면 스레드는 남은 지점부터 실행을 다시 시작한다
- join() 을 수행중인 스레드가 여러 개일 경우 각 스레드의 작업이 종료될 때 까지 현재 스레드는 대기하고 종료 이후 실행을 재개하는 흐름을 반복한다.
- join() 을 수행중인 스레드에게 인터럽트가 발생할 경우 현재 스레드는 대기에서 해제되고 실행상태로 전환되어 예외를 처리하게 된다.
인터럽트
자바 스레드에서 interrupt()는 특정한 스레드에게 인터럽트 신호를 알려줌으로써 스레드의 실행을 중단하거나, 작업 취소, 강제 종료 등으로 사용할 수 있다.
interrupt()
- interrupt() 는 스레드에게 인터럽트가 발생했다는 신호를 보낸다.
- interrupt() 는 스레드가 현재 실행 흐름을 멈추고 인터럽트 이벤트를 먼저 처리하도록 시그널을 보낸다.
- 스레드는 인터럽트 상태로 알려진 interrupted 속성을 가지고 있으며 인터럽트 발생 여부를 확인할 수 있는 상태 값이다. 기본 값은 false
- 인터럽트된 스레드가 처리해야 하는 특별한 규칙이나 정해진 기준은 없으나 일반적으로 인터럽트 상태를 사용해서 스레드를 중지하거나, 작업을 취소하거나, 스레드를 종료하는 등의 기능을 구현할 수 있다.
- 한 스레드가 다른 스레드를 인터럽트 할 수 있고 자기 자신도 인터럽트할 수 있다.
- 횟수에 제한이 없으며 인터럽트 할 때마다 스레드의 인터럽트 상태를 true로 반환한다.
인터럽트 상태 확인 방법
- interrupted()
- 스레드의 인터럽트 상태를 반환
- 만약 현재 인터럽트 상태가 true인 경우 true를 반환, 인터럽트 상태를 false로 초기화하므로 인터럽트를 해제하는 역할
- 인터럽트를 해제하는 경우 다른 곳에서 스레드에 대한 인터럽트 상태를 체크하는 곳이 있따면 별도의 처리가 필요할 수 있다.
- 인터럽트를 강제로 해제했기 때문에 다시 인터럽트를 걸어서 인터럽트 상태를 유지할 수 있다.
- isInterrupted()
- 스레드의 인터럽트 상태를 반환
- 스레드의 인터럽트 상태를 변경하지 않고 유지
InterruptedException
- InterruptedException 은 interrupt() 메카니즘의 일부이며 대기나 차단 등 블록킹 상태에 있거나 블록킹 상태를 만나는 시점의 스레드에 인터럽트 할 때 발생하는 예외이다
- InterruptedException 이 발생하면 인터럽트 상태는 자동으로 초기화 된다. 즉 Thread.interrupted() 한 것과 같은 상태로 된다( interrupted = false)
- 다른 곳에서 인터럽트 상태를 참조하고 있다면 예외 구문에서 대상 스레드에 다시 interrupt() 해야 할 수도 있다
- InterruptedException 이 발생하는 케이스는 다음과 같다
- Thread.sleep(), Thread.join(), Object.wait()
- Future.get(), BlockingQueue.take()
Thread 활용
스레드 예외처리
- 기본적으로 스레드의 run()은 예외를 던질 수 없기 때문에 예외가 발생한 경우 run()안에서만 예외를 처리해야 한다.
- RuntimeException 타입의 예외가 발생할지라도 스레드 밖에서 예외를 캐치할 수 없고 사라진다.
- 스레드가 비정상적으로 종료되었거나 특정한 예외를 스레드 외부에서 캐치하기 위해서 자바에는 UncaughtExceptionHandler 인터페이스를 제공한다.
UncaughtExceptionHandler
- 캐치되지 않은 예외에 의해 Thread가 갑자기 종료했을 때에 호출되는 핸들러 인터페이스
- 어떤 원인으로 인해 스레드가 종료되었는지 대상 스레드와 예외를 파악할 수 있다.
- 예외가 발생하면 대상 스레드 t와 예외 e가 인자로 전달된다.
참고할 Thread API
- static void setDefaultUncaughtExceptionHandler - 모든 스레드에서 발생하는 uncaughtException을 처리하는 정적 메서드
- void setUncaughtExceptionHandler(UncaughtExceptionHandler ueh)
- 대상 스레드에서 발생하는 uncaughtException을 처리하는 인스턴스 메서드
- setDefaultUncaughtEx ceptionHandler보다 우선순위가 높다.
사용자 스레드와 데몬 스레드
- 자바에는 스레드가 크게 두 종류가 있는데, 사용자 스레드와 데몬 스레드다.
- 사용자 스레드는 사용자 스레드를 낳고 데몬 스레드는 데몬 스레드를 낳는다. 즉 자식 스레드는 부모 스레드의 상태를 상속 받는다.
- 자바 애플리케이션이 실행이 되면 JVM은 사용자 스레드인 메인 스레드와 나머지 데몬 스레드를 동시에 생성하고 시작한다.
메인 스레드
- 메인 스레드는 애플리케이션에서 가장 중요한 부분으로서 애플리케이션을 실행할 때마다 메인 스레드가 생성되어 실행된다.
- 메인 스레드는 애플리케이션을 실행하는 최초의 스레드이자 애플리케이션 실행을 완료하는 마지막 스레드의 역할을 한다.
- 메인 스레드에서 여러 하위 스레드를 추가로 시작할 수 있고 하위 스레드는 또 여러 하위 스레드를 시작할 수 있다.
- 메인 스레드가 사용자 스레드이기 때문에 하위 스레드는 모두 사용자 스레드가 된다.
사용자 스레드
- 사용자 스레드는 메인 스레드에서 직접 생성한 스레드를 의미한다.
- 사용자 스레드는 각각 독립적인 생명주기를 가지고 실행하게 되면 메인 스레드를 포함한 모든 사용자 스레드가 종료하게 되면 애플리케이션이 종료하게 된다.
- 사용자 스레드는 포그라운드에서 실행되는 높은 우선순위를 가지며, JVM은 사용자 스레드가 스스로 종료될 때까지 애플리케이션을 강제로 종료하지 않고 기다린다.
- 자바가 제공하는 스레드 풀인 ThreadPoolExecutor은 사용자 스레드를 생성한다.
데몬 스레드
- 데몬 스레드는 JVM에서 생성한 스레드이거나 직접 데몬 스레드로 생성한 경우를 말한다.
- 모든 사용자 스레드가 작업을 완료하면 데몬 스레드의 실행 여부에 관계 없이 JVM이 데몬 스레드를 강제로 종료하고 애플리케이션이 종료된다.
- 데몬 스레드의 생명주기는 사용자 스레드에 따라 다르며 낮은 우선순위를 가지고 백그라운드에서 실행된다.
- 데몬 스레드는 사용자 스레드를 보조 및 지원하는 성격을 가진 스레드로서 보통 사용자 작업을 방해하지 않으면서 백그라운드에서 자동으로 작동되는 기능을 가진 스레드다.
- 자바가 제공하는 스레드 풀인 ForkJoinPool은 데몬 스레드를 생성한다.
ThreadGroup
- 자바는 스레드 그룹(Thread Group)라는 객체를 통해서 여러 스레드를 그룹화하는 편리한 방법을 제공한다.
- ThreadGroup은 스레드 집합을 나타내며 스레드 그룹에는 다른 스레드 그룹도 포함될 수 있고 그룹 내의 모든 스레드는 한 번에 종료하거나 중단할 수 있다.
- 스레드는 반드시 하나의 스레드 그룹에 포함되어야 하며 명시적으로 스레드 그룹에 포함시키지 않으면 자신을 생성한 스레드가 속해있는 그룹에 포함되어 진다
- 일반적으로 사용자가 main 스레드에서 생성하는 모든 스레드는 기본적으로 main 스레드 그룹에 속하게 된다.
JVM의 스레드 그룹 생성 과정
- JVM이 실행되면 최상위 스레드 그룹인 system 스레드 그룹이 생성된다.
- JVM 운영에 필요한 데몬 스레드들을 생성해서 system 스레드 그룹에 포함시킨다.
- system 스레드 그룹의 하위 스레드 그룹인 main 스레드 그룹을 만들고 main 스레드를 그룹에 포함시킨다.
참고자료
https://techblog.woowahan.com/15398/
https://crunchify.com/java-thread-state-introduction-with-example/
반응형
댓글