Thread(쓰레드)

mskimdev·2026년 3월 13일

Java Library

목록 보기
4/4

Thread (스레드)

프로그램은 기본적으로 위에서 아래로 순서대로 실행된다. 그런데 파일을 다운로드하면서 동시에 다른 작업을 할 수 있는 건 어떻게 가능한 걸까? 그 답이 바로 스레드다.


스레드란?

스레드는 프로그램 안에서 독립적으로 실행되는 작업 단위다.

하나의 프로세스(실행 중인 프로그램) 안에서 여러 스레드가 동시에 실행될 수 있다. 마치 한 주방에서 여러 요리사가 각자 다른 요리를 동시에 만드는 것과 같다.

  • 프로세스: 주방 전체 (독립된 메모리 공간)
  • 스레드: 각각의 요리사 (프로세스의 메모리를 공유하며 각자 작업)

자바 프로그램을 실행하면 기본적으로 main 스레드 하나가 시작된다. 여기서 추가 스레드를 만들면 여러 작업을 동시에 처리할 수 있다.


스레드 생성 방법

자바에서 스레드를 만드는 방법은 두 가지다.

1. Thread 클래스 상속

public class MyThread extends Thread {

    @Override
    public void run() {
        // 스레드가 실행할 작업을 여기에 작성한다
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + ": " + i);
        }
    }
}

// 사용
MyThread t = new MyThread();
t.start(); // run()을 직접 호출하면 안 된다, start()를 호출해야 새 스레드로 실행된다

2. Runnable 인터페이스 구현 (권장)

자바는 단일 상속만 지원하기 때문에 Thread를 상속하면 다른 클래스를 상속받을 수 없다. 그래서 실무에서는 Runnable을 구현하는 방식을 더 많이 쓴다.

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

// 사용
Thread t = new Thread(new MyRunnable());
t.start();

// 람다식으로 더 간결하게 작성할 수도 있다
Thread t2 = new Thread(() -> {
    System.out.println("람다로 작성한 스레드");
});
t2.start();

run()을 직접 호출하면 새 스레드가 생성되지 않고 현재 스레드에서 일반 메서드처럼 실행된다. 반드시 start()를 호출해야 새로운 스레드가 생성되고 run()이 그 스레드에서 실행된다.


스레드의 생명주기

스레드는 생성부터 종료까지 여러 상태를 거친다.

NEW → RUNNABLE → RUNNING → TERMINATED
                    ↕
                BLOCKED / WAITING / TIMED_WAITING
  • NEW: new Thread()로 생성만 된 상태
  • RUNNABLE: start() 호출 후 실행 대기 중인 상태
  • RUNNING: 실제로 CPU를 점유하며 실행 중인 상태
  • BLOCKED/WAITING: 다른 스레드를 기다리거나 잠금을 기다리는 상태
  • TERMINATED: run() 메서드가 종료되어 스레드가 완전히 끝난 상태

동기화 문제 (Race Condition)

여러 스레드가 같은 데이터에 동시에 접근하면 예상치 못한 결과가 생긴다. 이를 경쟁 조건(Race Condition) 이라고 한다.

public class Counter {
    int count = 0;

    public void increment() {
        count++; // 이 연산은 사실 3단계 (읽기 → 더하기 → 쓰기)
    }
}

Counter counter = new Counter();

// 두 스레드가 동시에 increment()를 1000번 호출
// 예상: 2000, 실제: 매번 다른 값이 나올 수 있다

count++는 단순해 보이지만 내부적으로 "읽기 → 더하기 → 쓰기" 3단계로 실행된다. 두 스레드가 동시에 이 과정에 끼어들면 한쪽의 증가가 덮어씌워지는 문제가 생긴다.

synchronized로 해결하기

synchronized 키워드를 붙이면 한 번에 하나의 스레드만 해당 메서드에 접근할 수 있다.

public class Counter {
    int count = 0;

    public synchronized void increment() {
        count++; // 한 스레드가 실행 중이면 다른 스레드는 대기한다
    }
}

자물쇠를 거는 것과 같다. 한 스레드가 작업 중이면 다른 스레드는 자물쇠가 풀릴 때까지 기다린다. 안전하지만 대기 시간이 생기므로 성능이 낮아질 수 있다.


주요 메서드

메서드설명
start()새 스레드를 생성하고 run()을 실행한다
sleep(ms)지정한 시간(밀리초)만큼 현재 스레드를 일시 정지한다
join()해당 스레드가 끝날 때까지 현재 스레드가 기다린다
interrupt()대기 중인 스레드를 깨운다
isAlive()스레드가 아직 실행 중인지 확인한다
Thread t = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("작업 중: " + i);
    }
});

t.start();
t.join(); // t 스레드가 끝날 때까지 main 스레드가 여기서 기다린다
System.out.println("t 스레드 작업 완료");

마무리

스레드는 강력하지만 다루기 까다로운 개념이다. 동시성 문제는 재현하기도 어렵고, 디버깅도 쉽지 않다. 스레드를 직접 다루는 코드보다는 자바가 제공하는 ExecutorService, CompletableFuture 같은 고수준 API를 사용하는 것이 실무에서는 일반적이다. 스레드의 동작 원리를 이해한 뒤 그쪽으로 나아가면 된다.

profile
<- 개발 공부하는 나

0개의 댓글