과정을 즐기자

자바 쓰레드 관리의 시작 본문

Java

자바 쓰레드 관리의 시작

320Hwany 2023. 8. 18. 11:33

OS 쓰레드와 유저 쓰레드

자바 쓰레드 관리에 대해 알아보기 전에 OS 쓰레드와 유저 쓰레드에 대해 먼저 알아보겠습니다.

OS 쓰레드는 OS 커널 레벨에서 생성되고 관리되는 쓰레드로 CPU 에서 실제로 실행되는 단위로 CPU 스케줄링의 단위입니다.

OS 쓰레드의 컨텍스트 스위칭은 커널이 개입하고 비용이 발생하게 됩니다.

유저 쓰레드는 OS 커널이 아닌 User Program과 관련되어 있는 쓰레드로 쓰레드 개념을 프로그래밍 레벨에서 추상화한 것입니다.

이때 유저 쓰레드가 CPU에서 실행되려면 OS 쓰레드와 반드시 연결되어야 합니다.

 

유저 쓰레드와 OS 쓰레드를 연결하는 모델로는 One-to-One, Many-to-One, Many-to-Many 모델이 있습니다.

자바의 초창기는 Many-to-One 모델이었습니다.

Many-to-One 모델은 컨텍스트 스위칭이 커널이 전혀 개입하지 않기 때문에 훨씬 더 빠르게 진행된다는 장점이 있습니다.

하지만 OS 쓰레드가 1개이기 때문에 CPU 코어가 많아져도 활용을 하지 못하고 한 쓰레드가 Blocking 되면 다른 쓰레드도

잘 동작하지 않는다는 단점이 있습니다.

이에비해 One-to-One 모델은 유저 쓰레드와 OS 쓰레드가 1대1로 연결되어 쓰레드 관리를 OS에 위임합니다.

그래서 CPU 코어가 많아지면 이를 잘 활용할 수 있다는 장점이 있습니다. 

또한 한 쓰레드가 Blocking 되어도 다른 쓰레드는 잘 동작할 수 있습니다.  

따라서 자바는 쓰레드 모델을 One-to-One 모델로 변경하고 현재까지 사용하고 있습니다. 

자바의 멀티 쓰레드

자바 애플리케이션을 만들어 실행하면 1개의 main 쓰레드에 의해 프로그램이 실행됩니다. 

하지만 1개의 쓰레드로는 여러 작업을 동시에 실행할 수 없습니다.

자바는 멀티 쓰레드 기반의 동시성 프로그래밍을 지원하기 위해  자바 5 이전의 자바 초기에 Thread, Runnable 을 사용했습니다. 

Thread

public class CustomThread extends Thread {

    @Override
    public void run() {
        System.out.println("Thread : " + Thread.currentThread().getName());
    }
}

Thread를 활용하기 위해 CustomThread를 만들고 Thread를 상속 받아 run() 메소드를 오버라이딩 해줍니다.  

 

@Test
void test1() {
    // given
    Thread thread = new CustomThread();

    // when
    thread.start();

    // then
    System.out.println("Thread: " + Thread.currentThread().getName());
}

start() 메소드를 실행하면 위에서 오버라이딩한 run() 메소드를 실행합니다.

실행 결과를 살펴보면 서로 다른 쓰레드에서 출력이 실행되었음을 확인할 수 있습니다.

이때 위 테스트에서는 main 쓰레드가 항상 먼저 출력되는데 이에 대한 이유를 찾기 위해 Thread의 start() 메소드에

들어가보겠습니다.

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

 

start() 메소드는 먼저 쓰레드가 실행 가능한 상태인지 확인합니다.

주석에 다음과 같이 나와 있습니다. A zero status value corresponds to state "NEW"

즉 쓰레드의 상태 중 NEW 상태가 아니면 예외가 발생합니다.  

쓰레드 상태가 new 상태일 때만 start 메소드를 실행할 수 있습니다.  

 

실행 가능한 상태인지 확인했으면 쓰레드를 쓰레드 그룹에 추가합니다. 

그 다음으로 JVM이 native 메소드인 start0() 메소드를 실행하는데 start0() 는 JNI 라는 기술을 통해서 OS의 시스템 콜

호출합니다.

private native void start0();

이 과정을 통해 유저 쓰레드와 OS 쓰레드가 1대1로 연결이 되는 것입니다.

아까 main 쓰레드의 결과가 더 먼저 출력된 이유는 위와 같은 과정을 통해 쓰레드가 생성되는 시간이 걸리기 때문입니다.

Runnable

Runnable 인터페이스는 run() 이라는 1개의 인터페이스만 갖는 함수형 인터페이스입니다.

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

    Thread thread = new Thread(runnable);

    // when
    thread.start();

    // then
    System.out.println("Thread: " + Thread.currentThread().getName());
}

Thread와 달리 익명 클래스를 사용하여 만들 수 있으며 더 간결하게 람다로 표현할 수도 있습니다.

@Test
void test3() {
    // given
    Runnable runnable = 
   		() -> System.out.println("Thread: " + Thread.currentThread().getName());

    Thread thread = new Thread(runnable);

    // when
    thread.start();

    // then
    System.out.println("Thread: " + Thread.currentThread().getName());
}

Runnable은 Thread에 비해 자원 사용량이 더 적고 상속도 필요하지 않으며 람다를 사용할 수 있다는 장점이 있습니다.

정리

지금까지 자바 초기의 쓰레드 관리에 대해 알아보았습니다.

1개의 쓰레드로만 실행하지 않고 멀티 쓰레드를 이용해 동시성 프로그래밍을 가능하게 했습니다.

하지만 Thread, Runnable은 매번 쓰레드의 생성과 종료를 직접해야 하며 쓰레드 관리가 어렵습니다.  

또한 쓰레드를 실행만 할 수 있을 뿐 결과 값을 반환할 수는 없습니다.

이러한 단점들이 명확했기 때문에 자바는 쓰레드 관리를 발전시켜 왔습니다.

다음 글에서는 자바 5 이후 쓰레드 관리의 발전에 대해 알아보겠습니다. 

 

참고한 자료

 

[Java] Thread와 Runnable에 대한 이해 및 사용법

이번에는 자바 초기부터 멀티 쓰레드 기반의 동시성 프로그래밍을 위해 만들어졌던 Thread와 Runnable를 살펴보도록 하겠습니다. 1. Thread와 Runnable에 대한 이해 및 사용법 [ 쓰레드와 자바의 멀티 쓰

mangkyu.tistory.com

 

[Java/자바] - Thread(쓰레드)의 생명주기

쓰레드에 대한 설명은 여기 클릭! Runnable 상태: 쓰레드가 실행되기 위한 준비 단계 Running 상태: 스케...

blog.naver.com