Java 언어 설계자인 Brian Goetz 분이 IBM developer 사이트에서 작성하신 Java Thread 의 튜토리얼 글을 바탕으로 정리한 글입니다.
이 분은 Java의 Lambda Expressions 의 기술적 사양(Specification) 리드 역할을 담당한 경험 등 Java 언어에 많은 관여를 하신 개발자 분입니다.

https://inside.java/u/BrianGoetz/
ㅤ

대부분의 OS (Operating System) 은 프로세스 (processes) 를 지원한다.
Process 는 특정 계층에서 동떨어져서 독립적으로 실행되는 각 프로그램이다.
원문에서 특정 계층 (some degree) 으로 표현한 이유는 아마 구체적인 내용으로 풀어쓰게 되면 길어질 것이라 간단히 설명하신 것 같습니다.
정확히 풀어쓰면, 프로세스는 독립적인 메모리 공간을 가지고 격리되어 실행되는 각 프로그램입니다.
Java 는 주요 프로그래밍 언어중 첫 번째로 언어 자체만으로 Thread 기법을 명시적으로 포함한 언어다.
Thread 는 Process 와 유사하게 다음과 같은 공통점이 있다.
1. 독립적이다.
2. 고유의 Program Counter (PC) 가 존재한다.
3. 고유의 Local Variables 를 갖는다.
- Program Counter (PC)
- 다음에 실행될 명령어의 메모리 주소를 담고 있는 CPU 레지스터
- CPU가 순차적인 프로그램 실행 흐름을 추적하고 관리
- Local Variables
- 특정 내부 레벨에서만 사용 가능한 지역 변수들
- 해당 레벨 외부에서는 해당 변수에 접근 및 사용이 불가능
Thread 는 Process 와 다음과 같은 차이점이 있다.

하나의 Process 는 여러 Thread를 지원할 수 있다.
Threads 는 Parallelism (simultaneously) 와 Concurrency 를 지원한다.
- Parallelism: 병렬성 - 여러 리소스(코어)에서 여러 작업을 빠르게 처리하기 위해 동시간대에 실행
- Concurrency: 동시성 - 한정된 리소스(코어)에서 여러 작업을 효율적으로 처리하기 위해 전환하며(scheduling) 실행
- Concurrency는 Multi-threading과 Single-threading 기반 Asynchronous programming(non-blocking I/O) 등 다양한 방식으로 구현 가능
하나의 Process 내부의 여러 Thread 는 동일한 메모리 주소 공간을 공유한다. 즉, 해당 여러 Thread는 아래의 작업이 가능하다.
장점으로 단일 Process 내부의 여러 Thread 가 동일한 정보에 접근할 수 있다.
주의해야 할 점은 각 Thread 가 다른 Thread 를 방해해서는 안된다.
Java 19 버전에서는 Virtual Thread 기능이 새로 나왔다. 이는 기존 Thread를 더욱 가볍게 사용 가능한 버전으로 아래와 같은 특성이 있다.
ㅤ
Virtual Thread 은 Kotlin 의 Coroutine 의 목적은 유사할 수 있지만 내부 동작은 다른 것을 확인할 수 있다.
https://inside.java/2021/10/26/why-user-mode-thread-video/






주의해야 하는 것은 coroutine 과 user-mode thread 는 미세하게 다르지만 다른 측면에서는 공통 분모가 존재하기 때문에 동의어로 사용할 수 도 있다.
https://inside.java/2020/08/07/loom-performance/

ㅤ

모든 Java 프로그램은 Main Thread 를 필수로 갖는다. 즉, 최소 하나 이상의 Thread 를 갖는다.
Java 프로그램이 실행되면, JVM 은 main thread 를 생성하고, 해당 thread 내부에서 프로그램의 main() 메서드를 호출한다.
JVM 은 또한 보이지 않는 다른 여러 thread 들을 생성한다.
Garbage Collection (GC) 와 Object Finalization 의 관련된 Thread
JVM 의 기타 housekeeping (유지보수) 작업과 관련된 Thread
- Garbage Collection - JVM의 Heap 메모리 영역에서 프로그램 실행에 있어 더 이상 어떤 변수도 참조하지 않아 사용되지 않는 객체(Object)를 자동으로 찾아내어 메모리에서 제거하고 해제하는 역할을 하는 자동 메모리 관리 프로세스
- Object Finalization - JVM에서 더 이상 사용되지 않는 객체(Object)가 메모리에서 완전히 제거되기 직전에 수행되는 특별한 내부 절차
- JVM 작업 - 메모리 관리, 내부 상태 모니터링, 클래스 로딩/언로딩 등
여러 라이브러리/프레임워크들 또한 여러 Thread 를 생성한다.
(AWT, Swing, servlet containers, application servers, RMI 등)
ㅤ

