Java Thread Basics

류태웅·2025년 12월 5일

Java Tutorial 설명 글

IBM Developer

Java 언어 설계자인 Brian Goetz 분이 IBM developer 사이트에서 작성하신 Java Thread 의 튜토리얼 글을 바탕으로 정리한 글입니다.

이 분은 Java의 Lambda Expressions 의 기술적 사양(Specification) 리드 역할을 담당한 경험 등 Java 언어에 많은 관여를 하신 개발자 분입니다.

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


Java Thread 란?

  • 대부분의 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 와 다음과 같은 차이점이 있다.

    1. Thread 간의 격리된 상태는 Process 간의 경우보다 덜 격리되어 있다.
    2. Threads 는 메모리를 공유한다.
    3. Threads 는 파일을 다룬다.
    4. 각 Process 의 상태를 공유한다.

  • 하나의 Process 는 여러 Thread를 지원할 수 있다.

  • Threads 는 Parallelism (simultaneously) 와 Concurrency 를 지원한다.

    • Parallelism: 병렬성 - 여러 리소스(코어)에서 여러 작업을 빠르게 처리하기 위해 동시간대에 실행
    • Concurrency: 동시성 - 한정된 리소스(코어)에서 여러 작업을 효율적으로 처리하기 위해 전환하며(scheduling) 실행
    • Concurrency는 Multi-threading과 Single-threading 기반 Asynchronous programming(non-blocking I/O) 등 다양한 방식으로 구현 가능
  • 하나의 Process 내부의 여러 Thread 는 동일한 메모리 주소 공간을 공유한다. 즉, 해당 여러 Thread는 아래의 작업이 가능하다.

    • 동일한 변수와 객체에 접근 가능하다.
    • 동일한 heap 에 객체들을 할당할 수 있다.
    • Heap : 메모리의 한 구역으로, 참조 변수 (Reference Variables) 와 동적 할당 객체 (Dynamically Allocated Objects) 가 저장되는 공간이다.
  • 장점으로 단일 Process 내부의 여러 Thread 가 동일한 정보에 접근할 수 있다.

  • 주의해야 할 점은 각 Thread 가 다른 Thread 를 방해해서는 안된다.

  • Java 19 버전에서는 Virtual Thread 기능이 새로 나왔다. 이는 기존 Thread를 더욱 가볍게 사용 가능한 버전으로 아래와 같은 특성이 있다.

    • 코드 작성 길이를 줄여준다.
    • 유지보수를 쉽게해준다.
    • 높은 처리량 (high-throughput)의 동시성 어플리케이션 관찰을 쉽게해준다.


Virtual Thread vs Coroutine

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 프로그램의 Thread 사용

  • 모든 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 등)


Threads 사용 이유

  • Thread 를 프로그램 개발에서 사용하는 이유

    1. 향상된 반응형 UI 제작

    2. 멀티 프로세서 (멀티 코어) 시스템 장점 사용

    3. 모델링 단순화

    4. 비동기 처리와 백그라운드 처리 실행


향상된 반응형 UI

  • Event 기반 UI 툴킷은 UI 이벤트 처리를 하는 event thread 를 갖는다.

    • UI 이벤트에는 키보드 입력, 마우스 클릭 (화면 터치) 등이 있다.
  • UI 툴킷에서의 Event Threads 흐름 과정은 아래 순서와 같다.

    1. AWT 와 Swing 같은 UI 툴킷의 UI 객체에 event 리스너가 붙어있다.

    2. event 리스너는 특정 UI 이벤트를 감지한다.

    3. UI 툴킷의 event thread 로부터 event 리스너가 호출된다


멀티 프로세스 시스템의 장점 활용

  • 모든 현대 OS 는 멀티 프로세서 (멀티 코어) 를 활용하며, 사용 가능한 모든 프로세서에서 thread를 실행하도록 스케줄링 (Scheduling, 관리 및 제어) 한다.

  • Scheduling 의 기본 단위는 보통 Thread 다.

  • 프로그램이 다수의 활성 threads를 갖는다면, 다수의 threads가 동시에 스케줄링될 수 있다.


모델링의 간단함

  • 서로 독립적인 여러 작업을 처리할 때, 각 작업에 별도의 thread를 사용하면 프로그램을 더 간단하게 작성하고 유지보수할 수 있다.


비동기 또는 백그라운드 실행

  • 예를 들어, 단일 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) 은 클라이언트 프로그램이 외부 장치의 상태를 동기적인(순차적인) 활동으로서 능동적으로 확인(점검)하는 것을 의미한다.
  • 간단히 설명해서, 프로그램이 장치에게 "준비됐어?"라고 계속 물어보고 답변을 받을 때까지 기다리는 순차적인 확인 방식이다.

