멀티 스레드 — 1. 자바에서 스레드의 생성과 시작, 그리고 자바 스레드와 os 스레드 맵핑

woody
9 min readApr 25, 2021

--

프로세스와 스레드

프로세스란 메모리를 할당 받아 실행중인 프로그램을 의미합니다. (애플리케이션 단위)그리고 스레드는 어떠한 프로그램, 특히 프로세스 내에서 실행되는 흐름의 단위를 의미합니다. (애플리케이션 내 테스킹 단위)

흐름의 단위는 다른 말로 하면 “명령어 블록” 으로, 시작점과 종료점을 가진다 할수 있으며, 스레드는 실행 중에 멈출 수 있으며 동시 또는 병렬적으로 실행가능합니다.

스레드 사용과 관련해서 알아둬야할 기본 내용은 아래와 같으며, 앞으로 1번 부터 하나씩 알아보겠습니다.

  1. 스레드를 시작하고 종료하고, (=>스레드와 생성과 시작, 종료 방법, 자바 스레드와 os 스레드 맵핑)
  2. 필요에 따라 실행 중에 멈추었다가 재 실행하고 (=>스레드의 상태 및 상태 제어 방법)
  3. 여러 스레드를 동시 또는 병렬적으로 실행 가능하고, 스레드를 그룹화 해 일괄 제어도 가능하다. (=>스레드 스케쥴링 및 동기화, 그리고 스레드 그룹)
  4. 또한 스레드 개수의 증가로 바빠진 cpu의 메모리 사용량 증가는 애플리케이션의 성능저하를 불러올수 있기 때문에 스레드 관리 기법이 필요하다. (=>스레드 풀)

자바에서 스레드의 생성과 시작, 그리고 자바 스레드와 os 스레드 맵핑

자바에서 스레드의 생성과 시작

자바에서는 스레드도 객체로 생성되기 때문에 클래스가 필요합니다. 그 클래스가 바로 java.lang.Thread 입니다. Thread 클래스는 아래 그림과 같이 Object 클래스를 상속받고, Runnable 인터페이스를 구현하였습니다.

Thread 클래스로 스레드 객체를 생성하는 방법은 2가지가 있을수 있겠습니다.

  • 직접 생성 -> ex) Thread thread = new Thread(Runnable target);
  • Thread 클래스를 상속받은 하위 클래스 작성 및 생성

Thread 클래스로 스레드 객체를 생성할때 매개값으로 Runnable 객체를 받아 생성자를 호출하는데, 이 Runnable 인터페이스는 인수없이 하나의 메소드 run () 만 정의하고 있습니다. 스레스 스케쥴링에 의해 여러 스레드들은 아주 짧은 시간에 번달아 가면서 이 run() 메소드를 조금씩 실행합니다. 즉 run() 메소드에는 스레드에서 실행해야하는 코드를 보유해야 하며, 따라서 Runnable 인터페이스를 구현하는 클래스는 run () 메서드를 재정의해야합니다.

public class RunnableDemo {

public static void main(String[] args) {
System.out.println("From main() method: " + Thread.currentThread().getName());

Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("From run() method: " + Thread.currentThread().getName());
}
};


System.out.println("Creating a Thread Instance");
Thread thread = new Thread(runnable);

System.out.println("Launching the thread...");
thread.start();
}
}

스레드는 생성 즉시 실행되는 것이 아니라, 위의 예제와 같이 start() 메소드를 호출하면 스레드가 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 작업 처리를 시작합니다.

자바 스레드와 os 스레드 맵핑

자바 프로그램은 jvm 위에서 동작하기 때문에, 자바 스레드와 os 스레드간의 맵핑이 어떻게 이뤄지는지 궁금할수 있는데요, 결론부터 이야기하자면 자바 스레드와 os 스레드는 1:1 맵핑이 됩니다.

즉 생성된 모든 자바 스레드 객체에 대해 os 스레드를 생성합니다. 스레드 스케쥴링 역시 os 의 스케쥴링 정책을 그대로 따릅니다. (초창기 jvm 에서는 jvm이 직접 스레드를 생성하고, 스케쥴링 했다고 합니다.-> green thread)

예제 코드를 통해 jvm 이 자바 스레드 객체를 어떻게 처리하는지 살펴보겠습니다.

