[JVM] 자바 유저 스레드와 커널 스레드

2025. 7. 5. 17:56·Java/JVM

 

자바 유저 스레드와 커널 스레드

#Java/JVM



스레드에는 사용자 수준 스레드(User Level Threads)와 커널 수준 스레드(Kernel Level Threads) 두 가지 유형이 있다. 두 가지의 장점을 합친 Hybrid 모델도 존재한다. Java에서는 Kernel thread와 User thread를 모두 사용하는 Hybrid 모델이다.


  • 사용자 수준 스레드는 사용자 라이브러리를 통해 사용자가 만든 스레드로, 스레드가 생성 된 프로세스의 주소 공간에서 해당 프로세스에 의해 실행되고 관리된다.
  • 커널 수준 스레드는 커널에 의해 생성되고 운영체제에 의해 직접 관리된다. 사용자 수준 스레드보다 생성 및 관리 속도가 느리다.

두 유형의 차이는 누가 스레드를 제어하느냐에 있다.
사용자 수준 스레드는 스레드가 생성 된 프로세스 자체에 의해 제어되며, 커널은 프로세스 내의 사용자 스레드에 대해 알지 못한다.
커널 수준 스레드는 OS에 의해 직접 관리된다.


사용자 스레드를 실행한다고 가정해보자. 스레드를 실행하기 위해서는 커널의 CPU 스케줄러가 스레드를 CPU 에 스케줄링 해야 한다. 그런데 CPU 스케줄러는 커널의 일부이기 때문에 사용자 스레드에 대해 알지 못한다.
커널 스레드와 사용자 스레드는 매핑되어야 한다.


이 과정을 신제품 출시에 비유할 수 있다. 우리가 출시하려는 신제품은 사용자 스레드다.
이 제품을 판매하기 위해 제품의 회사를 정부에 등록해야한다. 여기서 회사는 프로세스, 정부는 커널이다.
회사는 상점에 입점을 하고, 입점을 위해서 정부의 규칙을 따라야한다. 이 상점이 커널 스레드다.
 
즉, 커널 스레드(상점)와 프로세스(회사)가 매핑되어 있고, 그 프로세스 내에서 사용자 스레드가 생성되기 때문에 커널에서 인식할 수 있는 것이다. 

커널 스레드와 사용자 스래드를 매핑하는 방법

1. Many-to-One Model, 다대일 모델


다대일 모델은 여러 사용자 스레드를 하나의 커널 스레드에 매핑한다. 스레드는 사용자 공간의 스레드 라이브러리에 의해 관리된다.
한 스레드가 Blocking 되는 경우에 전체 프로세스가 Block 된다. 한 번에 하나의 스레드만이 커널에 접근할 수 있기 때문에, 멀티 코어 시스템에서 병렬로 실행될 수 없다. 하지만 동기화 및 리소스 공유가 쉽기 때문에 실행 시간이 단축된다.


Java 의 초기 버전의 스레드 모델인 Green Thread 가 다대일 모델을 사용하였다.
대부분의 컴퓨터 시스템에서 멀티 코어가 표준이 되면서 그 이점을 살릴 수 없는 Green Thread 는 현재 Java 에서 사용되지 않는다.


One-to-One Model, 일대일 모델


일대일 모델은 각 사용자 스레드를 각각 하나의 커널 스레드에 매핑한다.
그렇기 때문에 하나의 스레드가 Blocking 되더라도 다른 스레드가 실행될 수 있기 때문에 병렬 실행이 용이하다.
 
이 모델의 단점은 사용자 스레드를 만들기 위해서는 커널 스레드를 만들어야 한다는 것이다.
많은 수의 커널 스레드는 시스템 성능에 부담을 줄 수 있기 때문에 Window 나 Linux 는 스레드 수의 증가를 제한한다.


Many-to-Many Model, 다대다 모델

다대다 모델은 여러 개의 사용자 스레드를 그보다 작은 수 혹은 같은 수의 커널 스레드로 멀티플렉스한다.
커널 스레드의 수는 응용 프로그램에 따라 다르다. 4코어 시스템보다 8코어 시스템에서 더 많은 커널 스레드를 할당받는다.
 
이전 두 모델의 단점을 절충한 방법으로, 개발자는 필요한 만큼 사용자 스레드를 생성하고 그에 상응하는 커널 스레드가 병렬로 수행될 수 있다. 가장 높은 정확도의 동시성 처리를 제공하는 모델로, 하나의 스레드가 Blocking 되었을 때 커널은 다른 스레드의 수행을 스케쥴할 수 있다.
 
