본문 바로가기
Language/Java

[Java] Thread 01

by 기몬식 2024. 6. 30.

Thread



출처


프로세스는 할당받은 자원을 소유하는 일종의 컨테이너로 OS 에 의해 스케줄링 되어 할당됩니다. 프로세스는 방대한 크기를 가지기 때문에 정해진 시간에 처리가 불가능하므로 단위를 나누게 되는데 그 나누어지는 단위가 경량 프로세스(light process)인 스레드입니다.


하나의 프로세스는 여러 스레드를 포함할 수 있으며 멀티 스레딩(Multi Threading) 기능을 통해 여러 스레드들을 동시에 실행하여 메모리와 같은 해당 프로세스의 리소스를 공유합니다. 여러 스레드들이 프로세스의 자원을 공유한다는 것은 프로세스 내에서 생성된 모든 스레드가 동일한 메모리 공간과 시스템 자원들을 함께 사용할 수 있음을 의미합니다.


기본적으로 프로세스는 OS 에서 할당한 자체 리소스를 처리하기 위해 생성, 삭제 및 전환되는데 상당히 많은 비용이 들어가는 커널 스케줄링의 무거운(heavyweight) 단위입니다. 리소스에는 메모리(코드 및 데이터 모두용), 파일 핸들, 소켓, 장치 핸들, 창 및 프로세스 제어 블록이 포함되며 이렇게 OS 로 부터 할당된 프로세스들은 기본적으로 프로세스 격리(process isolation) 을 통해 격리되기 때문에 명시적인 방법(IPC(Inter-Process Communication))을 제외하고는 리소스를 공유하지 않습니다.


따라서 자원을 공유 가능한 스레드는 자원을 효율적으로 활용하고 데이터 교환이 신속하게 이루어질 수 있습니다.

Java

Java 는 복잡한 멀티 스레드 프로그래밍을 쉽게 할 수 있도록 다양한 API 를 제공합니다. 여타 프로그래밍 언어와 같이 스레드와 스레드 풀과 같은 개념을 완전히 추상화하려고 노력하여 개발자가 세부적인 스레드 관리에 신경 쓰지 않고도 효율적인 멀티 스레드 프로그래밍 방식으로 개발할 수 있도록 돕습니다.

Thread

Virtual Thread 이전 Java 의 기존 스레드 모델은 KLT(kernel-level thread)와 ULT(user-level thread)를 1:1 매핑하여 사용합니다. 때문에 Java 의 스레드는 JVM 에서 직접 스케줄링에 관여하는 것이 아닌 OS 의 스케줄링 정책에 따르게 됩니다. 이번 포스팅에서는 Java 의 기존 스레드 모델을 기준으로 Thread 클래스에 대한 글을 작성하도록 하겠습니다.

실습

먼저 Thread 클래스로 실행하는 방법입니다.


