기타/자바의 신

(자바)자바 쓰레드(Thread) 완전 정리 *자바의 신

불광동 물주먹 2025. 7. 11. 01:02

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()으로 종료 필수