또한, 다대다 모델은 two-level-model(두 수준 모델) 이라고도 불리는데 다대다 모델은 사용자 스레드가 하나의 커널 스레드에만 연결되는 일대일 모델로 동작하는 것도 허용하기 때문이다.



다대다 모델이 세 모델 중 가장 융통성있는 것 처럼 보이지만 실제로는 구현하기가 어렵다.
그리고 대부분의 시스템에서 코어 수가 증가하면서 커널 스레드를 제한하는 것의 중요성이 줄어들었다.
따라서 현재 대부분의 운영체제는 일대일 모델을 사용한다.


그렇다면 현재 자바의 스레드는 어떤 모델을 사용할까?
자바는 JDK 1.3 이후로 다대다 모델을 사용한다. 이를 Native Thread 라고 한다.


Green Thread vs. Native Thread


자바는 구체적으로 커널 스레드를 어떻게 관리할까?

Step 1. LWP(Light Weight Process)




   Oracle 레퍼런스를 참고했다. 먼저 그림과 용어에 대해서 설명이 필요하다.

  • JavaApplication-level의 스레드: Java 프로그래머가 1차적으로 마주하는 스레드
  • Kernel-level 스레드: OS가 관리하는 스레드

앞으로는 JavaApplication-Level-Threads 를 JLTs, Kernel-Level-Threads를 KLTs 라고 지칭하겠다


 중요한 것은 LWP 이다. KLTs 와 JLTs의 중간다리 역할을 한다. 즉 OS에게 KLTs 관리를 부탁하고, 자바 어플리케이션 프로그래머가 요청한 JavaThread를 KernelThread에 매핑한다. 보통 스레드를 다른 말로 LWP(Light Weight Process) 라고도 하는데, 이 맥락에서는 LWP가 단순 스레드만을 의미하지 않는다. 위 그림에서 볼 수 있다시피, 커널과 user-level 스레드 사이에서 일종의 인터페이스 같은 역할을 수행한다. Solaris의 맥락에서는 LWP 의미가 다르다는 점을 기억하자. 


LWP의 기능은 Solaris Threads Library로 구현되어 있다. LWP와 JavaThread를 binding 하는 역할을 수행한다. JavaThread는 unbound 된 상태가 default 이다. 이렇게 함으로써, unbound JLTs 의 요청에 맞게 pool of LWPs 의 크기를 조절한다. 
 
참고로 라이브러리는 c++ 코드로 구현되어 있다. Java 소스 코드를 실행하면 JNI(JavaNativeInterface, 외부의 다른 소스코드를 실행할 수 있도록 하는 인터페이스)를 통해 이 라이브러리 코드가 실행되도록 하는 원리이다. 
 

Step2. LWP 매핑 시뮬레이션

java에서 스레드를 사용해 본 사람은 대충 알겠지만 start() / run() / join() / sleep() 같은 인터페이스를 갖는다. 해당 메서드는 os에 종속적이다. 즉 system call 을 통해서 os 에 해당 기능을 요청해야한다. os에 요청하는 코드는 jvm에 c++ 코드로 작성되어 있다. 요청과 응답 flow 는 대략 아래와 같다.


실제 JVM 코드는 꽤 복잡하고 방대하기 때문에 바로 들여다보기에는 무리가 있었다. 찾아보던 중 java thread 생성 관련해서 위의 동작을 간단하게 구현한 project 가 있어서 공유한다. 코드를 일일이 보기 귀찮으면, 위에서 설명한 LWP의 개념과 OS까지의 요청흐름 정도만 알아도 될 듯하다.


How Java thread maps to OS thread? JVM thread maps to OS thread is a commonly read statement. But what does it really mean?. We create Thread object in java and call its… medium.com
위 프로젝트(단순하게 Java에서 스레드 19개를 생성)의 실행결과는 아래와 같다.


  • “Starting thread_entry_point" 는 JVM이 os에게 스레드를 요청 시도(c++)할 때의 출력.
  • “Started a linux thread 140531437143808" 는 os가 실제로 스레드를 생성 완료했을 때의 출력.
  • “Running Thread 1"는 JavaThread 객체가 run메소드(java 코드)를 실행했을 때의 출력.