@Test
void testThread() {
  Thread thread = new Thread(() -> System.out.printf("current thread name : %s\n", Thread.currentThread().getName()));

  thread.start();`
}

위의 예제는 람다식을 사용하여 Runnable 인터페이스의 run() 메소드를 구현한 후 Thread 객체를 생성하여 strat() 메소드를 통해 새로운 스레드를 생성했습니다. 결괏값은 main 스레드가 아닌 새로운 스레드명이 콘솔에 출력됩니다.


다음 두번째로는 Runnable 인터페이스를 구현하여 스레드의 작업을 정의하는 방법입니다.


@Test
void testRunnable() {
  Thread thread = new Thread(new MyRunnable());
  thread.start();
}

static class MyRunnable implements Runnable {
  @Override
  public void run() {
      System.out.printf("current thread name : %s\n", Thread.currentThread().getName());
  }
}

Runnable 인터페이스를 구현한 객체 인스턴스를 Thread 객체 생성자에 전달 한 뒤 start() 메소드를 호출합니다. 첫번째 예제와 같이 생성한 객체의 start() 메소드를 실행하게 되면 main 스레드가 아닌 새로운 스레드명이 콘솔에 출력됩니다.


Thread 와 Runnable 의 차이

위의 실습을 통해서 Java 에서 제공하는 저수준 API 를 통해 메인 스레드 외의 별도의 새로운 스레드를 생성해서 호출하는 것을 직접 확인했습니다. 그렇다면 Thread 와 Runnable 의 차이는 무엇이고 왜 run() 메소드가 아닌 start() 메소드를 실행해야만 하는지 알아 보도록 하겠습니다.


@Test
void testRunnable() {
  Thread thread = new Thread(new MyRunnable());
  thread.run();
}

static class MyRunnable implements Runnable {
  @Override
  public void run() {
      System.out.printf("current thread name : %s\n", Thread.currentThread().getName());
  }
}

run() 메소드를 실행시키니 콘솔에는 아래와 같이 메인 스레드라고 출력이 됩니다.



Runnable 인터페이스의 run() 메소드 주석의 내용을 간략하게 요약하자면 요약하자면 "스레드가 생성되면 해당 스레드가 생성된 후 메소드가 별도로 실행된다." 입니다.


When an object implementing interface Runnable is used to create a thread, starting the thread causes the object's run method to be called in that separately executing thread.
The general contract of the method run is that it may take any action whatsoever.

또한 Thread#start() 문서Thread 클래스의run() 메소드 주석에 또한 start() 메소드 이후 실행시키는 메소드 정도로만 정의되어 있습니다. 따라서 Runnable 인터페이스는 Thread 를 서브 클래스화 하지 않으면서 활성화 할 수 있는 수단을 제공합니다.

스레드 생성

그렇다면 start() 메소드는 어떻게 새로운 스레드를 생성하는 즉 멀티 스레드로 동작할 수 있도록 하는 것인지 알아 보겠습니다.


public synchronized void start() {
  if (threadStatus != 0)
      throw new IllegalThreadStateException();

  group.add(this);

  boolean started = false;
  try {
      start0();
      started = true;
  } finally {
      try {
          if (!started) {
              group.threadStartFailed(this);
          }
      } catch (Throwable ignore) {
          }
  }
}

private native void start0();

start() 메소드는 크게 세가지 과정으로 진행됩니다.


  1. 스레드가 실행 가능한 상태인지 검사
  2. 스레드를 스레드 그룹에 추가함
  3. JVM 이 스레드를 실행시킴

1. 스레드 상태 검사



출처


NEW(0), RUNNABLE(1), BLOCKED(2), WAITING(3), TIMED_WAITING(4), TERMINATED(5) 이렇게 여섯가지 상태가 있는데 현재 스레드가 시작되지 않은 상태(NEW)인지 검사합니다.

2. 스레드 그릅 추가

자바는 스레드를 논리적으로 그룹화하여 관리할 수 있게끔 스레드를 그룹으로 관리하는데 이 그룹에 해당 스레드를 추가합니다.

3. 스레드 실행

JNI 를 통해 native method 'start0()' 를 실행 시키면서 실제 KLT 를 생성하게 됩니다.

스레드를 실행시키는 네이티브 코드는 해당 문서에서 그 실제 코드를 확인 할 수 있는데 그 내용을 요약 해보면 다음과 같습니다.


1. 먼저 C++ 로 구현된 JVM_StartThread 메소드를 실행합니다.


static JNINativeMethod methods[] = {
  {"start0",           "()V",        (void *)&JVM_StartThread},
  ...
};

2. Java Thread Instance 실행 여부를 확인합니다.

스레드가 시작되면 OS 에서 해당 스레드와 관련된 자원을 할당하게 됩니다.
하지만 스레드가 종료된 후에는 이러한 자원이 해제됨으로 이미 시작된 스레드를 다시 시작하는 것을 방지하기 위해 Java Thread 가 null 임을 확인해야 합니다.


if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
  throw_illegal_thread_state = true;
}

여담으로 왜 해당 스레드 객체의 실행 여부를 검사하는지 서치하던 중 아주 재밌는 글을 발견해서 공유해봅니다.
작성자는 저와 같은 궁금증으로 글을 올렸고 거기에 대한 답변으로 다음과 같이 달렸습니다.


A java thread is a like a human life.  
After starting it (being born), you cannot start it again (get born again).  

재치 있고 이해하기 쉬운 비유인 것 같습니다.

3. Call Stack 사이즈 확인 후 Thread 생성 및 실행

jthread 객체의 스택 크기를 가져 온 후 스택 크기가 유효하면 해당 크기로 설정합니다.


jlong size = java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));

size_t sz = size > 0 ? (size_t) size : 0;

그 후 네이티브 스레드를 생성합니다.


native_thread = new JavaThread(&thread_entry, sz);

네이티브 스레드를 자바 스레드 객체와 연결하고 자바 스레드 실행 준비를 합니다.


그 후 네이티브 스레드 생성이 실패한 경우와 같은 최종적으로 발생할 수 있는 예외 상황을 확인한 후 그디어 자바 스레드 객체를 실행시킵니다.


if (native_thread->osthread() != NULL) {
  native_thread->prepare(jthread);
}

// 예외 처리

Thread::start(native_thread);

결과적으로 Java 는 JNI 를 통해 실제 KLT 와 매핑되는 ULT 를 획득하게 됩니다.


JavaCalls::call_virtual(&result,
obj,
KlassHandle(THREAD, SystemDictionary::Thread_klass()),
vmSymbols::run_method_name(),
vmSymbols::void_method_signature(),
THREAD);

또한 JNI 는 미리 등록된 Thread 클래스의 run() 메소드 시그니처를 실행시킴으로써 사용자가 구현한 메소드가 실제로 JVM 상에서 구동되게 됩니다.


오탈자 및 오류 내용을 댓글 또는 메일로 알려주시면, 검토 후 조치하겠습니다.

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

[Java] Native Method  (0) 2024.06.15
[Java] 동기화  (0) 2024.03.13
[Java] 데이터 병렬 처리(Java 8)  (0) 2024.01.08
[Java] 데이터 병렬 처리(Java 5, 7)  (1) 2023.12.31
[Library] Assertions  (1) 2023.11.27