본문 바로가기
Java

[Java] Thread 라이프 사이클과 스레드 우선순위로 보는 데몬스레드와 비데몬 스레드

by 곰민 2023. 3. 1.
728x90

Java Thread(스레드) 라이프사이클(life cycle)을 간단하게 확인하고, Thread(스레드) 우선순위로 데몬스레드(Demon Thread)와 비데몬 스레드(non demon thread or user thread)에 대해서 알아보는 시간을 갖도록 하겠습니다.

 

java Thread
그만패... 그만!

 

Concurrent Programming


Concurrent Programming은 여러 작업이 동시에 실행되는 환경에서 프로그램이 제대로 동작하도록 하는 프로그래밍 기법입니다. 

이러한 환경에서는 다른 작업들과 자원들과의 경합이 발생할 수 있으며, 이를 해결하기 위해 스레드 동기화와 같은 기술이 사용됩니다.

 

Thread


Java에서 프로세스는 컴퓨터 시스템에서 실행되는 프로그램의 인스턴스입니다. 

각 프로세스에는 고유한 메모리 공간, 시스템 리소스 및 환경 변수가 있습니다. 

프로세스는 운영 체제에 의해 관리되며 각 프로세스는 자체적으로 격리된 환경에서 실행됩니다.

반면에 스레드는 프로세스 내의 가벼운 실행 단위입니다. 

스레드는 동일한 프로세스 내의 다른 스레드와 동일한 메모리 공간 및 시스템 리소스를 공유합니다. 
프로세스 내의 여러 스레드가 동시에 실행될 수 있으며, 각 스레드는 독립적으로 동시에 별도의 작업을 수행할 수 있습니다.

 

하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 각 스레드는 독립적으로 실행될 수 있습니다.

이러한 특성으로 인해 멀티태스킹이 가능해지며, 다중 사용자 환경에서 효율적인 작업 처리가 가능해집니다.

 

Java Thread Runable and Callable


Java에서 스레드는 Runnable interface를 구현하거나 Thread 클래스를 상속 하여 생성할 수 있습니다. 

Runnable interface는 스레드에서 실행할 코드가 포함된 단일 run method를 정의합니다. 

반면에 Thread 클래스를 상속하면 run 메서드를 직접 재정의할 수 있습니다.



또한 Java는 Runnable과 유사하지만 스레드가 결과를 반환하고 예외를 던질 수 있는 Callable interface를 제공합니다.

Callable interface는 스레드에서 실행할 코드가 포함된 단일 call method를 정의하고 지정된 유형의 결과를 반환합니다.

스레드를 시작하려면 스레드 클래스의 인스턴스를 생성하고 생성자에 Runnable 또는 Callable 인스턴스를 전달한 다음 start 메서드를 호출하면 됩니다.

그러면 새 스레드가 시작되고 사용된 인터페이스에 따라 run 또는 call 메서드가 호출됩니다.

 

예를 들어 Spring Batch와 같이 대량의 정보를 처리하는 프레임워크는 데이터를 관리하기 위해 스레드를 사용합니다.

스레드 또는 CPU 프로세스를 동시에 조작하면 성능이 향상되어 더 빠르고 효율적인 프로그램을 만들 수 있습니다.

사실 우리에게 스레드는 익숙합니다.
Java의 main() 메서드에는 메인 스레드가 포함되어 있기 때문에 Java 스레드로 직접 작업해 본 적이 없더라도 간접적으로 작업해 본 적이 있을 것입니다. 

main() 메서드를 실행한 적이 있다면 메인 스레드도 실행한 것입니다.

다음과 같이 currentThread().getName() 메서드를 호출하여 실행 중인 스레드에 액세스 할 수 있습니다

 

@Slf4j
public class MainThread {

    public static void main(String... mainThread) {
        log.info(Thread.currentThread().getName());
    }

}

 

현재 실행중인 스레드가 무엇인지 알아보는 것부터 시작해 봅시다.

 

[main] INFO com.javafeature.demo.thread.MainThread - main

 

이 코드는 현재 실행 중인 스레드를 식별하는 "main"을 출력합니다.

 

Java Thread Life - Cycle


스레드로 작업할 때는 스레드 상태를 파악하는 것이 중요합니다.
스레드의 Life-Cycle에 대해 간단하게 알아보도록 하겠습니다.

  1. NEW – 아직 실행을 시작하지 않은 새로 생성된 스레드입니다.
  2. RUNNABLE – 스레드의 start() 메서드가 호출되었고 실행 중이거나 실행 준비가 완료되었지만 리소스 할당을 기다리는 중입니다.
  3. RUNNING : start() 메서드가 호출되었고 스레드가 실행 중입니다.
  4. SUSPENDED : 스레드가 일시적으로 일시 중단되었으며 다른 스레드에서 다시 시작할 수 있습니다.
  5. BLOCKED – synchronized block/method 에 들어가거나 다시 들어가기 위해 모니터 잠금을 획득하기 위해 대기 중입니다.
  6. WAITING –시간제한 없이 다른 스레드가 특정 작업을 수행하기를 기다리는 중입니다.
  7. TIMED_WAITING – 다른 스레드가 지정된 기간 동안 특정 작업을 수행하기를 기다리는 중입니다.
  8. TERMINATED – 실행을 완료했습니다.