[info] Loading settings from build.sbt ...
[info] Set current project to threading (in build file:/threading/)
[info] Executing in batch mode. For better performance use sbt's shell
[success] Total time: 1 s, completed Jan 1, 2018 3:39:01 AM
[success] Total time: 1 s, completed Jan 1, 2018 3:39:02 AM
[info] Running (fork) com.threading.ThreadingApp 
[info] Started a linux thread Starting thread_entry_point 140531437143808!
[info] Started a linux thread 140531428751104!
[info] Starting  thread_entry_point Started a linux thread 140531420358400!
[info] Starting  thread_entry_point Started a linux thread 140531411965696!
[info] Starting  thread_entry_point Started a linux thread 140530323289856!
[info] Running Thread 1
[info] Running Thread 3
[info] Starting  thread_entry_point Started a linux thread 140530314897152!
[info] Starting  thread_entry_point Started a linux thread 140531437143808!
[info] Running Thread 2
[info] Running Thread 4
[info] Starting  thread_entry_point Started a linux thread 140530306504448!
[info] Started a linux thread 140531428751104!
[info] Starting  thread_entry_point Starting  thread_entry_point Started a linux thread 140530298111744!
[info] Running Thread 5
[info] Started a linux thread 140530314897152!
[info] Started a linux thread 140531411965696!
[info] Started a linux thread 140530289719040!
[info] Started a linux thread 140530281326336!
[info] Started a linux thread 140530272933632!
[info] Started a linux thread 140529987745536!
[info] Started a linux thread 140529979352832!
[info] Started a linux thread 140529970960128!
[info] Started a linux thread 140529962567424!
[info] Running Thread 9
[info] Running Thread 8
[info] Running Thread 7
[info] Running Thread 6
[info] Running Thread 10
[info] Running Thread 15
[info] Running Thread 16
[info] Running Thread 13
[info] Running Thread 14
[info] Running Thread 12
[info] Running Thread 11
[info] Running Thread 17
[info] Running Thread 19
[info] Running Thread 18

결과를 잘 보면 JavaThread는 1~19번까지 19개가 생성된 것을 볼 수 있다. "Started a linux thread 140531437143808" 라는 linux thread 실행 문구도 19번 출력된 것을 볼 수 있다. 그런데 진짜 잘보면 linux thread의 번호가 중복되는 게 존재한다. 중복되는 스레드 번호를 제외한 스레드 개수는 15개이다. JVM이 한번 생성한 osThread 를 배열처럼 들고 있어서 osThread 가 실행을 완료한 시점에 바로 삭제하지 않는 것 같다. JavaThread 요청이 있으면, 바로 새로 osThread를 만드는것이 아니라 가용 osThread를 매핑해주는 방식인 듯하다. 정확히는 커널 코드를 직접 봐야 알 수 있을 듯하다.


Step3. Openjdk 커널 코드 분석



  Native thread는 Library-Level의 c++ thread 객체가, OS kernel thread 소유하게 된 상태를 의미한다. 구현 코드를 볼 때 중요하다.


1) Thread

Thread 클래스는 java에서 사용되는 거의 모든 스레드의 조상이라고 보면 된다. Thread 인스턴스는 KLT를 멤버변수로 가지고 있다. 내부적으로는 osThread 라고 한다. Thread 인스턴스가 OS에 의해서 kernel thread를 실제로 할당받은 상태를 native thread 라고 할 수 있다.


(1) 스레스 상속 구조

// thread.hpp:96
// Class hierarchy
// - Thread
//   - JavaThread
//     - various subclasses eg CompilerThread, ServiceThread
//   - NonJavaThread
//     - NamedThread
//       - VMThread
//       - ConcurrentGCThread
//       - WorkerThread
//     - WatcherThread
//     - JfrThreadSampler
//     - LogAsyncWriter
//
// All Thread subclasses must be either JavaThread or NonJavaThread.
// This means !t->is_Java_thread() iff t is a NonJavaThread, or t is
// a partially constructed/destroyed Thread.

(2) 스레드 실행 흐름

// thread.hpp:113
// Thread execution sequence and actions:
// All threads:
//  - thread_native_entry  // per-OS native entry point
//    - stack initialization
//    - other OS-level initialization (signal masks etc)
//    - handshake with creating thread (if not started suspended)
//    - this->call_run()  // common shared entry point
//      - shared common initialization
//      - this->pre_run()  // virtual per-thread-type initialization
//      - this->run()      // virtual per-thread-type "main" logic
//      - shared common tear-down
//      - this->post_run()  // virtual per-thread-type tear-down
//      - // 'this' no longer referenceable
//    - OS-level tear-down (minimal)
//    - final logging
//
// For JavaThread:
//   - this->run()  // virtual but not normally overridden
//     - this->thread_main_inner()  // extra call level to ensure correct stack calculations
//       - this->entry_point()  // set differently for each kind of JavaThread

