List
Search
1. 스레드의 생성과 실행
1.1. 스레드의 생성 (Thread와 Runnable)
•
자바에서 멀티스레드를 만드는 대표적인 방법
1.
Thread 클래스를 직접 상속받는 방법
2.
Runnable 인터페이스를 구현하는 방법
•
Thread 상속 방식
◦
class MyThread extends Thread 형태로 정의하고 run() 메서드를 오버라이드.
◦
코드가 단순, 곧바로 new MyThread().start()로 실행 가능.
◦
자바는 단일 상속만 허용 → 이미 다른 클래스를 상속 중이라면 사용할 수 없음.
▪
즉, Thread를 상속하면 유연성이 떨어짐.
▪
스레드 자체와 작업 로직이 강하게 결합되어 재사용성이 낮음.
•
Runnable 구현 방식
◦
Runnable 인터페이스의 run() 메서드 안에 작업 로직을 정의.
◦
Thread 객체를 생성할 때 new Thread(new MyRunnable())와 같이 실행 환경과 작업을 분리.
◦
다른 클래스를 상속하면서도 병행 가능.
◦
작업(Task)과 실행(Thread)을 분리하여 구조가 명확해짐.
◦
같은 작업 인스턴스를 여러 스레드에서 공유할 수도 있음.
◦
실무에서는 대부분 이 방식을 사용하며, ExecutorService 같은 스레드 풀과 함께 사용하면 더 강력함.
•
예시 코드
// Thread 상속
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread 상속 방식 실행");
}
}
// Runnable 구현
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable 구현 방식 실행");
}
}
public class ThreadVsRunnable {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new MyThread(); // Thread 상속
Thread t2 = new Thread(new MyRunnable()); // Runnable 구현
t1.start();
t2.start();
t1.join();
t2.join();
}
}
Java
복사
1.2. 스레드의 실행 (start 메서드와 run 메서드)
•
run()
◦
단순 메서드 호출. 멀티 스레드 실행과는 무관함.
◦
따라서 호출한 스레드(main 등)에서 실행됨. 멀티스레드가 되지 않음.
•
start()
◦
새로운 OS 수준의 스레드를 생성하고, 그 안에서 run() 메서드를 실행시킴.
◦
이게 진짜 멀티스레드 실행을 의미함.
•
즉, 멀티스레드를 원한다면 반드시 start()를 호출해야 함.
sequenceDiagram participant M as main participant T as Thread Note over M: run() 호출 M->>T: t.run() Note over T: 실제 실행은 main 스레드에서 처리 Note over M: start() 호출 M->>T: t.start() T-->>T: (새 스레드 생성 후 run() 실행)
Mermaid
복사
•
예시 코드
class Task implements Runnable {
@Override
public void run() {
System.out.println("실행 스레드: " + Thread.currentThread().getName());
}
}
public class StartVsRun {
public static void main(String[] args) {
Thread thread = new Thread(new Task(), "WorkerThread");
thread.run(); // main 스레드에서 실행
thread.start(); // WorkerThread 스레드에서 실행
}
}
Java
복사
2. 스레드 생명 주기와 제어
2.1. 스레드의 생명주기
•
NEW (생성)
◦
new Thread(...) 로 스레드 객체를 만들었지만, 아직 시작하지 않은 상태.
◦
start()를 호출해야 다음 상태로 진행.
•
RUNNABLE (실행 대기)
◦
start()가 호출되면 스레드가 실행 대기 큐에 들어감.
◦
CPU 스케줄러가 언제 실행할지는 OS와 JVM이 결정.
◦
실제 실행 중(RUNNING)과 대기 상태를 통칭해서 RUNNABLE로 부름.
◦
자바의 Thread.State에서 RUNNABLE은 실행 대기와 실행 중을 구분하지 않고 포함하는 개념임.
◦
RUNNING이라는 상태명은 공식적으로 없지만, 이해를 돕기 위해 흔히 구분해서 설명.
•
RUNNING (실행 중)
◦
CPU를 점유하고 run() 메서드를 실행하는 상태.
◦
여러 스레드가 번갈아가며 RUNNING 상태가 됨(시분할).
•
WAITING (무기한 대기)
◦
다른 스레드가 깨워줄 때까지 영원히 기다리는 상태.
◦
Object.wait() 등이 원인.
•
TIMED_WAITING (시간 제한 대기)
◦
일정 시간 동안만 대기.
◦
Thread.sleep(ms), join(ms), wait(ms) 같은 메서드 호출 시.
•
BLOCKED (블로킹)
◦
모니터 락(lock)을 얻기 위해 대기하는 상태.
◦
동기화 블록(synchronized)에 진입하려는데 이미 다른 스레드가 점유 중일 때 발생.
•
TERMINATED (종료)
◦
run() 메서드가 끝나면 스레드는 더 이상 실행되지 않음.
◦
종료된 스레드는 다시 시작할 수 없음(IllegalThreadStateException 발생).
stateDiagram-v2 [*] --> NEW NEW --> RUNNABLE: start() RUNNABLE --> RUNNING: 스케줄러 선택 RUNNING --> RUNNABLE: yield / 스케줄러 전환 RUNNING --> TIMED_WAITING: sleep(ms) / join(ms) RUNNING --> WAITING: wait() RUNNING --> BLOCKED: lock 획득 대기 WAITING --> RUNNABLE: notify() TIMED_WAITING --> RUNNABLE: 시간만료 BLOCKED --> RUNNABLE: lock 획득 RUNNING --> TERMINATED: run() 종료 TERMINATED --> [*]
Mermaid
복사
•
예시 코드
public class ThreadLifecycleDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
System.out.println("RUNNING: 작업 시작");
Thread.sleep(1000); // TIMED_WAITING
synchronized (ThreadLifecycleDemo.class) {
System.out.println("BLOCKED/WAITING 예시 준비");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("RUNNING → TERMINATED");
});
System.out.println("NEW: 스레드 생성됨");
System.out.println("t state = " + t.getState());
t.start();
System.out.println("RUNNABLE: start() 호출 후");
System.out.println("t state = " + t.getState());
Thread.sleep(500);
System.out.println("중간 상태: " + t.getState()); // RUNNABLE or TIMED_WAITING
t.join();
System.out.println("TERMINATED: 종료 후");
System.out.println("t state = " + t.getState());
}
}
Java
복사
2.2. Thread의 join의 역할
•
기본적으로 main 스레드는 하위 스레드를 기다리지 않음.
•
따라서 하위 스레드가 아직 작업 중인데도 main은 종료될 수 있음
•
join()은 호출된 스레드가 완전히 끝날 때까지 현재 스레드를 대기 상태로 만듬
•
join()을 사용하면 main이 하위 스레드의 작업을 기다린 뒤 안전하게 결과를 사용할 수 있다.
•
예시 코드
class SumTask implements Runnable {
int result = 0;
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
result += i;
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
}
}
}
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
SumTask task1 = new SumTask();
SumTask task2 = new SumTask();
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.start();
t2.start();
// main이 하위 스레드 종료까지 기다림
t1.join();
t2.join();
System.out.println("task1.result = " + task1.result);
System.out.println("task2.result = " + task2.result);
System.out.println("총합 = " + (task1.result + task2.result));
}
}
Java
복사
2.3. interrupt가 하는 역할
•
역할
◦
interrupt()는 스레드를 즉시 강제 종료하지 않는다.
◦
대신 스레드의 인터럽트 플래그를 true로 바꿈
•
일반적인 예시
◦
스레드가 sleep(), wait(), join() 같은 대기 상태일 때 → InterruptedException 발생.
◦
스레드가 루프 작업 중이라면 isInterrupted()를 체크해서 중단할 수 있음
•
올바른 패턴
◦
catch (InterruptedException e) 블록에서 Thread.currentThread().interrupt()를 호출해 상태를 복원하고, 루프를 종료하거나 자원 정리를 하는 것.
◦
InterruptedException이 발생하면 인터럽트 플래그가 자동으로 내려가기 때문에, 다시 interrupt()를 호출해 플래그를 true로 세팅해줘야 이후 상위 로직이 “아직 인터럽트 상태구나”를 알 수 있음.
•
예시 코드
class MyTask implements Runnable {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("작업 중...");
Thread.sleep(1000); // 여기서 interrupt되면 예외 발생
}
} catch (InterruptedException e) {
System.out.println("대기 중 인터럽트 감지");
Thread.currentThread().interrupt(); // 상태 복원
} finally {
System.out.println("리소스 정리 후 종료");
}
}
}
public class InterruptExample {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new MyTask(), "worker");
t.start();
Thread.sleep(3000);
System.out.println("메인: interrupt() 호출");
t.interrupt();
t.join();
System.out.println("메인 종료");
}
}
Java
복사
3. 스레드와 체크 예외
3.1. 체크 예외와 언체크 에외
•
체크 예외 (Checked Exception)
◦
Exception을 상속하지만 RuntimeException은 아님.
◦
컴파일러가 예외 처리 강제 → try-catch 또는 throws 선언 필요.
◦
파일, DB, 네트워크처럼 외부 환경 요인으로 실패할 수 있는 경우 대표적.
◦
예: IOException, SQLException
public void readFile() throws IOException {
FileReader fr = new FileReader("없는파일.txt"); // IOException 발생 가능
fr.read();
}
Java
복사
IOException은 체크 예외이므로 반드시 처리하거나 throws로 던져야 함.
•
언체크 예외 (Unchecked Exception)
◦
RuntimeException을 상속한 예외들.
◦
처리 강제 없음 → 개발자 코드의 실수에서 발생하는 경우 많음.
◦
예: NullPointerException, IllegalArgumentException
public void divide(int a, int b) {
int result = a / b; // b가 0이면 ArithmeticException 발생
System.out.println(result);
}
Java
복사
ArithmeticException은 언체크 예외이므로, 따로 처리하지 않아도 컴파일 에러 없음.
3.2. Thread.sleep()과 체크 예외
•
sleep()은 외부 요인에 의해 깨질 수 있음
•
Thread.sleep(ms)는 현재 스레드를 지정한 시간 동안 멈추게 하지만, 이건 보장된 대기가 아님.
◦
Thread.sleep(ms)는 지정한 시간 동안 스레드를 중단시키지만, 실제 깨어나는 시점은 OS 스케줄러에 따라 달라질 수 있어 “정확한 대기 보장”은 아님.
•
다른 스레드가 interrupt()를 호출하면 → sleep() 중이던 스레드가 즉시 깨어남
•
즉, “외부 환경(다른 스레드)”이 영향을 주는 상황이라서, 반드시 대비 코드가 필요.
•
이런 “외부 요인”에 대한 대비는 체크 예외로 설계필요
public class SleepCheckedExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
System.out.println("작업 시작: 3초 대기");
Thread.sleep(3000); // 체크 예외 발생 가능
System.out.println("정상적으로 3초 대기 완료");
} catch (InterruptedException e) {
// 다른 스레드가 interrupt() 호출 시 여기로 진입
System.out.println("sleep 중 인터럽트 발생: " + e);
Thread.currentThread().interrupt(); // 상태 복원 (권장)
}
});
t.start();
// 1초 뒤에 강제로 깨우기
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
System.out.println("메인: interrupt() 호출");
t.interrupt();
}
}
Java
복사
작업 시작: 3초 대기
메인: interrupt() 호출
sleep 중 인터럽트 발생: java.lang.InterruptedException: sleep interrupted
Bash
복사
3.3. InterruptedException과 체크 예외
•
자바 설계자들은 스레드 인터럽트 신호를 무시하지 말라는 메시지를 주고 싶었던 것.
•
만약 InterruptedException이 언체크 예외였다면?
→ 개발자가 아무 대비 없이 코드를 짜더라도 컴파일러가 경고하지 않음.
→ 그러면 중요한 인터럽트 신호를 놓치고, 시스템 안정성이 깨질 수 있음.
public class InterruptedExceptionExample {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
while (true) {
System.out.println("작업 중...");
Thread.sleep(500); // 인터럽트 되면 InterruptedException 발생
}
} catch (InterruptedException e) {
// 체크 예외이므로 반드시 처리 필요
System.out.println("InterruptedException 처리: " + e.getMessage());
Thread.currentThread().interrupt(); // 상태 복원
}
});
worker.start();
Thread.sleep(1500); // 잠시 기다렸다가
System.out.println("메인: interrupt() 호출");
worker.interrupt();
worker.join();
System.out.println("메인 종료");
}
}
Java
복사
작업 중...
작업 중...
작업 중...
메인: interrupt() 호출
InterruptedException 처리: sleep interrupted
메인 종료
Bash
복사
3.4. Runnable.run()의 제약
•
Runnable.run() 선언부에 throws가 없음 → 체크 예외를 밖으로 던질 수 없음.
•
따라서 반드시 try-catch로 내부에서 처리하거나, 언체크 예외로 감싸서 던져야 함.
class CheckedTask implements Runnable {
@Override
public void run() {
try {
throw new java.io.IOException("체크 예외 발생");
} catch (java.io.IOException e) {
System.out.println("run() 내부에서 처리: " + e.getMessage());
}
}
}
Java
복사