[Java] Chapter 14. 멀티스레드

SeungWoo Cha·2025년 9월 13일

[Java] 이것이 자바다

목록 보기
12/17

Chapter 14. 멀티스레드

14.1 멀티 스레드 개념

  • 프로세스(Process): 운영체제가 실행 중인 프로그램.
  • 멀티태스킹(Multi-Tasking): 두 개 이상의 작업을 동시에 처리하는 것. (반드시 멀티 프로세스일 필요는 없음)
  • 스레드(Thread): 코드 실행의 최소 단위(흐름).

멀티 프로세스 vs 멀티 스레드

  • 멀티 프로세스: 독립적 실행. 하나가 오류 나도 다른 프로세스에 영향 없음.
  • 멀티 스레드: 프로세스 내부에서 동작. 하나의 스레드 예외 발생 시 전체 프로세스 종료 위험.
  • 활용 예시: 서버에서 다수의 클라이언트 요청 처리.

14.2 메인 스레드

  • 모든 자바 프로그램은 메인 스레드(main 메소드)에서 시작.
  • main() 코드 실행 후 return을 만나면 종료.

싱글 스레드 vs 멀티 스레드

  • 싱글 스레드: 메인 스레드 종료 = 프로세스 종료.
  • 멀티 스레드: 메인 스레드가 종료돼도 다른 스레드가 실행 중이면 프로세스는 종료되지 않음.
public class MainThreadExample {
    public static void main(String[] args) {
        Thread worker = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("작업 스레드 실행 중: " + i);
                try { Thread.sleep(500); } catch (InterruptedException e) {}
            }
        });
        worker.start();
        System.out.println("메인 스레드 종료"); // 메인 스레드가 끝나도 worker가 남아있음
    }
}

14.3 작업 스레드 생성과 실행

  • 멀티스레드로 동작하는 프로그램을 개발하려면, 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 작업별로 스레드를 생성해야 한다.

  • 자바 프로그램은 항상 메인 스레드가 존재하므로, 메인 작업 이외에 실행할 추가 작업의 수만큼 스레드를 생성하면 된다.

  • 자바에서는 작업 스레드도 객체로 관리되므로, 스레드를 정의하려면 Runnable을 구현하거나 Thread를 상속하는 등의 클래스(혹은 익명 구현)가 필요하다.

1. Thread + Runnable 사용

class Task implements Runnable {
    @Override
    public void run() {
        System.out.println("작업 스레드 실행!");
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        Runnable task = new Task();
        Thread thread = new Thread(task);
        thread.start(); // run() 직접 호출이 아니라 start()로 실행
    }
}

익명 클래스 활용:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("익명 Runnable 실행!");
    }
});
thread.start();

람다식 활용 (자주 사용됨):

Thread thread = new Thread(() -> {
    System.out.println("람다식으로 실행!");
});
thread.start();

2. Thread 클래스 상속

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread 상속 실행!");
    }
}

public class ExtendThreadExample {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
    }
}

14.4 스레드 이름

  • 기본 이름: main, Thread-0, Thread-1
  • 이름 변경: setName("이름")
  • 현재 실행 스레드 확인: Thread.currentThread()
public class ThreadNameExample {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();
        System.out.println("메인 스레드 이름: " + mainThread.getName());

        Thread worker = new Thread(() -> {
            System.out.println("작업 스레드 실행: " +
                Thread.currentThread().getName());
        });
        worker.setName("Worker-1");
        worker.start();
    }
}

14.5 스레드 상태

스레드의 주요 상태:

  • NEW → 객체 생성됨 (new Thread())
  • RUNNABLE(실행 대기)start() 호출 시 진입
  • RUNNING(실행) → CPU 점유 시
  • WAITING/ TIMED_WAITING(일시 정지)sleep(), join(), wait()
  • TERMINATED(종료) → run() 종료