(3) 스레드 종류 및 우선순위

// os.hpp:70
enum ThreadPriority {        // JLS 20.20.1-3
  NoPriority       = -1,     // Initial non-priority value
  MinPriority      =  1,     // Minimum priority
  NormPriority     =  5,     // Normal (non-daemon) priority
  NearMaxPriority  =  9,     // High priority, used for VMThread
  MaxPriority      = 10,     // Highest priority, used for WatcherThread
                             // ensures that VMThread doesn't starve profiler
  CriticalPriority = 11      // Critical thread priority
};

// os.hpp:455
enum ThreadType {
    vm_thread,
    gc_thread,         // GC thread
    java_thread,       // Java, CodeCacheSweeper, JVMTIAgent and Service threads.
    compiler_thread,
    watcher_thread,
    asynclog_thread,   // dedicated to flushing logs
    os_thread
  };

2) JavaThread

JavaThread 는 Thread로부터 상속받은 osthread 변수가 하나 있고, _threadObj라는 java level 스레드가 있다. 즉 자바스레드와 커널 스레드를 연결하는 인스턴스인 셈이다.


// thread.hpp:677
class JavaThread: public Thread {
  friend class VMStructs;
  friend class JVMCIVMStructs;
  friend class WhiteBox;
  friend class ThreadsSMRSupport; // to access _threadObj for exiting_threads_oops_do
  friend class HandshakeState;
 private:
  bool           _on_thread_list;                // Is set when this JavaThread is added to the Threads list
  OopHandle      _threadObj;                     // The Java level thread object
  
  /* ...생략... */
}

3) Thread.start in Java

java 코드에서 thread.start()를 호출 시 아래의 JNINativeMethod를 통해 JVM_startThread 메소드가 호출된다. 여기서 가장 중요한 흐름은 아래와 같다.


  1. Solaris Library Thread 생성 (c++ JavaThread 클래스 인스턴스)
  2. Kernel Thread 생성
  3. Solaris Library Thread와 kernel Thread를 연결해서 native Thread 완성
  4. native Thread 와 JavaApplication-level Thread 를 연결 (binding)
  5. 완성된 스레드를 스레드큐에 추가
  6. JavaApplication-level Thread 가 원하는 코드를 native Thread 에게 실행시킴 
(Thread.run() in Java code)

⠀JNINativeMethods
//prims/jvm.cpp:2850
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  
  JavaThread *native_thread = NULL;

  {
    MutexLocker mu(Threads_lock);

    // javaThread가 이전에 start상태였으면 생성 불가!
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
     
      // c++ thread와 os thread를 연결해서 native_thread 생성
      // 부모는 자식스레드(Java와 연결될 os 커널스레드)를 생성하고 나옴. (INITIALIZED 상태가 됨)
      // 자식스레드(os 스레드)는 부모 스레드가 run() 실행을 허락하기 전까지 대기.
      native_thread = new JavaThread(&thread_entry, sz);  // <-- 중요!
      

      if (native_thread->osthread() != NULL) {
        // 자식 스레드를 실행시키기 전에 꼭 처리되어야 하는 전처리 작업 -> 조건동기화가 필요!
        // native thread를 java thread와 연결, 스레드 리스트에 추가
        native_thread->prepare(jthread);
      }
    }
  }

    /* ...생략... */

  Thread::start(native_thread);  // 자식 스레드가 run()을 수행하도록 허락

JVM_END

자식 스레드는 waiting 후 부모에게 허락 signal 받으면 run() 수행
(condition variable을 이용한 조건 동기화처럼 보인다.)


// new JavaThread -> create_thread -> pthread_create(..., thread_native_entry, ...)

// os_linux.cpp:660
// Thread start routine for all newly created threads
static void *thread_native_entry(Thread *thread) {
  
  /* ...생략... */
  
  // handshaking with parent thread
  {
    MutexLocker ml(sync, Mutex::_no_safepoint_check_flag);

    // notify parent thread
    osthread->set_state(INITIALIZED);
    sync->notify_all();

    // wait until os::start_thread()
    // 여기서 멈춰있음, 부모가 바인딩과 같은 전처리 작업을 수행 후 RUNNABLE 상태로 변경시켜주면 진행
    while (osthread->get_state() == INITIALIZED) {  
      sync->wait_without_safepoint_check();
    }
  }
  
  // call one more level start routine
  thread->run()

  /* ...생략... */
}