출처:https://user-images.githubusercontent.com/61622657/222007709-1dc7dadd-3ae9-47fa-800d-eec3a97f747b.jpg



Concurrent processing


가장 간단한 동시 처리는 아래 그림과 같이 Thread 클래스를 상속하여 수행합니다.

 

@Slf4j
public class InheritingThread extends Thread{
    InheritingThread(String threadName) {
        super(threadName);
    }

    public static void main(String... inheriting) {
        log.info(Thread.currentThread().getName() + " is running");

        new InheritingThread("inheritingThread").start();
    }

    @Override
    public void run() {
        log.info(Thread.currentThread().getName() + " is running");
    }
}

 

여기서는 두 개의 스레드, 즉 메인 스레드와 상속 스레드를 실행하고 있습니다. 

새로운 inheritingThread()를 사용하여 start() 메서드를 호출하면 run() 메서드의 로직이 실행됩니다.

또한 Thread 클래스 생성자에서 두 번째 스레드의 이름을 전달하므로 출력은 다음과 같습니다

 

[main] INFO com.javafeature.demo.thread.InheritingThread - main is running
[inheritingThread] INFO com.javafeature.demo.thread.InheritingThread - inheritingThread is running

 

Runnable Interface

상속을 사용하는 대신 Runnable Interface를 구현할 수 있습니다.

Thread 생성자 안에 Runnable을 전달하면 결합이 줄어들고 유연성이 향상됩니다.

Runnable을 전달한 후에는 이전 예제에서와 똑같이 start() 메서드를 호출할 수 있습니다.

 

@Slf4j
public class RunnableThread implements Runnable {

    public static void main(String... runnableThread) {
        log.info(Thread.currentThread().getName());

        new Thread(new RunnableThread()).start();
    }

    @Override
    public void run() {
        log.info(Thread.currentThread().getName());
    }
}


비데몬 스레드(non-demon-thread)와 데몬 스레드(demon-thread)



실행 측면에서 스레드에는 두 가지 유형이 있습니다

 

비데몬 스레드 또는 User Thread

비데몬 스레드는 끝까지 실행됩니다. 

JVM은 사용자 스레드가 작업을 완료할 때까지 기다립니다. 

모든 사용자 스레드가 작업을 완료할 때까지 종료하지 않습니다.

메인 스레드가 비데몬 스레드의 좋은 예입니다. 

main()의 코드는 System.exit()가 프로그램을 강제로 종료하지 않는 한 항상 끝까지 실행됩니다.

 

데몬 스레드

데몬 스레드는 그 반대이며, JVM은 데몬 스레드가 작업을 완료할 때까지 기다리지 않습니다. 

모든 사용자 스레드가 작업을 완료하는 즉시 JVM이 종료됩니다.

 

데몬 스레드가 아닌 스레드가 데몬 스레드보다 먼저 종료되면 데몬 스레드는 끝까지 실행되지 않습니다.

 

@Slf4j
public class DemonThread {
    public static void main(String... nonDaemonAndDaemon) throws InterruptedException {
        log.info("Starting the execution in the Thread " + Thread.currentThread().getName());

        Thread daemonThread = new Thread(() -> IntStream.rangeClosed(1, 100000)
            .forEach(i -> log.info(Integer.toString(i))));

        daemonThread.setDaemon(true);
        daemonThread.start();

        Thread.sleep(10);

        log.info("End of the execution in the Thread " +
            Thread.currentThread().getName());
    }
}

 

데몬 스레드가 아닌 메인 스레드가 먼저 완료되면 데몬 스레드가 실행을 완료하지 못합니다.

출력
1. 메인 스레드에서 실행 시작.
2. 1부터 100,000까지 숫자를 출력합니다.
3. 메인 스레드에서 실행 종료, 100,000까지 반복이 완료되기 전일 가능성이 매우 높음.
최종 출력은 JVM 구현에 따라 달라집니다.

포인트는 스레드는 예측할 수 없다는 것입니다.

 

 


스레드 우선순위와 JVM



setPriority 메서드를 사용하여 스레드 실행 우선순위를 지정할 수 있지만, 처리 방법은 JVM 구현에 따라 다릅니다.

Linux, MacOS 및 Windows는 모두 서로 다른 JVM 구현을 가지고 있으며, 각 구현은 자체 기본값에 따라 스레드 우선순위를 처리합니다.
그러나 설정한 스레드 우선순위는 스레드 호출 순서에 영향을 미칩니다.

Thread 클래스에 선언된 세 가지 상수는 다음과 같습니다

 

     /**
    * The minimum priority that a thread can have.
     */
    public static final int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public static final int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public static final int MAX_PRIORITY = 10;