(참고 : https://medium.com/@unmeshvjoshi/how-java-thread-maps-to-os-thread-e280a9fb2e06 자바 스레드와 os 스레드가 어떻게 연결되어 동작하는지 설명이 잘 되어있습니다. 아래 내용은 위 블로그의 글을 정리했습니다.)

  1. 자바 Thread 클래스

아래는 Thread 샘플 클래스입니다. Thread 클래스는 start() 와 run() 2개의 메소드만 존재합니다.

public class Thread {
static AtomicInteger threadCount = new AtomicInteger(1);

public void run() {
System.out.println("Running Thread " + threadCount.getAndIncrement());
}

public void start() {
start0();
}
private native void start0();
}

start() 메서드가 네이티브 메소드로 선언 된 start0() 메서드를 호출하면서 우리가 알고싶어하는 마법이 시작됩니다. (여기에서 ‘native’ 키워드는 jvm 에게 호출해야하는 메서드가 c/c++ 로 작성된 네이티브 메서드임을 알립니다. JNI 는 네이티브 메서드 인터페이스 사양을 의미하며, 자바가 다른 언어로 만들어진 어플리케이션과 상호 작용할 수 있는 인터페이스를 제공합니다.)

2. 네이티브 메서드를 선언하고 있는 Thread 클래스의 헤더 파일 생성

JDK 에는 ‘네이티브 메서드를 선언하고있는 클래스’에 대한 헤더 파일을 생성하는 javah 라는 도구를 제공하여 네이티브 구현에 사용하도록 하고 있습니다.

//example
javah -classpath ../../../target/scala-2.12/classes -jni com.threading.Thread

위의 명령으로 만든 네이티브 메서드는 아래와 같습니다.

/*
* Class: com_threading_Thread
* Method: start0
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_threading_Thread_start0
(JNIEnv *, jobject);

3. os 스레드 생성과 os 스레드와 자바 스레드 간의 접근

헤더를 만들었으니 이제 네이티브 c/c++로 Java_com_threading_Thread_start0() 메서드를 구현해야합니다. threading.cpp 에 아래와 같이 스레드를 생성하도록 구현합니다. (참고 : linux에서 스레드를 생성하려면 pthread 인터페이스를 사용하고, pthread_create 함수를 호출해 생성합니다. )

  • tid : 새로 생성된 스레드의 id
  • attr : 설정해야하는 스레드 속성
  • thread_entry_point : 새 스레드에서 호출 될 함수의 포인터 (Thread 객체의 run() 메서드를 호출하는 함수의 포인터)
  • arg_to_entrypoint : thread_entry_point 에 전달하는 함수에 전달한 인수
pthread_t tid;
if (pthread_create(&tid, &attr, thread_entry_point, arg_to_entrypoint))
{
fprintf(stderr, "Error creating thread\n");
return;
}

자바 스레드 객체의 run() 메서드를 호출하는 thread_entry_point 함수에는 아래와 비슷하게 callRunMethod() 를 통해 자바 스레드 객체의 run() 메서드를 호출하는 로직이 구현되어있습니다.

void *thread_entry_point(void *args)
{
std::cout << "Starting thread_entry_point";

JavaThreadWrapper *javaThreadWrapper = (JavaThreadWrapper*)args;
javaThreadWrapper>callRunMethod();

delete javaThreadWrapper;
return NULL;
}
void JavaThreadWrapper::callRunMethod() {
JNIEnv *env = attachToJvm();
jclass cls = env->GetObjectClass(threadObjectRef);
jmethodID runId = env->GetMethodID(cls, "run", "()V");
if (runId != nullptr) {
env->CallVoidMethod(threadObjectRef, runId);
} else {
cout << "No run method found in the Thread object!!" << endl;
}
env->DeleteGlobalRef(threadObjectRef); //delete global ref before detaching the thread.
}

위코드를 자세히 보신분들은 이미 눈치채셨겠지만, javaThreadWrapper 클래스의 역할은 무엇일까요?

자바 스레드 객체에 접근하는 진입점 함수(thread_entry_point) 는 jvm jni 객체에 대한 접근 권한자바 스레드 객체에 대한 전역 JNI 참조 권한을 가져야만 자바 스레드 객체에 접근이 가능합니다. 이렇게 접근 및 참조 권한을 주기 위해 JavaThreadWrapper 라는 래퍼 객체를 만들어 줍니다.

JNIEXPORT void JNICALL Java_com_threading_Thread_start0(JNIEnv *env, jobject javaThreadObjectRef) {

JavaThreadWrapper* args = new JavaThreadWrapper(env,
javaThreadObjectRef);
...

(아래 참고 : JavaThreadWrapper의 생성자는 jvm 참조에 접근하고, 자바 스레드 객체에 대한 전역 참조를 만듭니다. )

JavaThreadWrapper::JavaThreadWrapper(JNIEnv *env, jobject javaThreadObjectRef) {
env->GetJavaVM(&(this->jvm));
this->threadObjectRef = env->NewGlobalRef(javaThreadObjectRef);
}

--

--

woody
woody

Written by woody

딱 1%가 모든것을 바꾼다

No responses yet