오늘은 개발하면서 많은 고민을 던져줬던 자바 쓰레드 이야기를 해보려 한다.
쓰레드를 설명하기 앞서 프로세스에 대해서 짚고 넘어가야 한다.
프로그램을 실행하면 실행에 필요한 자원을 할당 받아 프로그램이 실행된다.
실행되어 동작하고 있는 프로그램을 프로세스(Process)라고 한다.
💡 프로세스 = 데이터 + 자원 + 쓰레드
보통 하나의 프로세스는 하나의 작업을 하지만,
여러 가지 작업을 동시에 하기 위해 쓰레드를 활용한다.
쓰레드(Thread)란 프로세스의 실행 단위로 한 프로세스 내에서 동작되는 여러 실행의 흐름으로 프로세스 내의 주소 공간이나 자원을 공유할 수 있다.
자바에서는 Thread 클래스, Runnable 인터페이스 두 가지 방법을 통해 쓰레드를 구현할 수 있다.
먼저 Thread 클래스 예제를 작성해보았다.
public class ThreadExample extends Thread {
public void run() {
System.out.println("thread is run, thread name: "+this.getName());
}
public static void main(String[] m) {
ThreadExample threadExample = new ThreadExample();
threadExample.setName("threadExample");
threadExample.start();
}
}
thread is run, thread name: threadExample
예제를 보면 ThreadExample 클래스가 Thread 클래스를 상속하였고 Thread 클래스의 run 메소드를 구현하면 ThreadExample.start() 실행시 ThreadExample 객체의 run메소드가 수행된다.
run을 구현하였는데 왜 start로 쓰레드를 실행시키나?
Thread 클래스를 상속받았기에 start 실행시 run 메소드가 수행된다.
Thread 클래스는 start 실행 시 run 메소드가 수행되도록 내부적으로 동작한다.
그런데 우리의 실무 개발환경은 쓰레드 하나만 쓰진 않을 거라고 생각한다.
쓰레드의 동작을 확인할 수 있게 여러 개 쓰레드(멀티쓰레드)를 실행해보자.
public class ThreadExample extends Thread {
public void run() {
System.out.println("thread is run, thread name: "+this.getName());
}
public static void main(String[] m) {
ThreadExample t1 = new ThreadExample();
ThreadExample t2 = new ThreadExample();
ThreadExample t3 = new ThreadExample();
t1.setName("111");
t2.setName("222");
t3.setName("333");
t1.start();
t2.start();
t3.start();
}
}
thread is run, thread name: 333
thread is run, thread name: 111
thread is run, thread name: 222
출력을 보면 멀티쓰레드 환경이기에 쓰레드가 동시다발적으로 생성된다.
위의 예제처럼 Thread를 상속하여 구현하면 다른 클래스를 상속할 수 없기 때문에
보통은 Runnable 인터페이스를 구현하도록 하는 방법을 주로 사용한다고 한다.
자바는 다중상속이 불가능한 특성이 존재하기에 Runnable 인터페이스로 구현한다면 다른 클래스를 상속받아서 재사용성을 높일 수 있다.
Runnable interface는 Thread class와 다르게
Runnable class를 구현(implements)하는 방식이다.
위에서 작성한 Thread class 예제를 Runnable interface를 사용하는 방식으로 수정해보자.
public class RunnableExample implements Runnable {
public void run() {
System.out.println("thread is run, thread name: " + Thread.currentThread().getName());
}
public static void main(String[] m) {
Thread t1 = new Thread(new RunnableExample());
Thread t2 = new Thread(new RunnableExample());
Thread t3 = new Thread(new RunnableExample());
t1.setName("111");
t2.setName("222");
t3.setName("333");
t1.start();
t2.start();
t3.start();
}
}
thread is run, thread name: 111
thread is run, thread name: 333
thread is run, thread name: 222
Thread class와 다른점을 꼽자면 Thread를 상속(extends) 하는 것이 아닌 Runnable을 구현(implements)하도록 변경되었고, Thread의 생성자로 Runnable 인터페이스 객체를 지정하도록 변경되었다.
Thread class를 구현하는 것이 단순하긴 하나, 다른 클래스를 상속받을 수 없다.
반면에 Runnable interface를 구현할 경우 다른 인터페이스를 구현할 수 있으며 다른 클래스도 상속받을 수 있다. 고로 코드의 재사용성이 높고 일관성을 유지할 수 있기에 객체지향적이다.
나도 실제로 API 개발할 때 Runnable 인터페이스를 구현하는 방식으로 구현한만큼
실제로 실무에서 Runnable interface가 많이 사용되는 이유인 듯 하다.
구글링하면 거의 비스무리하게 알 수 있는 내용들이지만 적어보면서 다시금 공부하게 되는 시간이었다.
사실 이번 포스팅에 하고 싶은 이야기가 더 남아있었지만..
멀티쓰레드를 효율적으로 처리하는 것과 관련된 ExecutorService와 Executors를 통해 Thread pool를 구현해보고 이에 대한 장단점을 적어보려 했지만 생각보다 길어져 다음 포스팅에 이야기 해보려한다.
https://wikidocs.net/230#thread
https://gnaseel.tistory.com/21
https://aileen93.tistory.com/105
https://jongminlee0.github.io/2020/03/16/thread/
https://www.daleseo.com/java-thread-runnable/