다음 코드에서 몇 가지 테스트를 실행하겠습니다.

 

@Slf4j
public class ThreadPriority {

    public static void main(String... threadPriority) {
        Thread lionThread = new Thread(() -> log.info("lion"));
        Thread tigerThread = new Thread(() -> log.info("tiger"));
        Thread dragonThread = new Thread(() -> log.info("dragon"));

        lionThread.setPriority(Thread.MIN_PRIORITY);
        tigerThread.setPriority(Thread.NORM_PRIORITY);
        dragonThread.setPriority(Thread.MAX_PRIORITY);

        lionThread.start();
        tigerThread.start();
        dragonThread.start();
    }
}

 

moeThread를 MAX_PRIORITY로 설정하더라도 이 스레드가 먼저 실행될 것이라고 기대할 수는 없습니다. 

 

실행 순서는 무작위입니다.

 

public class TestCelebrityThreads {


    private static final Logger log = LoggerFactory.getLogger(TestCelebrityThreads.class);

    private static int testline = 10;

    @Test
    public void testCelebrityThreads() throws InterruptedException {
        new Celebrity("Tom Cruise").start();

        Celebrity famousActor = new Celebrity("Dwayne Johnson");
        famousActor.setPriority(Thread.MAX_PRIORITY);
        famousActor.setDaemon(false);
        famousActor.start();

        Celebrity famousSinger = new Celebrity("Adele");
        famousSinger.setPriority(Thread.MIN_PRIORITY);
        famousSinger.start();

        Thread.sleep(1000);

        log.info("Wolverine adrenaline: {}", testline );
    }

    static class Celebrity extends Thread {
        Celebrity(String name) { super(name); }

        @Override
        public void run() {
            testline ++;
            if (testline  == 13) {
                log.info(this.getName());
                }
            }
        }
}

위의 코드에서는 3개의 스레드를 생성했습니다. 

첫 번째 스레드는 Tom Cruise이며, 이 스레드에 기본 우선순위를 할당했습니다. 두 번째 스레드는 Dwayne Johnson이며 MAX_PRIORITY가 할당되었습니다.

세 번째 스레드는 Adele로 MIN_PRIORITY를 할당했습니다.

그런 다음 스레드를 시작했습니다.

스레드가 실행될 순서를 결정하기 위해 먼저 Celebrity 클래스가 Thread 클래스를 상속하고 생성자에서 스레드 이름을 전달한 것을 확인할 수 있습니다.

또한 조건으로 run() 메서드를 재정의했습니다.

testline이 13과 같으면.

Adele이 실행 순서에서 세 번째 스레드이고 MIN_PRIORITY를 갖지만, 모든 JVM 구현에서 마지막에 실행된다는 보장은 없습니다.

또한 이 예제에서는 Dwayne Johnson 스레드를 데몬으로 설정했다는 점에 주목할 수 있습니다.

데몬 스레드이기 때문에 Dwayne Johnson이 실행을 완료하지 못할 수도 있습니다.

 

그러나 다른 두 스레드는 기본적으로 데몬이 아니므로 Tom Cruise 및 Adele 스레드는 확실히 실행을 완료할 것입니다.
결론적으로, 스레드 스케줄러가 실행 순서나 스레드 우선순위를 따를 것이라는 보장이 없기 때문에 결과는 불확실합니다.
프로그램 로직(스레드 순서 또는 스레드 우선순위)에 의존하여 JVM의 실행 순서를 예측할 수 없습니다.

주의해야 할 실수


run() 메서드를 호출하여 새 스레드를 시작하려고 시도하는 경우.
스레드를 두 번 시작하려고 시도하는 경우(이로 인해 IllegalThreadStateException이 발생함).
객체의 상태가 변경되지 않아야 하는데 여러 프로세스가 객체의 상태를 변경하도록 허용하는 경우.
스레드 우선순위에 의존하는 프로그램 로직을 작성하는 경우(예측할 수 없음).
스레드 실행 순서에 의존하는 경우 - 스레드를 먼저 시작하더라도 스레드가 먼저 실행된다는 보장이 없습니다.

 

스레드 포인트


start() 메서드를 호출하여 스레드를 시작합니다.
스레드를 사용하기 위해 Thread 클래스를 직접 상속 할 수 있다.
실행 가능한 인터페이스 내에서 스레드 작업을 구현할 수 있다.
스레드 우선순위는 JVM 구현에 따라 달라진다.
스레드 동작은 항상 JVM 구현에 따라 달라진다.
데몬 스레드를 둘러싸고 있는 데몬이 아닌 스레드가 먼저 종료되면 데몬 스레드가 완료되지 않는다.

 

참조


https://www.geeksforgeeks.org/difference-between-daemon-threads-and-user-threads-in-java/

https://www.baeldung.com/java-thread-lifecycle

https://www.infoworld.com/article/3336222/java-challengers-6-thread-behavior-in-the-jvm.html

 

728x90

댓글