008 Thread Start

강의 메모 - 스레드 실행 및 종료 #

개요 #

자바 스레드는 OS 스케줄러에 의해 실행 순서가 결정되며, 스레드 실행 시점을 JVM에서 제어할 수 없다. kernel이 제어한다. 새로운 스레드는 현재 스레드와 독립적으로 실행되고, 최대 한번 시작할 수 있고, 스레드가 종료된 이후에는 다시 시작할 수 없다.

스레드 실행 #

  • start() : 스레드를 실행시키는 메서드로 시스템 콜을 통해서 커널에 커널 스레드 생성을 요청한다.

    • start() 호출 후 System Call -> (execute) -> Kernel -> (create) -> Kernel Thread
  • 순서

      1. 메인 스레드가 새로운 스레드를 생성한다.
      1. 메인 스레드가 start() 메서드를 호출해서 스레드 실행을 시작한다.
      1. 내부적으로 네이티브 메서드인 start0()을 호출해서 커널에게 커널 스레드를 생성하도록 시스템 콜을 요청한다.
      1. 커널 스레드가 생성되고 자바 스레드와 커널스레드가 1:1 매핑이 이루어진다.
      1. 커널 스레드는 OS 스케줄러로부터 CPU 할당을 받기까지 실행대기 상태에 있다.
      1. 커널 스레드가 스케줄러에 의해 실행상태가 되면 JVM에서 매핑된 자바 스레드의 run() 메서드를 호출한다.

유저 영역의 JVM이 하나의 프로세스 Kernel 스레드 제어/정보 관리하는 TCB 존재 JVM(프로세스)를 관리하는 PCB 존재 img.png

스레드 실행 #

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() + ": 스레드 실행 중...");
        }
    }
}

img_1.png run() 메서드 스택만 1개 생성될 뿐이다.

img_2.png 쓰레드가 새로 생성되어 Stack 안에서 JVM이 run()을 호출한다.

스레드 스택(stack) #

  • 스레드가 생성되면 해당 스레드를 위한 스택(stack)이 같이 만들어진다.
  • 스택은 각 스레드마다 독립적으로 할당되어 작동하기 때문에 스레드간 접근하거나 공유할 수 없고, 이는 스레드로부터 안전하다 할 수 있다.
  • 스택은 OS에 따라 크기가 주어지고 주어진 크기를 넘기게되면 java.lang.StackOverFlowError가 발생하게된다.

스택의 구성 정보 #

  • 스택에 대한 메모리 접근은 Push & Pop에 의한 후입선출(LIFO; Last In First Out) 순서로 이루어지며 스택은 프레임(Frame)으로 구성되어있다.
  • 프레임은 새 메서드를 호출할 때마다 로컬 변수(지역변수, 파라미터) 및 객체 참조 변수와 함께 스택의 맨 위에 생성(push)되고 메서드 실행이 완료되면 해당 스택 프레임이 제거(pop)되고 흐름이 호출한 메서드로 돌아가며 공간이 다음 메서드에 사용 가능해진다.

img_3.png

스택 메모리 상태 관리 #

  • 스택 내부의 변수는 변수를 생성한 메서드가 실행되는 동안에만 존재한다.
  • 스택 메모리에 대한 엑세스는 Heap 메모리와 비교할때 빠르다.

스레드 종료 #

  • 스레드는 run() 메서드의 코드가 모두 실행되면 자동으로 종료한다.
  • 스레드는 예외가 발생할 경우 종료된다. 이는 다른 스레드에 영향을 미치지 않는다.
  • 어플리케이션은 싱글스레드인 경우와 멀티스레드인 경우 종료 기준이 다르다.

싱글스레드 #

  • 싱글스레드는 사용자 스레드(user thread)가 없는 기본 main thread만 있는 상태이다.
  • main thread만 종료되면 애플리케이션은 종료된다. img_4.png
/**
 * 싱글 스레드
 * 모든 작업이 끝나면 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의 스케줄링에 의해 결정되므로 매번 다르게 나올 수 있다. img_5.png
/**
 * 멀티 스레드
 * 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);
        }
    }
}