[Thread] 2. JAVA에서 Thread사용하기
개요
JVM은 하나의 프로세스로 실행되고, 여러 개의 Thread를 실행할 수 있는 환경을 갖춥니다.
또 우리가 만든 자바 프로그램을 실행하고, main thread를 생성하고, 코드를 실행합니다.
JAVA에서 Thread를 사용하는 대표적인 2가지 방법이 있습니다.
Thread Class를 사용하는 방법과 Runnable Interface를 사용하는 방법이 바로 그것입니다.
Thread 사용하기
public class Thread {
public static void main(String[] args) {
Thread thread1 = new ExampleThread();
Thread thread2 = new Thread(new ExampleRunnable());
thread1.start();
thread2.start();
}
}
class ExampleThread extends Thread {
@Override
public void run() {
System.out.println(this.getName());
System.out.println(this.getThreadGroup());
}
}
class ExampleRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getThreadGroup());
}
}
간단히 코드를 보면 이렇게 될 것입니다.
Thread Class, Runnable Interface방식 모두 run메서드를 오버라이드하는 모습을 볼 수 있습니다.
Thread Class는 Runnable Interface의 구현체이고, Runnable의 구현체를 만들기 위해 run메서드를 구현해야 함을 알 수 있습니다.
실제로 스레드의 동작부분이기도 하기 때문에 Thread Class사용 시에도 필수적으로 구현해 주어야 합니다.
Thread의 생성자를 보면 ThreadGroup, ThreadName, Runnable, StackSize등을 받는 것을 볼 수 있습니다.
즉, new Thread(Runnable)으로 스레드를 사용할 때 해당 Runnable객체를 task로 Thread객체에 저장합니다.
따라서 Runnable Interface의 구현체만을 만들어도 Thread객체를 만들고 사용할 수 있는 것입니다.
Thread Class를 바로 구현해서 사용할 때, JAVA는 다중 상속이 불가능하므로 다른 Class를 상속받고 싶어도 불가능합니다.
Runnable Interface를 구현하면 다른 클래스를 상속받은 객체를 만들 수 있습니다.
그렇기 때문에, Thread를 사용해야 한다면 Runnable 구현체를 만들어 사용할 수 있을까 라는 부분을 생각해보면 좋을 것입니다.
Start() 메서드
Thread를 처음 사용할 때, start()메서드를 사용하는 부분이 적응되지 않을 수 있습니다.
run()메서드를 구현해놓고 start()메서드를 사용하니 발생하는 부분입니다.
public class Threadrun {
public static void main(String[] args) {
Thread thread = new Runthread();
thread.run();
thread.start();
}
}
class Runthread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " " + Thread.currentThread().getThreadGroup());
}
}
위의 코드를 직접 실행해보면 간단히 알 수 있습니다.
thread.run()을 이용해 실행시킨 경우, 콘솔창에 main스레드가 나올 것입니다.
thread.start()로 실행시킨다면 콘솔창에 Thread-X가 출력됩니다.
왜 이러한 문제가 발생할까요?
run메서드에서 스레드가 수행할 작업을 정의했습니다.
그런데, run메서드를 그냥 부르면 일반 클래스의 메서드를 부르는 것과 다르지 않습니다.
우리는 Thread가 JVM위에서 동작한다는 것을 알고 있습니다.
따라서 새 스레드를 만들기 위해 JVM에게 협조를 요청해야 합니다.
start()메서드는 바로 그 부분을 수행하는 메서드입니다.
Thread의 상태를 검사해 new인지 확인하고, 이미 사용한 Thread객체라면 예외를 발생시킵니다.
이미 JVM에 의해 status가 바뀐 Thread라면 더 이상 사용하지 못하도록 하는 것입니다.
이후 new상태인 것을 확인했다면, native method인 start0()메서드를 실행합니다.
start0()메서드는 JVM이 새로운 스레드를 만들고, 거기서 작업을 하도록 유도하는 메서드입니다.
그렇기 때문에 우리는 run()메서드가 아닌 start()메서드를 이용해야 하는 것입니다.
ThreadGroup
Thread생성자에서 주요 인자로 ThreadGroup, ThreadName, Runnable, stackSize를 받았습니다.
ThreadName, Runnable, stackSize는 알겠는데 ThreadGroup은 무엇일까요?
위의 코드들을 실행했을 때, ThreadName, ThreadGroup을 동시에 출력했습니다.
출력 내용으로는 아마, java.lang.ThreadGroup[name=main,maxpri=10] 이라는 결과가 나왔을 것입니다.
메인 메서드에서 Thread, Runnable을 만들고 실행하면, 이 Thread는 자동으로 ThreadGroup main에 등록됩니다.
그렇다는 말은, Thread를 관리하기 위해 ThreadGroup을 사용한다는 것입니다.
우리가 start()메서드를 호출했을 때
1. Thread가 new상태인지 검사한다.
2. Thread를 ThreadGroup에 추가한다.
3. JVM에서 해당 Thread를 실행한다.
의 순서로 작업이 진행될 것입니다.
public class Theadgroup {
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("under_main");
Thread th3 = new ExamThread(threadGroup, new ExamThread());
Thread th4 = new ExamThread(threadGroup, new ExamThread());
th3.start();
th4.start();
try {
Thread.sleep(3000);
threadGroup.interrupt();
} catch (InterruptedException e){
threadGroup.interrupt();
}
}
}
class ExamThread extends Thread {
public ExamThread(){
super();
}
public ExamThread(ThreadGroup threadGroup, Thread thread){
super(threadGroup, thread);
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
System.out.println(this.getName() + " " + this.getThreadGroup());
} catch (InterruptedException e) {
System.out.println(this.getName() + " interrupted");
break;
}
}
}
}
위의 코드를 실행해 보면, ThreadGroup을 임의로 만들어 관리할 수 있다는 것을 알 수 있습니다.
또, interrupt()메서드를 이용해 해당 ThreadGroup 전체를 통제할 수 있다는 것도 알 수 있습니다.
정리
이 글에서 Thread사용법, run()메서드와 start()메서드의 차이, ThreadGroup을 알아봤습니다.
현대 소프트웨어는 대부분 Thread로 돌아간다 해도 과언이 아닙니다.
Tomcat에서도 사용자의 Request를 받을 때 Thread를 이용해 받습니다.
비동기 처리, 병렬처리 등을 이해하고 사용함에 있어 가장 핵심적인 요소이기도 합니다.
이러한 Thread는 사실 상당히 오래된 친구입니다.
Java4이전까지 동시성 프로그래밍을 할 때 사용되었고, Java 5, 6 ...으로 넘어오며 Callable, Executor, Flow ...등등 많은 기술들이 추가되었습니다.
하지만 여전히 많은 곳에서 Runnable, Thread가 사용되고 있으며 이를 배우는 것은 상당히 중요합니다.
특히, Java19에서 얼리억세스로 Virtual Thread라는 기능이 추가되었습니다.
2023년 Java21에서 정식 기능으로 탑재되었는데 성능향상폭이 뛰어났습니다.
동시성 프로그래밍에 대해 공부해보고 싶다면, Thread를 넘어서서 Virtual Thread를 공부해 보는 것을 추천합니다.