1. 쓰레드란 무엇인가?
**쓰레드(Thread)**는 하나의 프로세스 내에서 독립적으로 실행되는 작업 단위다.
자바 프로그램은 main 쓰레드에서 시작되며,
추가적인 작업을 병렬로 처리하기 위해 새로운 쓰레드를 생성할 수 있다.
쓰레드의 특징
- 같은 프로세스 내의 쓰레드는 힙, 클래스 영역을 공유하고 스택은 독립적이다.
- 한 쓰레드에서 예외가 발생해 종료되더라도 다른 쓰레드는 영향을 받지 않는다.
- 자바는 쓰레드를 JVM 레벨에서 관리하고, 실제 실행은 OS의 네이티브 쓰레드를 사용한다. (Java 21부터는 가상 쓰레드도 가능)
2. 쓰레드 생성 방법 – Thread 상속 vs Runnable 구현
자바에서 쓰레드를 생성하는 대표적인 두 가지 방식이 있다.
2-1. Thread 클래스 상속
class MyThread extends Thread {
public void run() {
System.out.println("Thread 실행: " + getName());
}
}
Thread t = new MyThread();
t.start();
- start() 호출 시 JVM이 새로운 쓰레드를 생성하여 run()을 실행한다.
2-2. Runnable 인터페이스 구현
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable 실행: " + Thread.currentThread().getName());
}
}
Thread t = new Thread(new MyRunnable());
t.start();
왜 Runnable을 굳이 써야 하나?
- 자바는 다중 상속이 불가능하므로, 이미 다른 클래스를 상속 중이라면 Thread 상속은 불가
- Runnable은 **실행 로직(run)**과 **실행 제어(Thread)**를 분리함으로써 유연성과 재사용성이 뛰어나다
- 실무에서는 거의 대부분 Runnable 방식이 사용된다
3. start() vs run() 차이
t.start(); // ✅ 새로운 쓰레드 생성 → 병렬 실행
t.run(); // ❌ 현재 쓰레드에서 메서드 호출 → 순차 실행
- start()는 JVM이 새로운 쓰레드를 만들어 실행
- run()은 단순 메서드 호출일 뿐 → 병렬 처리 아님
4. 데몬 쓰레드(Daemon Thread)
- 데몬 쓰레드는 백그라운드에서 사용자 쓰레드를 돕는 용도로 동작
- 사용자 쓰레드가 모두 종료되면 데몬 쓰레드도 자동 종료된다
Thread daemon = new Thread(() -> {
while (true) {
System.out.println("작업 중...");
}
});
daemon.setDaemon(true);
daemon.start();
- 대표 예: GC, 로그 감시, 백업
5. synchronized – 공유 자원에 대한 동기화
멀티쓰레드 환경에서 여러 쓰레드가 하나의 객체를 동시에 접근하면 충돌이 발생할 수 있다. 이를 방지하는 것이 synchronized 키워드다.
synchronized(this) vs synchronized(lock)
public synchronized void method() {
// this 객체 기준으로 락
}
public void method() {
synchronized(this) {
// 역시 this 기준
}
}
private final Object lock = new Object();
public void method2() {
synchronized(lock) {
// 별도 객체 기준 → 병렬성 확보 가능
}
}
- synchronized(this)는 객체 전체에 락을 걸어 병렬성이 낮아짐
- synchronized(lock)은 세밀한 제어가 가능하여 실무에서 더 많이 사용됨
6. 객체가 달라도 같은 데이터를 공유한다면?
예를 들어, 여러 인스턴스가 동일한 stock 값을 참조해야 하는 상황이라면
→ static으로 선언하고 동기화 처리가 필수다.
class ProductService {
private static int stock = 100;
private static final Object lock = new Object();
public boolean purchase() {
synchronized(lock) {
if (stock > 0) {
stock--;
return true;
}
return false;
}
}
}
- static 필드는 클래스 전체에서 공유됨
- 동기화를 하지 않으면 동시성 문제 발생
7. Spring에서 인스턴스 변수는 안전할까?
Spring의 @Service, @Component 빈은 기본적으로 싱글톤 스코프
→ 모든 요청에서 같은 객체 인스턴스를 공유
→ 이 객체의 인스턴스 필드는 모든 쓰레드에서 공유됨
@Service
public class OrderService {
private int count = 0;
public void order() {
count++; // ❌ 멀티쓰레드에서 문제 발생 가능
}
}
해결 방법
- 상태는 지역 변수로 처리
- 혹은 DB, Redis, Session 등 외부 자원에 저장
- 또는 ThreadLocal 사용
8. sleep() vs join() – 둘 다 대기지만 의미는 다르다
| 메서드 | 대상 | 의미 | 예외 |
| sleep(ms) | 자기 자신 | 정해진 시간 동안 정지 | InterruptedException |
| join() | 다른 쓰레드 | 해당 쓰레드가 끝날 때까지 대기 | InterruptedException |
결론:
- sleep()은 내가 쉼
- join()은 남이 끝날 때까지 기다림
9. interrupt() – 쓰레드 중단 제어
- sleep() 또는 join() 중인 쓰레드에 대해 interrupt()를 호출하면 즉시 깨우고 예외 발생
Thread t = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("인터럽트 발생");
}
});
t.start();
t.interrupt(); // 예외 발생하며 깨짐
10. ThreadGroup – 쓰레드 묶음 관리
- 쓰레드를 그룹으로 묶어 activeCount(), interrupt() 등으로 일괄 관리 가능
- 현재는 거의 사용되지 않고 ExecutorService가 대체
ThreadGroup group = new ThreadGroup("MyGroup");
Thread t1 = new Thread(group, () -> {}, "t1");
11. 쓰레드 상태 (Thread.State)
| 상태 | 설명 |
| NEW | 생성됨 (start() 전) |
| RUNNABLE | 실행 대기 또는 실행 중 |
| BLOCKED | 락이 풀릴 때까지 대기 |
| WAITING | 무기한 대기 (join, wait) |
| TIMED_WAITING | 시간 제한 대기 (sleep, join(1000)) |
| TERMINATED | 종료됨 |
System.out.println(thread.getState());
12. wait(), notify(), notifyAll() – 객체 락 기반 쓰레드 협업
synchronized(lock) {
lock.wait(); // 현재 쓰레드 대기 상태
lock.notify(); // 대기 중인 쓰레드 하나 깨움
}
- synchronized 블록 안에서만 호출 가능
- 매우 정교한 제어가 가능하지만 어렵고 실수하기 쉬움
13. volatile – 가시성 보장, 하지만 원자성 없음
private volatile boolean running = true;
- 쓰레드 간 캐시 불일치를 방지
- 하지만 running++ 같은 연산은 여전히 unsafe → AtomicInteger 등 사용 필요
14. ThreadLocal – 쓰레드별 독립 상태 관리
ThreadLocal<Integer> local = ThreadLocal.withInitial(() -> 0);
local.set(100);
int val = local.get(); // 해당 쓰레드만의 값
- Spring Security, 트랜잭션, 로그인 정보 등에 사용
15. ReentrantLock – 고급 락 제어
Lock lock = new ReentrantLock();
try {
lock.lock();
// 임계 영역
} finally {
lock.unlock();
}
- tryLock(), fair, Condition 등을 통해 유연한 락 제어 가능
16. ExecutorService – 쓰레드 풀
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> System.out.println("작업 실행"));
executor.shutdown();
- 쓰레드를 재사용하여 성능과 안정성 향상
- shutdown()으로 종료 필수
'기타 > 자바의 신' 카테고리의 다른 글
| (자바) 자바 I/O 기본 *자바의 신 DAY 10 (0) | 2025.07.13 |
|---|---|
| (자바) 컬렉션 관련 정리 (자바의신 day9) (0) | 2025.07.06 |
| (자바) 내부 클래스란? (자바의 신 day7) (1) | 2025.07.01 |
| (자바)자바의 신 day6 - Java String 완벽 정리: intern(), 문자열 덧셈, 최적화 (0) | 2025.06.29 |
| (자바) 자바의 신 day5 - 자바에서 업캐스팅과 다운캐스팅, equals와 hashCode, toString의 역할 및 오버라이딩 이유 (0) | 2025.06.25 |