Thread

2025. 11. 28. 15:52·java

#Thread란?

Java로 프로그램을 만들다 보면 자연스럽게 마주치는 개념이 프로세스(Process)와 스레드(Thread)다.
처음 접할 때는 단순히 병렬 실행 정도로 이해하기 쉽지만 실제로는 그보다 다양한 상황에 쓰인다.
이번 글에서는 기본 구조부터 직접 구현한 예제까지 한 번에 정리해 본다.


# 프로세스와 스레드의 역할

운영체제에서 프로그램이 실행되면 하나의 프로세스가 만들어진다.
프로세스 안에는 기본적으로 메인 스레드(main thread)가 존재하고
우리가 코드에서 생성하는 스레드는 여기에 추가로 붙는 작업 단위라고 보면 된다.

프로세스(Process): 실행 중인 프로그램 전체
스레드(Thread): 프로세스 내부에서 실제로 작업을 수행하는 흐름(실행 단위)
한 프로세스에는 최소 1개의 메인 스레드가 존재

# Thread의 기본 실행 흐름

스레드는 생성만 한다고 실행되는 게 아니다.
실제로 CPU에서 동작하려면 다음 흐름을 거친다.

start() → Runnable → run() 실행(CPU 점유)

그리고 스레드는 상황에 따라 다음 상태로 이동한다.


상태 설명
Runnable 실행 가능 상태(언제든 CPU 잡으면 실행됨)
Not Runnable sleep(), wait(), join() 등에 의해 잠시 멈춤
Terminated run() 메소드 종료 후 종료
wait() → notifyAll()로 깨어나는 구조는 이후에 등장한다.

# Thread 생성 방법

상속하여 만드는 구조

public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread() + " start");
        for (int i = 1; i <= 10; i++) {
            System.out.println(i + " ");
        }
    }
}

실행 코드

class UseT {
    public static void main(String[] args) {
        var t1 = new MyThread("myThread-1");
        var t2 = new MyThread("myThread-2");

        t1.start();
        t2.start();
    }
}

# 예제: 경주마 스레드 게임 만들기

다음은 입력된 말의 숫자만큼 각각 독립적인 스레드가 움직이며
10번 이동하면 결승점을 통과하는 구조이다.

public class Horse implements Runnable {
    private String name;
    public Horse(String name) { this.name = name; }

    @Override
    public void run() {
        int distance = 0;
        Random r = new Random();

        for (int i = 0; i < 10; i++) {
            distance += 10;

            if (distance == 100)
                System.out.println(name + " reached~");
            else
                System.out.println(name + " 이동 : " + distance + "m");

            try {
                Thread.sleep(r.nextInt(1000) + 1);
            } catch (Exception e) {}
        }
    }
}

실행부

class Main {
    public static void main(String[] args) {
        System.out.println("게임 말의 개수 입력:");
        Scanner sc = new Scanner(System.in);
        int num = sc.nextInt();

        IntStream.range(0, num)
                .forEach(i -> new Thread(new Horse("[" + i + "]")).start());
    }
}

# join() - 특정 스레드가 끝날 때까지 대기하기

join()은 다른 스레드가 끝날 때까지 현재 스레드를 잠시 멈추게 하는 기능이다.

public class JoinT {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try { Thread.sleep(2000); } catch (Exception ignored) {}
            System.out.println("Thread 1 done");
        });

        Thread t2 = new Thread(() -> {
            try { Thread.sleep(2000); } catch (Exception ignored) {}
            System.out.println("Thread 2 done");
        });

        t1.start();
        t2.start();

        // main 스레드가 t1/t2 종료를 기다림
        t1.join();
        t2.join();

        System.out.println("모든 작업 종료");
    }
}

# 하나의 파일로 합치기 - 싱글 스레드 방식

여러 텍스트 파일을 읽어서 하나의 파일로 합치는 간단한 작업도
스레드를 사용하지 않으면 파일을 순차적으로 읽어서 결과를 만든다.

class FileReadTask {
    private List<String> results;
    private String fileName;

    public FileReadTask(String fileName, List<String> results) {
        this.fileName = fileName;
        this.results = results;
    }

    public void read() {
        try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
            String line;
            while ((line = br.readLine()) != null)
                results.add(line);
        } catch (Exception e) {}
    }
}

# 멀티 스레드로 파일 병합하기