https://en.wikipedia.org/wiki/Polling_(computer_science)


과도한 사용 금지

  • Threads 는 자원을 소모한다.

  • 과도한 Thread 사용은 많은 자원을 소모하여 성능과 유지가능성에 위험하다.

  • 성능을 저하시키지 않고 몇 개의 thread 를 생성하는지의 한계가 존재한다.

  • 다수의 Threads 사용은 싱글 프로세서 시스템에서 CPU 집약적인(CPU-bound) 프로그램을 더 빠르게 실행하지 못한다.


Thread 예시 코드

  • 예시 코드는 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 Deep Dive

  • 이를 파악하기 위해서 Thread 클래스 소스 코드를 확인했다.

  • Thread를 상속받은 클래스는 run( ) 메서드를 오버라이드 해야만 한다.

  • 위의 코드에서 target 프로퍼티가 무엇인지 궁금해졌다.

  • Thread 초기화 생성자에서 target 프로퍼티의 의미를 할 수 있었다.

  • Thread 생성자에서 6가지 프로퍼티를 이용하여 초기화하는 것을 알 수 있다.
    - Thread group
    - target : run ( ) 메서드를 통해 불리는 객체
    - name : 새로운 Thread 이름
    - stackSize : 희망하는 스택 크기, 0이면 stackSize 는 무시된다.
    - acc : AccessControlContext 상속. 만약 null 이면 AccessController.getContext()
  • start( ) 메서드에 대해서도 파악했다.

  • start() 메서드를 호출하면 해당 Thread의 실행이 시작된다. (새로운 Thread 가 시작된다)

    • JVM이 해당 Thread의 run() 메서드를 호출한다.

    • 즉, Thread 실행을 위해서는 start()를 호출하고, 그 후 내부에서 JVM이 run() 메서드를 호출한다.

  • start() 메서드 호출 결과, 두 개의 threads가 동시에 실행된다.

    • start() 메서드를 호출한 현재 Thread (코드 예시에서 main 쓰레드가 start() 호출 후 반환되어 계속 실행)
    • 새롭게 시작된 Thread (해당 Thread의 run() 메서드를 실행)
  • 한 번 이상 thread를 start하는 것은 규칙 위배다.

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

    • VM = JVM
  • start() 메서드에 추가되는 어떠한 새로운 기능도 VM에 추가되어야 할 수 있다.

  • start() 메서드 내부 코드에서 핵심으로 보이는 start0() 메서드가 궁금해졌다.

  • start0()는 native 메서드라 JDK native 레벨 소스코드를 확인해야만 기능을 알 수 있다.


Thread Deeper Dive

  • JDK native 에서 java 레벨의 start0 메서드는 C 파일에서 JVM_StartThread 이름의 메서드와 맵핑되어 있다.

  • JVM_StartThread 메서드 정의 소스 코드가 궁금해져 이 코드를 추적했다.

  • C++ Thread 생성자를 호출하여 native thread를 생성하고 있다.

  • JVM_StartThread 메서드의 코드 마지막에 native thread를 Thread::start 라는 메서드를 통해 thread를 start 하는 것을 확인할 수 있다.

  • Thread::start 메서드가 무엇인지 궁금해져서 이 메서드도 추적했다.

  • jvm.cpp 파일에서 Thread라는 클래스를 직접적으로 #include 하지 않아 이와 유사한 javaThread.hpp 를 확인했다.

  • javaThread.hpp 에서 thread.hpp를 참조하는 것을 발견했다.

  • thread.cpp 에서 Thread::start 메서드를 발견했다.

  • 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를 실행한다.

    • 더 이상 내부로 파고드는 것은 멈췄다.

  • JVM_StartThread 메서드 내부에서 thread_entry 메서드를 사용하는데, 이 메서드에서 Java 레벨의 run( ) 메서드를 실행하는 것을 확인할 수 있다.

  • run_method_name()이라는 심볼(symbol)이 Java 레벨의 run() 메서드와 매핑된다.

  • 이를 vmSymbols.hpp 에서 확인 가능하다.

  • 여기까지 JDK native 오픈소스 코드를 확인하여 start( ) 메서드 내부에서 run( ) 메서드를 실행시키는 것을 확인했다.


Thread 기본 요약

  • Thread를 사용하면 다음과 같은 이점이 있다.

    1. GUI 애플리케이션의 반응성 향상

    2. 멀티프로세서 시스템의 장점 활용

    3. 여러 독립적인 개체들을 다룰 때 프로그램 로직 단순화

    4. 전체 프로그램을 차단하지 않고 블로킹 I/O 수행

  • ⚠️ 멀티 쓰레딩에서 공유 데이터에 접근할 때 synchronization을 절대 잊지 말 것!!!

0개의 댓글