스레드란 무엇인가? 어떤 용도로 사용되는가?
모든 작업은 담당자가 필요하며, 컴퓨터에서도 작업마다 해당 작업을 처리하는 담당자가 있다. 바로 이를 스레드라고 부른다. 즉, 스레드는 작업을 처리하는 일꾼이다.
하나의 작업은 반드시 한 개 이상의 스레드를 필요로 한다. 즉 복잡하고 방대한 작업은 두 개 이상의 스레드가 붙을 수 있으며, 이를 멀티스레딩이라고 한다. 멀티스레딩은 여러 작업을 동시에 처리할 수 있게 하는데, 멀티 코어 환경에서는 진정한 병렬 처리가 가능하고, 싱글 코어에서는 CPU가 빠르게 스레드를 전환하며 동시에 실행되는 것처럼 보이게 한다. 반대로 멀티스레딩이 없다면 작업은 순차적으로 처리된다. 작업을 한 명이 맡으면 순차적으로 처리해야 하는 것과 같다.
자바의 Thread 클래스는 스레드의 작업 처리 과정을 관리하기 위해 만들어졌다. 이를 이용하여 아래와 같이 스레드 시뮬레이션을 해볼 수 있다.
MyThread 클래스 - 생성 순간 자신의 이름(변수명)을 출력하고, 0부터 9까지 500밀리초(0.5초)의 간격으로 총 10번을 카운팅하는 객체 생성
package thread.multi;
public class MyThread extends Thread{
@Override
public void run() { //run() 메서드는 직접 호출하지 않고, start() 메서드를 이용해 호출한다.
String threadName = Thread.currentThread().getName();
System.out.println("현재 스레드 이름 : " + threadName);
for(int i=0;i<10;i++){
System.out.println("현재 스레드 : " + threadName + " - " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("종료된 스레드 : " + threadName);
}
}
Main 함수 : 두 스레드를 생성하고 각각 실행시킨다.
package thread.multi;
public class Main {
static void main(String[] args) {
System.out.println(":::메인 스레드 시작");
MyThread thread0 = new MyThread();
MyThread thread1 = new MyThread();
//1. thread 0 시작
System.out.println("thread0 시작");
thread0.start(); //새로운 스레드에서 run() 실행
//2. thread 1 시작
System.out.println("thread1 시작");
thread1.start(); //새로운 스레드에서 run() 실행
System.out.println(":::메인 스레드 종료:::"); //main 스레드는 스레드0, 1을 실행시키고 종료됨.
}
}
출력 결과
:::메인 스레드 시작
thread0 시작
thread1 시작
:::메인 스레드 종료:::
현재 스레드 이름 : Thread-0
현재 스레드 이름 : Thread-1
현재 스레드 : Thread-0 - 0
현재 스레드 : Thread-1 - 0
현재 스레드 : Thread-0 - 1
현재 스레드 : Thread-1 - 1
현재 스레드 : Thread-1 - 2
현재 스레드 : Thread-0 - 2
현재 스레드 : Thread-1 - 3
현재 스레드 : Thread-0 - 3
현재 스레드 : Thread-1 - 4
현재 스레드 : Thread-0 - 4
현재 스레드 : Thread-1 - 5
현재 스레드 : Thread-0 - 5
현재 스레드 : Thread-1 - 6
현재 스레드 : Thread-0 - 6
현재 스레드 : Thread-1 - 7
현재 스레드 : Thread-0 - 7
현재 스레드 : Thread-0 - 8
현재 스레드 : Thread-1 - 8
현재 스레드 : Thread-0 - 9
현재 스레드 : Thread-1 - 9
종료된 스레드 :
종료된 스레드 :
종료 코드 0(으)로 완료된 프로세스
언뜻 보면 두 스레드가 완벽하게 번갈아가며 작업을 처리하는 듯 보이지만, 실행 순서를 자세히 보면 thread1이 연속으로 두 번 처리하는 것을 볼 수 있다. 이처럼 실행할 때마다 출력 순서가 달라질 수 있는데, 이는 CPU 스케줄러가 스레드에 자원을 비결정적으로 배분하기 때문이다. 간혹 위의 예시처럼 한 스레드가 연속으로 실행되는 모습을 볼 수도 있다. 하지만 각 스레드 차원에서 보면, 각자의 작업을 순차적으로 처리하고 있다.
Runnable이란 무엇인가?
위 코드에서는 Thread 클래스를 상속하는 클래스(MyThread)를 생성함으로써 스레드의 작업 처리 과정을 구현했다.
하지만 객체 지향적 관점에서 봤을 때 아쉬운 부분이 있는데, 바로 Thread라는 클래스를 이미 상속받기 때문에 다른 클래스는 상속이 불가하다는 점이다(자바는 다중상속을 지원하지 않는다). 또한 Main 클래스에서는 스레드 관리 로직과 실행 로직이 혼재되어 있다. 이는 프로그래밍 관점에서 좋지 않다. 클래스 상속을 떠나서 기능의 분리를 위해서라도 관리 로직과 실행 로직을 분리할 필요가 있다.
이를 해결할 방법이 있는데, 바로 Runnable 인터페이스를 사용하는 것이다. (인터페이스는 클래스와 별개이다).
일반적으로 Runnable 인터페이스로 구현하는 방법이 권장된다.
MyRunnable 클래스 생성 - MyNewClass의 특정 기능을 상속하면서 실행 로직이 구현되어 있다.
package thread.runnable;
public class MyRunnable extends MyNewClass implements Runnable{
@Override //Runnable 객체는 무엇도 상속하지 않기 때문에 다른 클래스 상속을 통해 기능 확장이 가능
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("현재 스레드 이름 : " + threadName);
for(int i=0;i<10;i++){
System.out.println("현재 스레드 : " + threadName + " - " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("종료된 스레드 : " + threadName);
}
}
myNewClass - 별개의 기능을 수행하는 클래스 (상속 용도로 만든 클래스)
package thread.runnable;
public class MyNewClass {
public void printMessage(){
System.out.println("MyNewClass의 기능을 실행합니다");
}
}
Main 클래스 - 실행 로직을 담당하고 있는 MyRunnable 객체 myTask가 먼저 생성되고, 해당 객체는 myTask.printMessage()와 같은 상속받은 메서드를 사용할수 있다. 정작 스레드 객체를 생성하고 제어하는 스레드 제어 로직은 Thread 클래스에서만 관리하게 된다는 점에서 차이가 드러난다.
package thread.runnable;
public class Main {
static void main(String[] args) {
MyRunnable myTask = new MyRunnable(); //실행 로직 담당
myTask.printMessage();
Thread thread0 = new Thread(myTask); //스레드 제어만 담당
Thread thread1 = new Thread(myTask);
thread0.start();
thread1.start();
}
}
Join()이란 무엇인가?
join()은 스레드들의 종료 시점을 맞추기 위해 사용된다.
참고로 아래 코드는 Runnable을 사용하지 않은 버전의 코드다.
이 프로그램에는 메인 스레드, thread0, thread1 총 세 개의 스레드가 동작하고 있다.
main 함수에 의해 가장 먼저 메인 스레드가 시작되고, 이후 각 스레드의 .start()메서드를 통해 두 스레드가 차례로 시작된다.
package thread.join;
import thread.multi.MyThread;
public class Main {
static void main(String[] args) {
System.out.println(":::메인 스레드 시작");
MyThread thread0 = new MyThread();
MyThread thread1 = new MyThread();
long startTime = System.currentTimeMillis();
//1. thread 0 시작
System.out.println("thread0 시작");
thread0.start(); //새로운 스레드에서 run() 실행
//2. thread 1 시작
System.out.println("thread1 시작");
thread1.start(); //새로운 스레드에서 run() 실행
//main 스레드 대기시키기
try {
thread0.join();
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(":::메인 스레드 종료:::"); //main 스레드는 스레드0, 1을 실행시키고 종료됨.
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
System.out.println("작업 소요 시간 : " + totalTime);
}
}
출력 결과
:::메인 스레드 시작
thread0 시작
thread1 시작
현재 스레드 이름 : Thread-0
현재 스레드 이름 : Thread-1
현재 스레드 : Thread-1 - 0
현재 스레드 : Thread-0 - 0
현재 스레드 : Thread-0 - 1
현재 스레드 : Thread-1 - 1
현재 스레드 : Thread-0 - 2
현재 스레드 : Thread-1 - 2
현재 스레드 : Thread-0 - 3
현재 스레드 : Thread-1 - 3
현재 스레드 : Thread-0 - 4
현재 스레드 : Thread-1 - 4
현재 스레드 : Thread-0 - 5
현재 스레드 : Thread-1 - 5
현재 스레드 : Thread-0 - 6
현재 스레드 : Thread-1 - 6
현재 스레드 : Thread-0 - 7
현재 스레드 : Thread-1 - 7
현재 스레드 : Thread-0 - 8
현재 스레드 : Thread-1 - 8
현재 스레드 : Thread-0 - 9
현재 스레드 : Thread-1 - 9
종료된 스레드 :
종료된 스레드 :
:::메인 스레드 종료:::
작업 소요 시간 : 5017
종료 코드 0(으)로 완료된 프로세스
위 실행 결과를 먼저 보면, 스레드 종료 시점에서 이전 코드들과 차이가 있음을 알 수 있다.
바로 스레드 두 개가 종료되고 나서 메인 스레드가 종료된다는 것이다.
이전에는 두 스레드를 실행시키고 메인 스레드가 종료되었던 것에 반해, 이번엔 두 스레드 종료 이후에 메인 스레드가 종료된 것이다. 이것이 join의 기능이다.
'ETC > 1. Today I Learned' 카테고리의 다른 글
| [Git] 내 코드를 다른 레포지토리의 코드로 덮어씌우기 (0) | 2025.12.16 |
|---|---|
| [Java] 클래스 간 통제 관계 (0) | 2025.12.15 |
| [Git 기초] Fork & Clone, Fetch & Pull 배워보기 (0) | 2025.12.11 |
| [Java] 특정 문자열을 입력해야만 무한루프를 종료하는 기능 구현 (0) | 2025.12.08 |
| [Java] 예외 처리 (Exception) (0) | 2025.12.05 |