같은 작업을 여러 스레드가 동시에 파일을 읽도록 개선한 버전이다.

💡포인트

공유 리스트는 동기화 필요 → Collections.synchronizedList()
파일당 1개의 스레드 생성
모든 스레드 종료까지 기다렸다가 파일 쓰기 실행
public class FileReadTask extends Thread {
    private final List<String> results;
    private final String fileName;

    public FileReadTask(String fileName, List<String> results) {
        this.fileName = fileName;
        this.results = results;
    }

    @Override
    public void run() {
        try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
            String line;
            while ((line = br.readLine()) != null)
                results.add(line);
        } catch (Exception e) {}
    }
}

메인 코드

class FileReaderMain {
    public static void main(String[] args) {
        List<String> results = Collections.synchronizedList(new ArrayList<>());
        String[] files = {"file1.txt", "file2.txt", "file3.txt"};
        List<Thread> threads = new ArrayList<>();

        for (String file : files) {
            var task = new FileReadTask(file, results);
            task.start();
            threads.add(task);
        }

        // 모든 파일 읽기 완료될 때까지 대기
        for (Thread t : threads) {
            try { t.join(); } catch (Exception ignored) {}
        }

        try (BufferedWriter bw = new BufferedWriter(new FileWriter("result.txt"))) {
            for (String line : results) {
                bw.write(line);
                bw.newLine();
            }
            System.out.println("파일 병합 완료");
        } catch (Exception e) {}
    }
}

# wait() / notifyAll() - Producer–Consumer 구조 구현

이제 동기화에서 가장 중요한 기능인 wait()와 notifyAll()을 사용해 보자.
예시는 도서관 좌석 3개를 학생 6명이 공유하는 시나리오로 만들었다.

💡포인트

자리가 없으면 wait() → 스레드 잠시 대기
누군가 자리를 반납하면 notifyAll() → 대기 중인 스레드 모두 깨움
좌석 관리는 synchronized 블록 내에서만 가능
public class Library {
    private static Library Instance;
    private List<String> seats;

    private Library() {
        seats = new ArrayList<>(Arrays.asList("1번 자리", "2번 자리", "3번 자리"));
    }

    public synchronized static Library getInstance() {
        if (Instance == null) Instance = new Library();
        return Instance;
    }

    public synchronized String study() throws InterruptedException {
        if (seats.isEmpty()) {
            System.out.println(Thread.currentThread().getName() + " wait!");
            wait();
        }

        if (!seats.isEmpty()) {
            var seat = seats.remove(0);
            System.out.println("Use seat = " + seat);
            return seat;
        }
        return null;
    }

    public synchronized void returnSeat(String seat) {
        seats.add(seat);
        notifyAll();
        System.out.println(Thread.currentThread().getName() + " returned: " + seat);
    }
}

학생 스레드

class Student extends Thread {
    public void run() {
        try {
            String seat = Library.getInstance().study();
            if (seat == null) return;

            sleep(2000);
            Library.getInstance().returnSeat(seat);
        } catch (Exception e) {}
    }
}

실행

class UseApp {
    public static void main(String[] args) {
        IntStream.range(0, 6).forEach(i -> new Student().start());
    }
}

'java' 카테고리의 다른 글

Stream  (0) 2025.11.28
Lambda Expression(=람다식)  (0) 2025.11.28
Generic Type(=제네릭 타입)  (0) 2025.11.28
Abstract class(=추상화)  (0) 2025.11.28
Encapsulation(=캡슐화)  (0) 2025.11.28
'java' 카테고리의 다른 글
  • Stream
  • Lambda Expression(=람다식)
  • Generic Type(=제네릭 타입)
  • Abstract class(=추상화)
차하비
차하비
chadev 님의 블로그 입니다.
  • 차하비
    chadev
    차하비
  • 전체
    오늘
    어제
    • 분류 전체보기 (47)
      • kubernetes, k8s (3)
      • docker (3)
      • cloud (6)
      • linux (12)
      • spring (7)
      • java (11)
      • error (4)
      • 자격증 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    restAPI
    monolithic
    비트 연산
    객체지향
    진법
    HTTP
    KUBECTL
    thread
    k8s
    모놀리식
    URI
    클래스
    Spring
    Java
    Kubernetes
    docker
    url
    OOP
    Join
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
차하비
Thread
상단으로

티스토리툴바