부모스레드는 대기하고 있는 자식스레드(osthread, JLT와 binding될 스레드)를 깨워서 run을 수행하게끔 한다.
Thread::start(Thread* thread)

//thread.cpp:518
void Thread::start(Thread* thread) {
  if (thread->is_Java_thread()) {
    java_lang_Thread::set_thread_status(JavaThread::cast(thread)->threadObj(),
                                        JavaThreadStatus::RUNNABLE);
  }
  os::start_thread(thread);
}

//os.cpp:873
void os::start_thread(Thread* thread) {
  // guard suspend/resume
  MutexLocker ml(thread->SR_lock(), Mutex::_no_safepoint_check_flag);
  OSThread* osthread = thread->osthread();
  osthread->set_state(RUNNABLE); // 여기서 자식의 상태를 변경하므로 자식스레드가 대기상태에서 벗어남
  pd_start_thread(thread); 
}

위 3 STEP을 쉽게 설명하자면

1. 자바 스레드 생성

  • 개발자 관점: 자바 코드에서 new Thread()로 스레드 객체를 만들고, start() 메서드를 호출합니다.

2. JNI를 통한 네이티브 호출 및 네이티브 스레드 생성

  • 내부 동작: start()가 호출되면, JVM은 JNI(Java Native Interface)를 통해 네이티브 코드(C++ 코드)를 호출하여 JavaThread라는 네이티브 객체를 생성합니다.

3. OS에 커널 스레드 요청 및 생성 (LWP)

  • 내부 동작: 네이티브 코드에서는 OS에 실제 스레드를 생성해달라고 요청합니다. 이때 생성된 스레드는 커널 스레드 또는 LWP(Light Weight Process)라 불립니다.

4. 매핑 및 바인딩 (유저 스레드와 커널 스레드 연결)

  • 내부 동작: 생성된 커널 스레드는 자바의 JavaThread 객체와 1:1로 바인딩됩니다. 다만, 자바는 필요에 따라 이미 생성된 커널 스레드(사용 가능한 OS 스레드)를 재활용하는 경우도 있습니다.

5. 스레드 실행 시작

  • 내부 동작: 모든 바인딩이 완료되면, 부모 스레드가 신호를 보내 커널 스레드의 상태를 RUNNABLE로 전환합니다. 그러면 OS 스케줄러가 해당 커널 스레드를 선택하여 자바 스레드의 run() 메서드를 실행하게 됩니다.

정리

  • Java에서는 User-Level thread의 요청을 감당할 수 있을만큼만, Kernel-Level thread를 생성해서 운용한다. 즉 java thread 와 kernel thread 의 개수가 1대 1이 아니라는 얘기.. (물론 binding 은 1대1이다.)

참고) JavaThread 에 대해 깊게 이해해보자 (feat. Openjdk 커널 분석)
https://e-una.tistory.com/70


참고) 자바스크립트, Node.js는 싱글스레드

자바스크립트는 싱글스레드 환경이다. 처음에는 여기서 이해가 되지 않았다. 그러면 자바스크립트에서는 비동기를 어떻게 처리하는거지?


Node.js는 싱글스레드를 효율적으로 사용할 수 있는 이벤트 루프 방식을 선택했다.


코드가 진행되다가 비동기 작업을 만나면 이 작업을 처리해주는 대상들에 위임한다. 그리고 대상들이 작업을 끝내면 전달받은 콜백을 큐에 넣어준다.


어려우니까 나중에 따로 공부해보자!

'Java > JVM' 카테고리의 다른 글

[JVM] 자바 코드부터 JVM의 동작까지 구체적으로 살펴보기  (0) 2025.07.05
'Java/JVM' 카테고리의 다른 글
  • [JVM] 자바 코드부터 JVM의 동작까지 구체적으로 살펴보기
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (463)
      • 개발 일지 (0)
        • Performance (0)
        • TroubleShooting (0)
        • Refactoring (0)
        • Code Style, Convetion (0)
        • Architecture (0)
      • Software Engineering (36)
        • Test (8)
        • 이론 (18)
        • Clean Code (10)
      • Java (72)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (13)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (16)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (130)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (6)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[JVM] 자바 유저 스레드와 커널 스레드
상단으로

티스토리툴바