Thread 를 프로그램 개발에서 사용하는 이유
향상된 반응형 UI 제작
멀티 프로세서 (멀티 코어) 시스템 장점 사용
모델링 단순화
비동기 처리와 백그라운드 처리 실행
ㅤ

Event 기반 UI 툴킷은 UI 이벤트 처리를 하는 event thread 를 갖는다.
UI 툴킷에서의 Event Threads 흐름 과정은 아래 순서와 같다.
AWT 와 Swing 같은 UI 툴킷의 UI 객체에 event 리스너가 붙어있다.
event 리스너는 특정 UI 이벤트를 감지한다.
UI 툴킷의 event thread 로부터 event 리스너가 호출된다
ㅤ

모든 현대 OS 는 멀티 프로세서 (멀티 코어) 를 활용하며, 사용 가능한 모든 프로세서에서 thread를 실행하도록 스케줄링 (Scheduling, 관리 및 제어) 한다.
Scheduling 의 기본 단위는 보통 Thread 다.
프로그램이 다수의 활성 threads를 갖는다면, 다수의 threads가 동시에 스케줄링될 수 있다.
ㅤ

ㅤ

예를 들어, 단일 Thread에서 Socket으로부터 데이터를 읽으려 할 때, 현재 사용 가능한 데이터가 없으면 데이터가 도착할 때까지 프로그램 실행이 차단(block)된다.
만약 별도의 Thread를 생성하여 Socket으로부터 데이터를 읽도록 한다면, main thread는 다른 작업을 처리할 수 있으며, Socket에서 데이터가 도착할 때까지 해당 Thread가 대기하면 된다.
polling 방식보다 별도의 Thread를 이용하여 Socket을 대기하는 것이 코드가 더 간단하고 에러 발생 가능성도 낮다 (less erro-prone).
ㅤ

멀티 Thread 프로그램을 개발할 때 주의해야 할 점
다수의 Thread가 동일한 데이터 아이템에 접근할 때, 각 Thread가 데이터의 일관된 상태를 보고 다른 Thread의 변경사항을 덮어쓰지 않도록 접근을 조정(coordinate)해야 한다.
여기서 데이터 아이템이란 static field, 전역적으로 접근 가능한 객체의 instance field, 또는 공유된 collection 등을 말한다.
두 개 이상의 thread에서 변수에 접근할 때, 접근이 적절하게 동기화(synchronized)되었는지 확인해야 한다.
간단한 변수의 경우 volatile 로 정의해도 충분하다.
대부분의 경우 synchronization 을 사용해야 한다.
⚠️ 만약 synchronization을 사용하여 공유 변수의 접근을 보호한다면, 프로그램에서 해당 변수에 접근하는 모든 곳에서 synchronization을 사용해야 한다.
- 폴링(Polling, 또는 Interrogation) 은 클라이언트 프로그램이 외부 장치의 상태를 동기적인(순차적인) 활동으로서 능동적으로 확인(점검)하는 것을 의미한다.
- 간단히 설명해서, 프로그램이 장치에게 "준비됐어?"라고 계속 물어보고 답변을 받을 때까지 기다리는 순차적인 확인 방식이다.
ㅤ

Threads 는 자원을 소모한다.
과도한 Thread 사용은 많은 자원을 소모하여 성능과 유지가능성에 위험하다.
성능을 저하시키지 않고 몇 개의 thread 를 생성하는지의 한계가 존재한다.
다수의 Threads 사용은 싱글 프로세서 시스템에서 CPU 집약적인(CPU-bound) 프로그램을 더 빠르게 실행하지 못한다.
ㅤ

