강의 메모 - 스레드 실행 및 종료 #
개요 #
자바 스레드는 OS 스케줄러에 의해 실행 순서가 결정되며, 스레드 실행 시점을 JVM에서 제어할 수 없다. kernel이 제어한다. 새로운 스레드는 현재 스레드와 독립적으로 실행되고, 최대 한번 시작할 수 있고, 스레드가 종료된 이후에는 다시 시작할 수 없다.
스레드 실행 #
-
start() : 스레드를 실행시키는 메서드로 시스템 콜을 통해서 커널에 커널 스레드 생성을 요청한다.
- start() 호출 후 System Call -> (execute) -> Kernel -> (create) -> Kernel Thread
-
순서
-
- 메인 스레드가 새로운 스레드를 생성한다.
-
- 메인 스레드가 start() 메서드를 호출해서 스레드 실행을 시작한다.
-
- 내부적으로 네이티브 메서드인 start0()을 호출해서 커널에게 커널 스레드를 생성하도록 시스템 콜을 요청한다.
-
- 커널 스레드가 생성되고 자바 스레드와 커널스레드가 1:1 매핑이 이루어진다.
-
- 커널 스레드는 OS 스케줄러로부터 CPU 할당을 받기까지 실행대기 상태에 있다.
-
- 커널 스레드가 스케줄러에 의해 실행상태가 되면 JVM에서 매핑된 자바 스레드의 run() 메서드를 호출한다.
-
유저 영역의 JVM이 하나의 프로세스 Kernel 스레드 제어/정보 관리하는 TCB 존재 JVM(프로세스)를 관리하는 PCB 존재
스레드 실행 #
run()
- 스레드가 실행이 되면 해당 스레드에 의해 자동으로 호출되는 메서드다.
- Thread의 run()이 자동 호출되고 여기서 Runnable 구현체가 존재할 경우 Runnable의 run()을 실행하게된다.
- public static void main(String[] args)가 메인 스레드에 의해 자동으로 호출되는 것과 비슷한 원리이다.
주의할점
- start()가 아닌 run()을 직접 호출하면 새로운 스레드가 생성되지 않고, 직접 호출한 스레드의 실행 스택에서 run)이 실행될 뿐이다.
public class ThreadStartRunExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " :스레드 실행중..");
}
});
thread.start();
// Thread.java > run () : JVM이 실행해줌
// Thread.java > start() > start0() (native 메서드) 호출
// 그 다음 우리가 정의한 run() 메서드가 호출된다.
// thread.run();
// run() 즉시 호출하면, 바로 쓰레드의 run()을 호출하게된다. (thread.java > run())
// 위 start()였을때의 차이점은 쓰레드 자체가 생기지 않은 것
// myRunnable.run(); // main 쓰레드에서 실행됨
// 출력 이후 쓰레드 run 로직의 출력 수행
// 쓰레드 추가할때마다 쓰레드 수행 순서는 다름 (독립적)
//
System.out.println("main 쓰레드 종료");
}
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": 스레드 실행 중...");
}
}
}
run() 메서드 스택만 1개 생성될 뿐이다.
쓰레드가 새로 생성되어 Stack 안에서 JVM이 run()을 호출한다.
스레드 스택(stack) #
- 스레드가 생성되면 해당 스레드를 위한 스택(stack)이 같이 만들어진다.
- 스택은 각 스레드마다 독립적으로 할당되어 작동하기 때문에 스레드간 접근하거나 공유할 수 없고, 이는 스레드로부터 안전하다 할 수 있다.
- 스택은 OS에 따라 크기가 주어지고 주어진 크기를 넘기게되면 java.lang.StackOverFlowError가 발생하게된다.
스택의 구성 정보 #
- 스택에 대한 메모리 접근은 Push & Pop에 의한 후입선출(LIFO; Last In First Out) 순서로 이루어지며 스택은 프레임(Frame)으로 구성되어있다.
- 프레임은 새 메서드를 호출할 때마다 로컬 변수(지역변수, 파라미터) 및 객체 참조 변수와 함께 스택의 맨 위에 생성(push)되고 메서드 실행이 완료되면 해당 스택 프레임이 제거(pop)되고 흐름이 호출한 메서드로 돌아가며 공간이 다음 메서드에 사용 가능해진다.
스택 메모리 상태 관리 #
- 스택 내부의 변수는 변수를 생성한 메서드가 실행되는 동안에만 존재한다.
- 스택 메모리에 대한 엑세스는 Heap 메모리와 비교할때 빠르다.
스레드 종료 #
- 스레드는 run() 메서드의 코드가 모두 실행되면 자동으로 종료한다.
- 스레드는 예외가 발생할 경우 종료된다. 이는 다른 스레드에 영향을 미치지 않는다.
- 어플리케이션은 싱글스레드인 경우와 멀티스레드인 경우 종료 기준이 다르다.
싱글스레드 #
- 싱글스레드는 사용자 스레드(user thread)가 없는 기본 main thread만 있는 상태이다.
- main thread만 종료되면 애플리케이션은 종료된다.
/**
* 싱글 스레드
* 모든 작업이 끝나면 main 스레드가 종료됨
*/
public class SingleThreadAppTerminatedExample {
public static void main(String[] args) {
int sum = 0;
for (int i = 0; i <1000; i++) {
sum += i;
}
System.out.println("sum : " + sum);
System.out.println("메인 스레드 종료");
}
}
멀티스레드 #
- 멀티스레드인 경우 JVM에서 실행하고있는 모든 스레드가 종료되어야 어플리케이션이 종료된다.
- 동일한 코드를 실행하는 각 스레드의 종료 시점은 처리 시간 및 OS의 스케줄링에 의해 결정되므로 매번 다르게 나올 수 있다.
/**
* 멀티 스레드
* main 스레드가 종료된다해서 종료되는 것이 아님
* main 스레드는 가장 먼저 종료됨
* 그 이후, 각 쓰레드가 모두 수행되고, 모두 종료가 되어야 애플리케이션이 종료된다.
* 멀티 스레드의 경우 모든 쓰레드가 모두 종료되어야한다.
* 쓰레드 중 단 하나라도 종료가 안되면 애플리케이션이 종료가 안된다.
*/
public class MultiThreadAppTerminatedExample {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new ThreadStackExample.MyRunnable(i));
thread.start();
}
System.out.println("메인 스레드 종료");
}
static class MyRunnable implements Runnable{
private final int threadId;
public MyRunnable(int threadId) {
this.threadId = threadId;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": 스레드 실행 중...");
firstMethod(threadId);
}
private void firstMethod(int threadId) {
int localValue = threadId + 100;
secondMethod(localValue);
}
private void secondMethod(int localValue) {
String objectReference = threadId + ": Hello World";
System.out.println(Thread.currentThread().getName() + " : 스레드 ID : " + threadId + ", Value:" + localValue);
}
}
}