근래에 프로젝트에서 트랜잭션 관리에 대해서 찾아보다가
Thread에 대해서 깊이 알지 못한채로 사용하고 있는 것 같아서 찾아보기 시작했다
강의(김영한의 실전 자바 - 고급1편)과 서적(Java의 정석2)을 통해 학습한 내용을 정리해보고자한다.
평소에 생각없이 말했던 용어 정리부터 해보자면
Process
실행중인 프로그램의 인스턴스
프로그램이 실행되기 전에는 하나의 코드 파일 -> 프로그램이 실행되면 Process
각각의 프로세스는 RAM에서 별도의 메모리 공간을 할당 -> 다른 프로세스에 영향을 주지 않음
- Code : 실행 프로그램의 코드가 저장되는 영역
- Data : 전역 변수 / 정적 변수 저장 영역
- Heap : 동적으로 할당되는 메모리의 영역
- Stack : 메서드 호출시 생성되는 지역변수 및 반환값의 참조 주소 저장
Multi Tasking? Multi Processing?
Multi Tasking
하나의 CPU Core가 여러개의 프로그램 코드를 번갈아(약 10ms) 실행
유져의 입장에서는 동시 실행되는 것 처럼으로 인지 -> 시분할 기법
Multi Processing
두개 이상의 CPU Core가 여러 작업을 동시에 처리
실제 물리적/하드웨어적으로 동시에 처리
Thread
Process 내에서 실행되는 작업의 단위
컴퓨터 내에서도 프로그램(Process)를 동시에 여러개 돌려야 하는 것처럼, 프로그램 내에서도 작업(Thread)를 여러개 동시에 해야함
Process의 독립된 메모리 공간을 공유 But 각각의 Stack을 가지고 있음
- 장점 - Multi Threading
- 하나의 Process내에서 여러 쓰레드가 동시 작업을 진행하는 것 (동시에 처리되는 Thread의 개수는 CPU Core의 개수와 같음)
- 하나의 프로세스의 공유 자원을 사용하므로 효율적 사용 가능
- 프로세스가 기반 (메모리 등)을 닦아놓았기 때문에 생성하기 쉬우며 응답성이 높음
- 프로세스의 작업을 Thread 단위로 분리할 수 있어서, 코드가 간결
- ex. 내가 사용 중인 메신저 프로그램(실행중인 코드니 Process)에서 파일을 다운 받으며(Thread 1) 텍스트 입력(Thread 2)을 할 수 있음
Thread 스케줄링
Thread를 어떠한 순서로 기준에 따라 실행하는 행위
CPU Core가 여러개 있을 때, 어떤 쓰레드를 어떤 Core에 할당할지 관리
스케줄링 Queue에 Thread들을 담아놓고 CPU를 최대한 활용할 수 있는 다양한 우선순위와 최적화 기법을 사용하여 관리
위의 설명은 컴퓨터가 스케줄러를 사용해서 OS Native Thread를 관리하는 것이고
Java 코드로 만들어진 Thread는 JVM Thread Scheduler가 관리한다
결국 JVM도 하나의 프로세스고, 결국 OS가 CPU Core들을 관리하는 것일텐데
OS의 Scheduler와는 JVM Thread Scheduler는 어떤 구조로 엮여 있을지 궁금했다
Threading Model은 JVM구현마다 다르지만 대표적 JVM인 Hotspot에서는 같은 경우에는
https://openjdk.org/groups/hotspot/docs/RuntimeOverview.html#Thread%20Management%7Coutline
Threading Model
The basic threading model in Hotspot is a 1:1 mapping between Java threads (an instance of java.lang.Thread) and native operating system threads. The native thread is created when the Java thread is started, and is reclaimed once it terminates. The operating system is responsible for scheduling all threads and dispatching to any available CPU.
The relationship between Java thread priorities and operating system thread priorities is a complex one that varies across systems. These details are covered later.
JVM의 Thread Scheduler와 Native Thread와 1:1 매핑을 시킨다고 한다
java.lang.Thread가 start()되면 JVM은 아래의 두 Object 인스턴스를 만든다
JavaThread
- java.lang.Thread 인스턴스 참조 주소
- Thread 고유 raw int 번호
- OS Native Thread의 참조 주소
OSThread
- 쓰레드 상태를 추적하기 위한 운영체제 수준의 정보
- 실제 native thread 식별 위한 플랫폼별 특화된 handle
위의 두 인스턴스가 생성된 후에, system call을 통해 커널 모드로 전환되며, OS Native Thread가 시작된다
OS Native가 실행된 다음에, 다시 유져 모드로 전환되어 JVM에게 주도권이 넘어오며 JavaThread의 run() 메서드가 실행된다
Thread 작업의 종류
CPU-bound task
- CPU 연산이 많이 필요한 작업
- 계산, 데이터 처리, 알고리즘 실행 등
- CPU 코어 수 + 1개 개수의 Thread Pool을 설정
I/O-bound tasks
- 디스크, 네트워크, 파일 등을 사용하여 입출력 작업이 많이 필요한 작업
- I/O가 끝날때까지는 쓰레드는 CPU 사용하지 않고 대기 상태
- CPU 코어보다 많은 개수의 Thread Pool을 설정하여 CPU의 유휴시간 줄인다 하지만 너무 많은 개수의 Thread 생성 시, Context Switching 비용도 고려해야함
전반적인 Thread의 개념과 Thread 생성 시, JVM과 OS의 역할을 간단히 살펴보았다면
JVM내에서 Thread가 시작되는 과정에 대해서 좀 더 자세히 알아보고자 한다
JVM Runtime Data Area 구성
Heap 영역
- 객체 인스턴스와 배열이 저장되는 영역
- new()로 만들어진 모든 인스턴스는 heap에 저장되고 참조됨
- 더이상 참조되지 않는 객체를 치우는 Garbage Collector의 주무대
Method 영역
- 프로세스 실행 시 필요한 공통 데이터
- 클래스 정보 / static 변수 / 런타임에 필요한 상수
Stack 영역
- 쓰레드 당 하나 생성됨
- Stack Frame이 Stack에 쌓이며, 지역변수 / 중간 연산 결과 / 메서드 호출 정보 등을 포함한다
- 메서드를 호출할 때마다, 메서드의 정보를 담은 Frame이 생성되고, 메서드가 종료되면 frame이 pop됨
Thread 생성 과정
- java.lang.Thread의 start() 메서드 호출 -> 호출한 Client는 바로 본인의 다음 코드 읽음
- JVM은 쓰레드를 위한 별도의 Stack 공간 할당
- JVM은 Thread 초기 정보 세팅
- Thread와 연관된 Object의 run() 메서드의 Stack Frame 생성
- run() 메서드의 stack frame을 thread stack에 올리면서 run() 메서드 실행
그렇기 때문에 Thread 생성시, run() 메서드만 호출 시
쓰레드가 새로 생성되지 않고, 우리가 새로운 쓰레드에서 작업하려고 한 로직이 현재 실행 쓰레드에서 같이 실행된다
Thread 생성 1 - Thread 클래스 상속 받기
public class HelloThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "run()");
}
}
package thread.start;
public class HelloThreadMain {
public static void main(String[] args) {
helloThread.start();
}
}
Thread 클래스를 상속 받아, run()을 구현하면 되지만
Java는 단일 상속만 허용하므로, 다른 부모 클래스를 상속하고 있다면 사용 불가하다 -> OOP에서는 Critical한 문제!
Thread 생성 2 - Runnable 인터페이스 구현
package thread.start;
public class HelloRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " : run()");
}
}
package thread.start;
public class HelloRunnableMain {
public static void main(String[] args) {
HelloRunnable runnable = new HelloRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
Runnable 객체를 생성 후, Thread 에 전달해야하는 번거로움이 있으나,
인터페이스를 구현하므로, 다른 클래스 상속 받아도 문제 없으며
쓰레드와 실행할 작업을 다른 인스턴스로 분리할 수 있어서 OOP에 적합
그리고 여러 쓰레드가 Runnable 객체를 공유할 수 있어서 자원 관리를 효율적으로 할수 있다
다음에는 Thread의 상태 및 interrupt / yield로 인한 상태 변화 등을 자세하게 알아보고자 한다