예시 코드는 1,000,000 까지 소수 (1과 자기 자신의 수 외에 나눌 수 없는 1보다 큰 자연수) 를 구하는 코드다.
알고리즘은 에라토스테네스의 체(Sieve of Eratosthenes)를 사용한 것이다.
2는 소수이므로 2를 제외한 2의 배수는 소수가 아니다
3은 소수이므로 3을 제외한 3의 배수는 소수가 아니다
4는 소수가 아니다
5는 소수이므로 5를 제외한 5의 배수는 소수가 아니다
…
이 예제는 두 개의 Thread를 사용한다. 하나는 타이밍(timing)용이고, 하나는 실제 작업(소수 계산)을 수행한다.
Main thread는 소수를 계산하고, main() 메서드에서 생성된 thread는 10초 동안 sleep한 후 finished 플래그를 설정하여 main thread의 작업을 중단시킨다.
/**
* CalculatePrimes -- calculate as many primes as we can in ten seconds
*/
public class CalculatePrimes extends Thread {
public static final int MAX_PRIMES = 1000000;
public static final int TEN_SECONDS = 10000;
public volatile boolean finished = false;
public void run() {
int[] primes = new int[MAX_PRIMES];
int count = 0;
for (int i=2; count<MAX_PRIMES; i++) {
// Check to see if the timer has expired
if (finished) {
break;
}
boolean prime = true;
for (int j=0; j<count; j++) {
if (i % primes[j] == 0) {
prime = false;
break;
}
}
if (prime) {
primes[count++] = i;
System.out.println("Found prime: " + i);
}
}
}
public static void main(String[] args) {
CalculatePrimes calculator = new CalculatePrimes();
calculator.start();
try {
Thread.sleep(TEN_SECONDS);
}
catch (InterruptedException e) {
// fall through
}
calculator.finished = true;
}
}
위의 코드에서 의문이 생긴 부분은 두 가지다.
run( ) 함수를 main( ) 메서드에서 호출하지 않는 점
코드에 존재하지 않는 start( ) 메서드를 호출한 점


ㅤ

Thread를 상속받은 클래스는 run( ) 메서드를 오버라이드 해야만 한다.
위의 코드에서 target 프로퍼티가 무엇인지 궁금해졌다.
ㅤ


start() 메서드를 호출하면 해당 Thread의 실행이 시작된다. (새로운 Thread 가 시작된다)
JVM이 해당 Thread의 run() 메서드를 호출한다.
즉, Thread 실행을 위해서는 start()를 호출하고, 그 후 내부에서 JVM이 run() 메서드를 호출한다.
start() 메서드 호출 결과, 두 개의 threads가 동시에 실행된다.
한 번 이상 thread를 start하는 것은 규칙 위배다.
ㅤ

start() 메서드는 main thread나 VM이 생성/설정하는 "system" 그룹 threads를 위해 호출되지 않는다.

start() 메서드에 추가되는 어떠한 새로운 기능도 VM에 추가되어야 할 수 있다.
start() 메서드 내부 코드에서 핵심으로 보이는 start0() 메서드가 궁금해졌다.

ㅤ
직접 github 에서 C/C++ 로 작성된 JDK 오픈소스 코드를 확인했다.




JVM_StartThread 메서드의 코드 마지막에 native thread를 Thread::start 라는 메서드를 통해 thread를 start 하는 것을 확인할 수 있다.
Thread::start 메서드가 무엇인지 궁금해져서 이 메서드도 추적했다.
ㅤ
ㅤ
jvm.cpp 파일에서 Thread라는 클래스를 직접적으로 #include 하지 않아 이와 유사한 javaThread.hpp 를 확인했다.



Thread가 java Thread 인 경우 Thread를 실행시키기 전에 Thread 상태를 RUNNABLE 로 초기화한다.
Thread를 실행시키기 위해 os::start_thread 메서드를 호출한다.
ㅤ
os::start_thread 메서드는 어떻게 생겼는지 궁금하여 추적했다.

해당 주석 설명이 궁금하여 확인
INITIALIZED vs SUSPENDED 상태 구분 이유
INITIALIZED (처음 시작) - Thread가 처음으로 시작되는 상황
SUSPENDED (일시정지 후 재개) - Thread가 일시정지되었다가 다시 재개되는 상황
처음 시작할 때의 조건 ≠ 일시정지 후 재개할 때의 조건
일시정지된 thread를 재개할 때는 더 엄격한 검사를 하고 싶지만, 처음 시작할 때는 이런 엄격한 검사를 적용하기 어렵다.
Java에서의 Thread.start()는 안전하다
Thread.start()는 Java Thread 객체에 synchronized되어 있음
즉, 동시에 여러 thread가 같은 thread를 start할 수 없음
Race condition(경쟁 상태)이 발생하지 않도록 방지함
os::start_thread 메서드의 동작 - OS 수준의 Thread 상태를 RUNNABLE로 설정하고, pd_start_thread를 통해 Thread를 실행한다.
ㅤ

run_method_name()이라는 심볼(symbol)이 Java 레벨의 run() 메서드와 매핑된다.
이를 vmSymbols.hpp 에서 확인 가능하다.

ㅤ

Thread를 사용하면 다음과 같은 이점이 있다.
GUI 애플리케이션의 반응성 향상
멀티프로세서 시스템의 장점 활용
여러 독립적인 개체들을 다룰 때 프로그램 로직 단순화
전체 프로그램을 차단하지 않고 블로킹 I/O 수행
⚠️ 멀티 쓰레딩에서 공유 데이터에 접근할 때 synchronization을 절대 잊지 말 것!!!