상태 전환 메소드

  • 일시 정지

    • sleep(long millis) → 지정 시간 동안 정지 후 자동 대기 상태
    • join() → 특정 스레드 종료까지 대기
    • wait() → 동기화 블록 내 정지
  • 재실행

    • interrupt() → InterruptedException 발생시켜 복귀
    • notify(), notifyAll()wait() 상태 해제
  • 양보

    • yield() → 실행 상태 → 실행 대기 상태 전환

예제: sleep() & join()

public class JoinExample {
    public static void main(String[] args) {
        Thread sumThread = new Thread(() -> {
            int sum = 0;
            for (int i = 1; i <= 5; i++) {
                sum += i;
                try { Thread.sleep(500); } catch (InterruptedException e) {}
            }
            System.out.println("합계: " + sum);
        });

        sumThread.start();
        try {
            sumThread.join(); // sumThread가 끝날 때까지 main 대기
        } catch (InterruptedException e) {}

        System.out.println("메인 스레드 종료");
    }
}

14.6 스레드 동기화 (synchronized)

  • 여러 스레드가 하나의 공유 객체를 동시에 접근하면 데이터 불일치 문제가 발생할 수 있음.
  • 이를 방지하기 위해 한 스레드가 작업하는 동안 다른 스레드가 접근하지 못하도록 객체 잠금(lock) 필요.
  • 자바는 synchronized 키워드로 동기화 메소드 또는 동기화 블록을 제공.

동기화 메소드

public class SharedCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SyncMethodExample {
    public static void main(String[] args) throws InterruptedException {
        SharedCounter counter = new SharedCounter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

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

        System.out.println("최종 카운트: " + counter.getCount()); // 항상 2000
    }
}

동기화 블록

public void add(int value) {
    // 여러 스레드가 접근 가능한 영역
    synchronized (this) {
        // 단 하나의 스레드만 실행 가능
        count += value;
    }
    // 다시 여러 스레드 접근 가능
}

wait() & notify() 활용 (교대 작업)

  • wait(): 현재 스레드를 일시 정지 상태로 보냄.
  • notify(): 대기 중인 다른 스레드 하나를 깨움.
  • 반드시 동기화 블록 내에서 사용해야 함.
class WorkObject {
    public synchronized void methodA() {
        System.out.println("ThreadA 실행");
        notify();  // 상대 스레드 깨움
        try { wait(); } catch (InterruptedException e) {}
    }
    public synchronized void methodB() {
        System.out.println("ThreadB 실행");
        notify();
        try { wait(); } catch (InterruptedException e) {}
    }
}

public class WaitNotifyExample {
    public static void main(String[] args) {
        WorkObject work = new WorkObject();

        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 5; i++) work.methodA();
        });

        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 5; i++) work.methodB();
        });

        threadA.start();
        threadB.start();
    }
}

14.7 스레드 안전 종료

스레드는 run() 종료 시 자동으로 끝남. 하지만 즉시 멈춰야 할 경우 안전 종료가 필요.
stop() 메소드는 위험하므로 사용하지 않음.

방법 1: 조건 변수 활용

public class SafeStopThread extends Thread {
    private volatile boolean stop = false;

    public void setStop(boolean stop) {
        this.stop = stop;
    }

    @Override
    public void run() {
        while (!stop) {
            System.out.println("스레드 실행 중...");
        }
        System.out.println("자원 정리 후 종료");
    }

    public static void main(String[] args) throws InterruptedException {
        SafeStopThread thread = new SafeStopThread();
        thread.start();
        Thread.sleep(1000);
        thread.setStop(true);
    }
}

방법 2: interrupt() 활용

public class InterruptExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                while (true) {
                    System.out.println("작업 중...");
                    Thread.sleep(200); // 일시 정지 상태
                }
            } catch (InterruptedException e) {
                System.out.println("인터럽트 발생, 안전 종료");
            }
        });

        thread.start();
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        thread.interrupt(); // 강제 종료 유도
    }
}

