티스토리 뷰

언어/Java

[Java]쓰레드(Thread)에 대하여

꼬마우뇽이(원종운) 2021. 12. 31. 10:00

1편 - 쓰레드(Thread)에 대하여 - 현재 글

2편 - 쓰레드 상태(Thread State)와 메서드에 대하여

3편 - 쓰레드 동기화(Thread Synchronization)에 대하여

4편 - 쓰레드 풀(Thread Pool)에 대하여

5편 - ThreadExecutor에 대하여

6편 - ForkJoinPool에 대하여


쓰레드(Thread)란 ?

프로세스가 처리하는 작업 또는 흐름의 단위입니다. 쓰레드는 프로세스의 메모리 자원(힙 영역) 일부를 공유하므로 공유 자원을 사용할 경우는 각별한 주의가 필요하며 원치 않은 결과가 발생할 수 있으므로 조심하여야 합니다.

 

멀티쓰레딩(Multi Thread)란 ?

여러 쓰레드를 동시에 실행시키는 프로그래밍 기법입니다.

 

장점

1. 프로세스의 메모리 자원의 일부를 공유하여 사용하기 때문에 시스템 자원을 효율적으로 사용할 수 있습니다.

2. 두 가지 이상의 작업을 동시에 수행할 수 있어 처리량을 늘릴 수 있습니다. (무조건 그런 것은 아닙니다.)

3. 멀티 프로세스(Multi Process) 기법보다 비용이 적게 듭니다. 프로세스를 생성하는 것보다 쓰레드를 생성하는 것이 비용이 적게 듭니다. 

 

단점

1. 메모리 자원의 일부를 공유하여 사용하므로 공유 자원에 대해서 원치 않은 결과가 발생할 수 있습니다.

2. 동기화를 고려한 코드 작성을 위하여 코드 작성 난이도가 올라갑니다.

 

쓰레드를 구성하는 방법

Java에서 쓰레드를 구성하는 방법은 크게 첫째 Thread 클래스를 상속하는 방법둘째 Runnable 인터페이스를 전달하는 방법이 있습니다. 각자의 장단점이 있고 어떠한 방법이 맞고 틀리지 않습니다.

 

1. Thread 클래스를 상속

Thread 클래스를 상속받은 후 run 메서드를 오버 라이딩하여 쓰레드를 사용할 수 있습니다. 쓰레드가 시작될 경우 오버 라이딩하여 정의한 run 메서드가 호출됩니다. 추가로 예외가 발생하였을 때 StackTrace를 찍는 것은 좋지 못한 방법이지만 지금의 목적은 쓰레드를 사용하는 방법이므로 넘어가겠습니다.

 

class MyThread extends Thread {

