Thread 클래스를 상속하는 것보다 Runnable 인터페이스를 구현해야 하는 이유
kindof
·2023. 5. 9. 22:24
이번 글에서는 자바에서 쓰레드를 구현하는 가장 핵심적인 두 가지 Thread 클래스와 Runnable 인터페이스에 대해 간략하게 소개하려고 합니다.
먼저 쓰레드의 생명 주기를 간략하게 보고, 왜 Runnable 인터페이스를 Implements 하는 방식의 구현이 Thread 클래스를 상속해서 구현하는 방식보다 나은지 그 이유를 적어보겠습니다.
1. 쓰레드의 생명 주기
쓰레드는 크게 아래와 같이 5가지 상태의 생명 주기를 갖습니다.
- New
- 쓰레드가 만들어진 상태로 아직 start() 메소드가 호출되지 않은 상태입니다.
- Runnable (실행대기)
- 쓰레드가 실행되기 위한 준비 단계입니다.
- 코드 상에서 start() 메소드를 호출하면 run() 메소드에 설정된 쓰레드가 Runnable 상태로 진입합니다.
- Running (실행상태)
- CPU를 점유하여 실행하고 있는 상태이며 run() 메서드는 JVM만이 호출 가능합니다.
- Runnable(준비상태)에 있는 여러 쓰레드 중 우선 순위를 가진 쓰레드가 결정되면 JVM이 자동으로 run() 메소드를 호출하여 쓰레드가 Running 상태로 진입합니다
- Blocked (일시 정지)
- 사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태입니다.
- wait() 메소드에 의해 Blocked 상태가 된 쓰레드는 notify() 메소드가 호출되면 Runnable 상태로 돌아갑니다.
- sleep(시간) 메소드에 의해 Blocked 상태가 된 쓰레드는 지정된 시간이 지나면 Runnable 상태로 돌아갑니다.
- Terminated (실행종료)
- Running 상태에서 쓰레드가 모두 실행되고 난 후 완료 상태로, run() 메소드 완료시 쓰레드가 종료되며, 그 쓰레드는 다시 시작할 수 없게 됩니다.
여기서 가장 중요한 포인트는 쓰레드의 Runnable 상태와 Running 상태입니다.
결국 '각 쓰레드가 언제 실행 가능한 상태가 되며, 어떻게 Running 상태로 전환되고, Running 상태에서 어떤 일을 하는가'가 멀티 쓰레드를 사용하는 핵심 과제이기 때문입니다(주관적인 생각).
2. Java Multithread - Thread 클래스
먼저 jdk 1.0부터 제공된 가장 기본적인 Thread 클래스를 보겠습니다.
[Thread - java.lang]
public class Thread implements Runnable {
...
/* What will be run. */
private Runnable target;
...
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 */
}
}
}
private native void start0();
/**
* If this thread was constructed using a separate
* {@code Runnable} run object, then that
* {@code Runnable} object's {@code run} method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of {@code Thread} should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
start() 메서드를 수행하면 native 메서드인 start0() 를 통해 쓰레드를 스케쥴러에 포함시킬 수 있습니다.
스케쥴러에 포함된 쓰레드는 쓰레드 큐 내에서 runnable로 대기하다가 수행 차례가 되면 run() 메서드를 호출하여 동작을 수행합니다.
그러면, Thread 클래스를 상속해서 커스텀 클래스를 하나 작성해보겠습니다.
[MyThread.java]
public class MyThread extends Thread {
int threadNum;
public MyThread(int threadNum) {
this.threadNum = threadNum;
}
@Override
public void run() {
System.out.println(this.threadNum + " thread start."); // 쓰레드 시작
try {
Thread.sleep(1000); // 1초 대기한다.
} catch (Exception ignored) {
}
System.out.println(this.seq + " thread end."); // 쓰레드 종료
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) { // 총 10개의 쓰레드를 생성하여 실행한다.
Thread t = new MyThread(i);
t.start();
}
System.out.println("main end."); // main 메서드 종료
}
}
######## 실행 결과 ###########
3 thread start.
8 thread start.
6 thread start.
0 thread start.
1 thread start.
5 thread start.
4 thread start.
2 thread start.
7 thread start.
main end.
9 thread start.
4 thread end.
7 thread end.
0 thread end.
1 thread end.
5 thread end.
2 thread end.
8 thread end.
6 thread end.
3 thread end.
9 thread end.
MyThread 클래스가 Thread 클래스를 상속했습니다. 그래서 Thread 클래스의 run() 메서드를 오버라이딩하면 위 예제와 같이 start() 수행 시 MySample 객체의 run() 메서드가 수행됩니다.
그런데 만약 MyThread 클래스에서 run() 메서드를 오버라이딩하지 않으면 어떻게 될까요?
그러면 MyThread의 부모인 Thread 객체의 run() 메서드가 실행되고, 부모 Thread는 현재 Runnable을 주입받지 않았기 때문에 결국 아무 일도 일어나지 않습니다.
즉, run() 메서드를 오버라이딩하지 않으면 아무 의미없는 쓰레드 상속 클래스가 됨에도 불구하고 컴파일 에러는 발생하지 않습니다. 뿐만 아니라, 부모인 Thread 클래스가 정의한 메서드들을 재사용할 가능성도 거의 없는 상황입니다.
3. Java Multithread - Runnable 인터페이스
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Runnable 인터페이스는 run() 메서드 하나만 정의하고 있는 함수형 인터페이스입니다.
위의 Thread 클래스를 상속하는 방식 대신에 Runnable 인터페이스를 Implements 하는 방식이 더 나은데요.
그 이유는 크게 세 가지 정도가 있을 수 있다고 생각합니다.
첫째, Java에서는 다중 상속이 불가능하기 때문에 인터페이스를 Implements 함으로써 다른 클래스를 상속받을 수 있다.
둘째, run() 메서드만 재정의하는 것이 중요하고 이 경우 Runnable 인터페이스가 오버라이딩을 강제한다.
셋째, 쓰레드와 실행할 코드를 분리하여 재사용성을 높일 수 있다.
따라서, Java에서 쓰레드를 구현할 때는 대부분 Runnable을 Implements하여 아래와 같이 사용하게 됩니다.
public class MyRunnable implements Runnable {
public void run() {
// 스레드에서 실행할 코드
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 스레드 시작
}
}
4. 정리
짧은 글이었지만 자바 멀티쓰레드의 가장 끝 단에 있는 Thread 클래스와 Runnable 인터페이스에 대해 살펴봤습니다.
그리고 많은 코드들을 보다보면 Thread를 상속하고 있는 코드를 작성하는 코드를 만나지 못했는지... Runnable 인터페이스를 구현해야 하는 이유에 대해 나름대로 정리해봤습니다.
감사합니다!
'Java & Kotlin' 카테고리의 다른 글
ExecutorService? Thread Pool Size는 어떻게 정해야할까 (1) | 2023.08.06 |
---|---|
내가 이해한 Garbage Collection (0) | 2023.06.05 |
인터페이스의 메서드가 각기 다른 리턴 타입과 파라미터를 필요로 한다면? (0) | 2023.05.08 |
[Java] Thread 안에서 발생하는 예외는 어떻게 처리되나 (0) | 2023.03.31 |
[JAVA] JDK 17에서 제공하는 새로운 기능들 정리해보기 (0) | 2022.11.18 |