🧵 쓰레드란?
- 프로세스 내에서 실행되는 독립적인 작업 흐름 단위
→ 하나의 자바 애플리케이션(JVM)은 하나 이상의 쓰레드를 가질 수 있음
→ 기본적으로 자바 프로그램은 Main 쓰레드에서 실행됨
→ 코드를 작성하면서 명시적으로 생성하는 쓰레드는 Main 이외의 추가 작업 흐름
📊 쓰레드와 프로세스 비교
| 항목 | 프로세스 | 쓰레드 |
|---|---|---|
| 정의 | 실행 중인 프로그램 | 프로세스 내의 실행 흐름 |
| 메모리 | 독립적 공간 사용 | 메모리 공유 (heap, static 등) |
| 생성 비용 | 높음 | 낮음 |
| 통신 | IPC 필요 | 공유 변수 사용 가능 |
| 자바에서 | JVM 단위 | Thread 클래스 |
➡️ 싱글 쓰레드(Single Thread)
- 하나의 쓰레드만 사용하는 프로그램
→ 프로그램 전체가 한 작업 흐름에서 차례대로 실행됨흐름에서 차례대로 실행됨
→ Main 쓰레드 하나만 존재
→ 작업이 순차적으로 처리됨(이전 작업이 끝나야 다음 작업 진행)
🗒️ 예시 코드:
public class Main {
public static void main(String[] args) {
System.out.println("::: main 쓰레드 시작 :::");
String threadName = Thread.currentThread().getName();
// ✅ 하나의 작업 단위: 숫자를 0 부터 9 까지 출력
for (int i = 0; i < 10; i++) {
System.out.println("현재 쓰레드: " + threadName + " - " + i);
try {
Thread.sleep(500); // 0.5 초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// ✅ 추가작업
for (int i = 0; i < 10; i++) {
System.out.println("현재 쓰레드: " + threadName + " - " + i);
try {
Thread.sleep(500); // 0.5 초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("::: 작업 끝 :::");
}
}
🔀 멀티 쓰레드(Multi-Thread)
- 여러 개의 쓰레드가 동시에 실행되는 프로그램
→ 여러 작업을 병렬(또는 동시)적으로 수행 가능
→ CPU 코어가 여러 개면 진짜 병렬, 하나면 빠른 전환
→ 작업이 서로 영향을 주지 않도록 동기화가 필요할 수 있음
🗒️ 예시 코드:
// ✅ Thread 클래스 상속으로 쓰레드 구현
public class MyThread extends Thread {
@Override
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); // 딜레이 0.5 초
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("::: " + threadName + "쓰레드 종료 :::");
}
}
public class Main {
public static void main(String[] args) {
System.out.println("::: main 쓰레드 시작");
MyThread thread0 = new MyThread();
MyThread thread1 = new MyThread();
// 1. thread0 실행
System.out.println("::: main 이 thread0 을 실행");
thread0.start();
// 2. thread1 실행
System.out.println("::: main 이 thread1 을 실행");
thread1.start();
System.out.println("::: main 쓰레드 종료");
}
}
📊 싱글 vs 멀티 쓰레드 비교
| 항목 | 싱글 쓰레드 | 멀티 쓰레드 |
|---|---|---|
| 실행 흐름 | 하나 | 여러 개 |
| 속도 | 느릴 수 있음 | 병렬 실행으로 더 빠름 |
| 구현 | 간단 | 상대적으로 복잡 |
| 자원 소비 | 적음 | 많음 (관리 필요) |
| 동시 작업 | 불가능 | 가능 |
| 예외/버그 발생 | 적음 | 동기화 안 되면 위험 가능성 있음 |
🔗 join()이란?
- 다른 쓰레드가 종료될 때까지 현재 쓰레드를 일시 정지시키는 메서드
A.join() → 현재 쓰레드는 A 쓰레드가 끝날 때까지 기다림join()을 호출하지 않으면 쓰레드들이 병렬로 실행되어 끝나는 순서 예측 불가InterruptedException을 반드시 처리해야 함🗒️ 예시 코드:
public class Main {
public static void main(String[] args) {
System.out.println("::: main 쓰레드 시작");
MyThread thread0 = new MyThread();
MyThread thread1 = new MyThread();
// 시작시간 기록
long startTime = System.currentTimeMillis();
// 1. thread0 시작
System.out.println("thread0 시작");
thread0.start();
// 2. thread1 시작
System.out.println("thread1 시작");
thread1.start();
// ⌛️ main 쓰레드 대기 시키기
try {
thread0.join();
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
System.out.println("총 작업 시간: " + totalTime + "ms");
System.out.println("::: main 쓰레드 종료");
}
}
🧩 Runnable 인터페이스란?
- 쓰레드에서 실행할 코드를 정의하는 함수형 인터페이스
- 즉,
run()메서드 하나만 가진 인터페이스로, 쓰레드에 넘겨서드 하나만 가진 인터페이스로, 쓰레드에 넘겨줄 수 있음@FunctionalInterface public interface Runnable { void run(); }→ 자바의
Thread클래스는Runnable을 매개변수로 받을 수 있음
→ 람다식으로도 구현 가능(함수형 인터페이스이기 때문)
1️⃣ 유지 보수성과 재사용성 향상
Thread는 쓰레드를 제어하기 위해 존재하는 클래스Thread 클래스를 상속받아 MyThread를 구현하면 실행 로직과 쓰레드 제어 로직이 결합되어 한 가지 클래스에서 두 가지 역할을 담당하게 됨start(), join(), isAlive() 등Runnable을 활용하면 실행 로직을 별도의 구현체로 분리할 수 있음Thread: 쓰레드 제어Runnable 구현체: 실행 로직 관리🗒️ 예시 코드:
// ⚠️ Thread는 Thread 제어 역할 그리고 실행로직 두가지를 담당합니다.
public class MyThread extends Thread {
@Override
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); // 딜레이 0.5 초
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("종료된 쓰레드: " + threadName);
}
}
public class MyRunnable implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println("현재 쓰레드: " + threadName + " - " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
MyRunnable task = new MyRunnable(); // ✅ 하나의 작업 객체 선언
// ✅ 하나의 작업을 여러 쓰레드에서 공유
Thread thread0 = new Thread(task); // 작업 객체 공유
Thread thread1 = new Thread(task); // 작업 객체 공유
// 실행
thread0.start();
thread1.start();
}
}
2️⃣ 확장 가능성
_Thread를 상속해서 MyThread를 구현하면 다른 클래스를 상속받지 못함Thread_를 상속하면 다른 클래스를 상속할 수 없어 확장성이 떨어짐Runnable은 인터페이스이므로 기존 클래스의 기능을 유지하면서 상속을 통해 확장할 수 있음🗒️ 예시 코드:
public class MyNewClass { // ✅ 새로운 클래스
public void printMessage() {
System.out.println("MyClass 기능 실행");
}
}
public class MyThread extends Thread, MyNewClass{ // ❌ 다중 상속 불가
...
}
public class MyRunnable extends MyNewClass implements Runnable { // ✅ 다중 상속
@Override
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println("현재 쓰레드: " + threadName + " - " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
MyRunnable task = new MyRunnable();
// ✅ 기존 클래스를 유지하면서 확장해서 활용
task.printMessage();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
thread0.start();
thread1.start();
}
}
✏️ 연습 문제) 싱글 VS 멀티 쓰레드 - 숫자 합계 시간 비교
- 1부터 1000까지의 숫자 합계 구하기
→ 연산 사이에 10ms 딜레이를 추가하여 0.01초마다 계산 -Thread.sleep(10);예시- 싱글 쓰레드는 하나의 루프로 계산
- 멀티 쓰레드는 4개의 쓰레드가 각각 숫자를 나누어 계산
→ 1~250
→ 251~500
→ 501~750
→ 751~1000
- SingleThread.java
public class SingleThread {
public long calculateSum() {
long start = System.currentTimeMillis(); // 시작 시간 기록
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
try {
Thread.sleep(10); // 0.01 초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis(); // 종료 시간 기록
System.out.println("싱글 쓰레드 소요 시간: " + (end - start) + "ms");
return sum;
}
}
- MultiThread.java
public class MultiThread implements Runnable {
private int start;
private int end;
private long sum;
public MultiThread(int start, int end) {
this.start = start;
this.end = end;
}
public long getSum() {
return this.sum;
}
@Override
public void run() {
for (int i = start; i <= end; i++) {
this.sum += i;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- Main.java
package chapter3_7;
public class Main {
public static void main(String[] args) {
System.out.println("::: main 쓰레드 시작");
// 싱글 쓰레드
SingleThread s1 = new SingleThread();
long single_result = s1.calculateSum();
System.out.println("싱글 쓰레드 결과: " + single_result);
// 멀티 쓰레드
long multiStartTime = System.currentTimeMillis();
MultiThread result0 = new MultiThread(1, 250);
MultiThread result1 = new MultiThread(251, 500);
MultiThread result2 = new MultiThread(501, 750);
MultiThread result3 = new MultiThread(751, 1000);
Thread thread0 = new Thread(result0);
Thread thread1 = new Thread(result1);
Thread thread2 = new Thread(result2);
Thread thread3 = new Thread(result3);
thread0.start();
thread1.start();
thread2.start();
thread3.start();
try {
thread0.join();
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long sum0 = result0.getSum();
long sum1 = result1.getSum();
long sum2 = result2.getSum();
long sum3 = result3.getSum();
long multi_total = sum0 + sum1 + sum2 + sum3;
long multiEndTime = System.currentTimeMillis();
long multiDuration = multiEndTime - multiStartTime;
System.out.println();
System.out.println("멀티 쓰레드 소요시간: " + multiDuration + "ms");
System.out.println("멀티 쓰레드 결과: " + multi_total);
System.out.println("::: main 쓰레드 종료");
}
}
출력 결과
::: main 쓰레드 시작
싱글 쓰레드 소요 시간: 10744ms
싱글 쓰레드 결과: 500500
멀티 쓰레드 소요시간: 2715ms
멀티 쓰레드 결과: 500500
::: main 쓰레드 종료