14.8 데몬 스레드

  • 데몬 스레드는 주 스레드의 작업을 보조하는 역할을 수행하며, 주 스레드가 종료되면 데몬 스레드도 자동으로 종료된다.
  • 스레드를 데몬으로 만들려면, 해당 스레드 객체에 setDaemon(true)를 호출하면 된다.
  • 데몬 스레드는 모든 작업 스레드가 종료되면 강제로 종료되며, 주로 백그라운드 작업에 활용된다.
  • 스레드를 생성한 후 데몬 스레드로 선언하여 실행하면 된다.
public class DaemonExample {
    public static void main(String[] args) {
        Thread autoSave = new Thread(() -> {
            while (true) {
                System.out.println("자동 저장 실행...");
                try { Thread.sleep(500); } catch (InterruptedException e) {}
            }
        });
        autoSave.setDaemon(true); // 데몬 스레드 지정
        autoSave.start();

        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        System.out.println("메인 종료 → 데몬 스레드도 종료됨");
    }
}

14.9 스레드 풀 (Thread Pool)

  • 병렬 작업으로 인해 스레드가 과도하게 생성되는 것을 방지하기 위해 스레드 풀을 사용하는 것이 좋다.
  • 스레드 풀은 작업 처리를 위해 제한된 개수의 스레드를 유지하고, 작업 큐에 들어오는 작업을 스레드가 하나씩 처리하는 방식으로 운영된다.
  • 작업 처리가 끝난 스레드는 다시 큐에서 새로운 작업을 가져와 처리한다.

1. 스레드 풀 생성

  • 자바는 java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Executors 클래스를 제공하여 스레드 풀을 쉽게 생성할 수 있다.
  • Executors의 정적 메소드를 사용하면 간단히 ExecutorService 구현 객체를 만들 수 있다.

newCachedThreadPool()

  • 초기 스레드 수와 코어 수는 0이며, 작업 개수가 많아지면 필요한 만큼 새 스레드를 생성하여 처리한다.
  • 60초 동안 작업이 없으면 해당 스레드를 풀에서 제거한다.

newFixedThreadPool(n)

  • 초기 스레드 수는 0이며, 최대 n개의 스레드를 생성하여 작업을 처리한다.
  • 생성된 스레드는 제거되지 않고 계속 유지된다.

2. 스레드 풀 종료

  • 스레드 풀의 스레드는 기본적으로 데몬 스레드가 아니므로, main 스레드가 종료되어도 작업이 남아있으면 계속 실행 상태로 남는다.
  • 따라서 스레드 풀을 종료하려면 ExecutorService의 다음 메소드 중 하나를 호출해야 한다.
  1. shutdown() : 현재 처리 중인 작업과 큐에 대기 중인 모든 작업을 마친 후 스레드 풀 종료
  2. shutdownNow() : 현재 실행 중인 작업을 인터럽트하여 강제로 종료하고, 큐에 남아 있는 미처리 작업 목록을 반환
  • 남아 있는 작업을 마무리한 후 종료하려면 shutdown()을, 강제 종료할 경우에는 shutdownNow()를 호출하면 된다.

3. 작업 생성과 처리 요청

  • 하나의 작업은 Runnable 또는 Callable 구현 객체로 표현할 수 있다.
  • Runnable은 작업 완료 후 반환값이 없으며, Callable은 작업 완료 후 결과 값을 반환한다.
  • Callable의 반환 타입은 Callable<T>에서 지정한 T 타입과 동일해야 한다.
  • 작업 처리 요청이란, ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 의미한다.

스레드 풀 예제

import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // Runnable 작업 제출
        executor.execute(() -> {
            System.out.println("Runnable 작업 실행: " + Thread.currentThread().getName());
        });

        // Callable 작업 제출
        Future<Integer> future = executor.submit(() -> {
            int sum = 0;
            for (int i = 1; i <= 5; i++) sum += i;
            return sum;
        });

        try {
            System.out.println("Callable 결과: " + future.get());
        } catch (Exception e) { e.printStackTrace(); }

        executor.shutdown(); // 남은 작업 끝내고 종료
    }
}

profile
한 발자국씩

0개의 댓글