    @Override
    public void run() {
        while(true) {
            try {
                System.out.printf("[%s] - Threading....%n", Thread.currentThread().getName());
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

이 방법의 단점은 Java는 클래스의 다중 상속을 지원하지 않습니다. 그렇기 때문에 쓰레드를 선언하기 위하여 Thread 클래스를 상속받는다면 다른 클래스를 상속받을 수 없습니다. 그 문제를 해결하는 방법은 Runnable 인터페이스를 사용하는 방법입니다.

 

2. Runnable 인터페이스를 전달

Thread 객체를 생성할 때 생성자를 통하여 Runnable 인터페이스 구현체를 전달할 수 있습니다. 해당 인터페이스 구현체는 Thread 클래스를 상속받은 후 오버 라이딩하여 정의한 run 메서드와 동일한 역할을 하게 됩니다.

public class App {
    public static void main(String[] args) {
        Thread myThread = new Thread(() -> {
            while(true) {
                System.out.printf("[%s] - Threading....\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

위 코드에서는 Runnable 인터페이스를 Java 8부터 지원하는 람다를 이용하여 전달하여 Thread 객체를 생성한 모습입니다. 어떻게 보면 Thread 클래스를 상속받는 방법보다 간편해 보이지 않나요.

 

Thread 클래스 상속과 Runnable 인터페이스의 장단점

Thread 클래스 상속의 단점

Java는 클래스의 다중 상속을 지원하지 않습니다. Thread 클래스를 상속받아 쓰레드를 선언한다면 다른 클래스를 상속받지 못합니다.

 

Runnable 인터페이스의 장점

쓰레드에서 실행하고자 하는 메서드를 람다를 사용하여 간단히 전달할 수 있습니다.

쓰레드를 시작하는 방법

Thread 클래스에는 시작과 관련 있어 보이는 run 메서드와 start 메서드가 있습니다. 과연 어떤 메서드가 쓰레드를 시작하여주는 메서드일까요? 만약 둘 다 시작하여준다면 뭐가 다를까요?

 

run 메서드는 무엇일까요?

    /**
     * If this thread was constructed using a separate
     * <code>Runnable</code> run object, then that
     * <code>Runnable</code> object's <code>run</code> method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * Subclasses of <code>Thread</code> should override this method.
     *
     * @see     #start()
     * @see     #stop()
     * @see     #Thread(ThreadGroup, Runnable, String)
     */
    @Override
    public void run() {
        if (target != null) {
            target.run(); // Runnable 객체의 run 메서드 호출
        }
    }

 

run 메서드는 위와 같습니다. 공식 문서에 적힌 설명을 한번 요약을 해보면 다음과 같습니다.

 

첫째, Runnable 인터페이스를 사용하여 쓰레드를 구성한 경우, Runnable 객체의 run 메서드를 호출.

둘째, Runnable 인터페이스를 사용하지 않았다면 아무런 동작을 하지 않고 반환됩니다.

셋째, Thread를 상속하여 쓰레드를 구성한 경우 run 메서드를 오버 라이딩하여야 합니다.

 

해당 메서드를 보면 이상한 점이 느껴지시지 않나요? 쓰레드를 만약 run 메서드로 시작하는 게 옳다면, 쓰레드를 생성해주는 부분은 도대체 어디에 있는 걸까요? 만약 이것이 전부라면 일반적인 객체의 메서드를 호출하는 것과 다를 바가 없습니다.

 

그렇습니다. run 메서드는 쓰레드를 시작하는 메서드가 아니라 Thread 객체 내부에 오버 라이딩된 run 메서드 또는 전달된 Runnable 객체의 run 메서드를 실행하여주는 역할을 합니다. 쓰레드를 생성하여 주지 않습니다.

 

결과 검증

public class App {
    public static void main(String[] args) {
        Thread myThread = new Thread(() -> {
            while(true) {
                System.out.printf("[%s] - Threading....\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        myThread.run(); // run 메서드 호출
    }
}

결과

앞서 알아본 쓰레드를 구성하는 방법으로 구성한 후 500ms 간격으로 쓰레드의 이름을 찍어보았습니다. run 메서드를 호출하여 쓰레드를 시작하게 될 경우 새로운 쓰레드가 생성된 것이 아니라 메인 쓰레드(main)에서 작업이 수행되는 것을 알 수 있었습니다.

 

즉, run 메서드를 호출하여 쓰레드를 시작할 경우 쓰레드를 생성하여 주지 않습니다.

 

start 메서드는 무엇일까요?

/**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    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 메서드는 위와 같습니다. 공식 문서에 적힌 설명을 한번 요약을 해보면 다음과 같습니다.

 

첫째, JVM이 해당 쓰레드의 메인 메서드(run 메서드)를 실행하여 줍니다.

둘째, 그 결과 동시에 두 개의 쓰레드가 동시에 실행됩니다.

셋째, 동일한 쓰레드를 두 번 이상 시작할 수 없습니다. 즉 실행을 완료한 후 재시작할 수 없습니다.

 

run 메서드가 쓰레드를 생성해주지 않는다면 start 메서드는 과연 쓰레드를 생성하여 줄까요? 호출하여 보도록 하죠.

 

결과 검증

run 메서드 호출될 때 사용한 코드와 호출하는 메서드만 다를 뿐 동일한 작업을 수행하는 코드입니다. 

public class App {
    public static void main(String[] args) {
        Thread myThread = new Thread(() -> {
            while(true) {
                System.out.printf("[%s] - Threading....\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        myThread.start(); // start 메서드 호출
    }
}

결과

run 메서드를 호출하였을 때와 결과가 다른 것을 확인할 수 있습니다. 메인 쓰레드(main)에서 작업이 수행되는 것이 아니라 새로운 쓰레드가 생성되어 작업이 수행됨을 알 수 있습니다. 

 

즉, 우리가 작성한 쓰레드가 정상적으로 생성되어 작업을 수행되기 위해서는 run 메서드가 아닌 start 메서드를 호출하여함을 알 수 있습니다.

 

왜 start 메서드에서는 새로운 쓰레드가 생성되는 걸까요?

start0 메서드

start 메서드 내부에서는 start0 메서드를 호출하여줍니다. start0가 어떠한 작업을 해주기 때문에 쓰레드가 생성되는 걸까요?

 

private native void start0();

start0 메서드는 구현부가 존재하지 않은 native 메서드로 구현되어있는 것을 알 수 있습니다. [여기]를 확인하면 start0이 어떠한 역할을 하는지 자세히 알 수 있습니다. 결론을 말하자면 native 메서드는 JNI을 이용하여 구현되며 C로 작성된 네이티브 코드를 사용하여 쓰레드를 실제로 생성한 후, 해당 쓰레드에 정의된 run 메서드를 호출하여줍니다.

 

즉, start0 메서드는 실질적으로 쓰레드를 생성한 후, run 메서드를 호출하여줍니다.

마무리

다음 2편에서는 쓰레드 상태(Thread State)와 메서드에 대하여 알아보도록 하겠습니다.

 

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함