ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Thread와 Runnable
    Java 2023. 4. 11. 20:28

     

    쓰레드와 자바의 멀티 쓰레드

     

    쓰레드란 프로그램 실행의 가장 작은 단위이다. 

    자바 애플리케이션을 만들어 실행하면 1개의 메인 쓰레드에 의해 프로그램이 실행된다. 하지만 1개의 쓰레드 만으로는 동시에 여러가지 작업을 할 수 없다. 동시에 여러 작업을 처리하고 싶다면, 별도의 쓰레드를 만들어 실행시켜주어야 한다.

    자바는 멀티 쓰레드 기반으로 동시성 프로그래밍을 지원하기 위한 방법들을 계속해서 발전시켜 왔다.

     

    쓰레드를 생성하는 방법은 크게 두 가지로 나눌 수 있다.

    1. Thread클래스를 상속받아 run() 메소드를 오버라이딩

    2. Runnable 인터페이스 구현 객체를 생성한 후 Thread 생성자로 Runnable 객체 전달

     

    2가지 방법 모두 run() 메서드를 재정의하고 있고, 결과적으로 Thread 객체를 생성한다.

    이렇게 생성한 쓰레드를 실행하는 방법은 Thread 객체 내의 start() 메서드를 호출하는 것이다. 

     

     

    Thread 클래스

    Thread는 쓰레드 생성을 위해 Java에서 미리 구현해둔 클래스이다. Thread는 기본적으로 다음과 같은 메소드들을 제공한다.

    • sleep
      • 현재 쓰레드 멈추기
      • 자원을 놓아주지는 않고, 제어권을 넘겨주므로 데드락이 발생할 수 있다.
    • interrupt
      • 다른 쓰레드를 깨워서 interruptedException을 발생시킨다.
      • interrupt가 발생한 쓰레드는 예외를 catch하여 다른 작업을 할 수 있다.
    • join
      • 다른 쓰레드의 작업이 끝날 때 까지 기다리게 한다.
      • 쓰레드의 순서를 제어할 때 사용할 수 있다.

     

    Thread 클래스로 쓰레드를 구현하려면 이를 상속받는 클래스를 만들고, 내부에서 run 메소드를 구현해야 한다. 

    그리고 Thread의 start 메소드를 호출하면 run 메소드가 실행된다.

    실행 결과를 보면 main 쓰레드가 아닌 별도의 쓰레드에서 실행됨을 알 수 있다.

     

    @Test
    void threadStart() {
        Thread thread = new MyThread();
    
        thread.start();
        System.out.println("Hello: " + Thread.currentThread().getName());
    }
    
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Thread: " + Thread.currentThread().getName());
        }
    }
    
    // 출력 결과
    // Hello: main
    // Thread: Thread-2

    여기서 run을 직접 호출하는 것이 아니라 start를 호출하는 것에 주의해야 한다. 

    run()을 호출하는 것은 생성된 스레드 객체를 생성하는 것이 아니라, 단순히 스레드 클래스 내부의 run 메서드를 실행시키는 것이다

    즉, main 함수의 스레드를 그대로 사용해서 run 메서드를 실행하기 때문에 새로운 스레드가 생기지 않고 병렬처리를 할 수 없다.

     

    반면에 start()는 새로운 스레드를 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run을 호출해서, 생성된 호출 스택에 run()이 첫 번째로 저장되게 한다.

     

    쉽개 말하면, start()를 호출하면 스레드를 새롭게 생성해서 해당 스레드를 runnable한 상태로 만든 후 run() 메서드를 실행하게 된다.

    따라서 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) {
            
            }
        }
    }

     

    start는 다음과 같은 과정으로 진행된다.

     

    1.  쓰레드가 실행 가능한지 검사한다.

    쓰레드는 New, Runnable, Waiting, Timed Waiting, Terminated 총 5가지 상태가 있다. start 가장 처음에는 해당 쓰레드가 실행 가능한 상태인지(0인지) 확인한다. 그리고 만약 쓰레드가 New(0) 상태가 아니라면 IllegalThreadStateException 예외를 발생시킨다.

     

    2. 쓰레드를 쓰레드 그룹에 추가한다.

    그 다음 쓰레드 그룹에 해당 쓰레드를 추가시킨다. 여기서 쓰레드 그룹이란 서로 관련있는 쓰레드를 하나의 그룹으로 묶어 다루기 위한 장치인데, 자바에서는 ThreadGroup 클래스를 제공한다. 쓰레드 그룹에 해당 쓰레드를 추가하면 쓰레드 그룹에 실행 준비된 쓰레드가 있음을  알려주고, 관련 작업들이 내부적으로 진행된다.

     

     

    3. 쓰레드를 JVM이 실행시킨다.

    그리고 start0 메소드를 호출하는데, 이것은 native 메소드로 선언되어 있다. 이것은 JVM에 의해 호출되는데, 이것이 내부적으로 run을 호출하는 것이다. 그리고 쓰레드의 상태 역시 Runnable로 바뀌게 된다. 그래서 start는 여러 번 호출하는 것이 불가능하고 1번만 가능하다.

    private native void start0();

     

    만약 다음과 같이 run을 직접 호출하면 새롭게 쓰레드가 만들어지지 않고, 메인 쓰레드에 의해 해당 메소드가 실행됨을 확인할 수 있다. 또한 여러 번 실행해도 아무런 문제가 없다. 그리고 출력 결과를 보면 main 메소드에 의해 실행됨을 실제로 확인할 수 있다.

    @Test
    void threadRun() {
        Thread thread = new MyThread();
    
        thread.run();
        thread.run();
        thread.run();
        System.out.println("Hello: " + Thread.currentThread().getName());
    }
    
    // 출력 결과
    // Thread: main
    // Thread: main
    // Thread: main
    // Hello: main

     

     

    Runnable 인터페이스

    Runnbale 인터페이스는 1개의 메소드 만을 갖는 함수형 인터페이스이다.

    @FunctionalInterface
    public interface Runnable {
    
        public abstract void run();
        
    }

     

    기존에 Thread로 작성되었던 코드를 Runnable로 변경하면 다음과 같다. 마찬가지로 별도의 쓰레드에서 실행됨을 확인할 수 있다.

    @Test
    void runnable() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread: " + Thread.currentThread().getName());
            }
        };
    
        Thread thread = new Thread(runnable);
        thread.start();
        System.out.println("Hello: " + Thread.currentThread().getName());
    }
    
    // 출력 결과
    // Hello: main
    // Thread: Thread-1

     

    Thead와 Runnable 비교

    Runnable은 익명 객체 및 람다로 사용할 수 있지만, Thread는 별도의 클래스를 만들어야 한다는 점에서 번거롭다. 

     

    또한 자바는 다중 상속을 허용하지 않기 때문에 Thread 클래스를 바로 상속받는 경우 다른 클래스를 상속받지 못한다. 하지만 Runnable 인터페이스를 구현한 경우에는 다른 인터페이스를 추가로 구현할 수 있을 뿐만 아니라, 다른 클래스도 상속받을 수 있다.

     

    또한 Thread 클래스를 상속받으면 Thread 클래스에 구현된 코드들에 의해 더 많은 자원(메모리와 시간 등)을 필요로 하므로 Runnable이 주로 사용된다. 

     

    물론 Thread 관련 기능의 확장이 필요한 경우에는 Thread 클래스를 상속받아 구현해야 할 때도 있다. 하지만 거의 대부분의 경우에는  Runnable 인터페이스를 사용하면 해결 가능하다.

     

     

     

    참고

    https://mangkyu.tistory.com/258

Designed by Tistory.