자바 스레드 (2) - 스레드 생성과 실행 방법
프로젝트 중 느낀 자바 기초의 부족함을 보완하고자 핵심 주제를 순차적으로 복습할 예정입니다.
자바 코어 → 자바 스레드 → 자바 OOP → 자바의 예외 → 자바의 컬렉션 → 최대 절전 모드 순으로 정리할 예정입니다.
스레드 생성과 실행 방법
- 얼마나 많은 방법으로 Java에서 스레드를 만들 수 있습니까?
- Runnable 클래스를 구현하여 스레드를 생성하는 방법을 설명합니다.
- Thread 클래스를 확장하여 스레드를 생성하는 방법을 설명합니다.
- 스레드를 생성하는 가장 좋은 방법은 무엇입니까?
- Java에서 스레드 스케줄러의 중요성을 설명하십시오.
얼마나 많은 방법으로 Java에서 스레드를 만들 수 있습니까?
자바에서 스레드를 생성하는 방법은 크게 세 가지 주요 방식이 있습니다.
첫째, Thread 클래스를 직접 상속하고 run() 메서드를 오버라이드하는 방식입니다.
둘째, Runnable 인터페이스를 구현하고 이를 Thread 생성자의 인자로 전달하는 방식입니다.
셋째, Callable 인터페이스와 FutureTask 또는 ExecutorService를 함께 사용하여 결과값을 반환하는 비동기 작업을 수행하는 방식입니다.
이 외에도 JDK 1.5 이후로는 Executor, ExecutorService, ForkJoinPool, CompletableFuture 등 다양한 고수준 API가 제공되어 직접적인 스레드 생성 없이도 병렬 작업을 수행할 수 있습니다. 실무에서는 일반적으로 직접적인 스레드 생성은 지양하고, 스레드 풀 기반의 구조를 선호합니다.
Runnable 클래스를 구현하여 스레드를 생성하는 방법을 설명합니다.
Runnable 인터페이스는 함수형 인터페이스로, 반환값 없이 실행만 하는 작업을 정의할 때 사용됩니다. Runnable을 구현한 클래스 또는 람다를 Thread의 생성자에 전달하여 실행할 수 있습니다.
예를 들어 다음과 같이 구현합니다:
Runnable task = () -> {
System.out.println("Runnable 실행 중");
};
Thread thread = new Thread(task);
thread.start();
이 방식은 Thread 클래스를 상속하지 않기 때문에 다중 상속이 필요한 클래스 설계에 유리하며, 구현과 실행을 분리할 수 있어 유지보수 측면에서도 효과적입니다. 또한 람다식과 함께 사용할 수 있어 코드가 간결해집니다.
Thread 클래스를 확장하여 스레드를 생성하는 방법을 설명합니다.
Thread 클래스를 직접 상속하여 스레드를 생성할 수 있으며, 이 경우 run() 메서드를 오버라이드하여 실행할 작업을 정의합니다. 이 방법은 간단하지만, 이미 다른 클래스를 상속하고 있다면 사용할 수 없습니다.
예시는 다음과 같습니다:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread 실행 중");
}
}
Thread t = new MyThread();
t.start();
이 방식은 학습 목적이나 단순한 구조에서는 유용하지만, 설계의 유연성이 떨어지고 재사용성이 낮다는 단점이 있습니다. 실무에서는 대체로 Runnable 기반의 구현 또는 ExecutorService를 활용한 구조가 더 많이 사용됩니다.
스레드를 생성하는 가장 좋은 방법은 무엇입니까?
가장 바람직한 방법은 직접 스레드를 생성하지 않고 ExecutorService 또는 ForkJoinPool 등 고수준 API를 사용하는 것입니다. 이 방식은 내부적으로 스레드 풀(Thread Pool) 을 활용하여 스레드를 재사용하고, 자원 관리 및 예외 처리를 체계적으로 처리할 수 있게 해 줍니다.
예를 들어 Executors.newFixedThreadPool() 을 사용하면 지정된 개수의 스레드를 유지하며 작업을 큐에 쌓아 처리할 수 있습니다. 이는 직접 스레드를 생성하는 방식과 달리, 과도한 스레드 생성으로 인한 시스템 과부하를 방지하고, 작업 큐를 통해 백프레셔(Backpressure)를 적용할 수 있다는 점에서 실무적으로 유리합니다.
Java에서 스레드 스케줄러의 중요성을 설명하십시오.
자바에서 스레드 스케줄러(Thread Scheduler)는 여러 개의 스레드가 동시에 실행 가능한 상태(RUNNABLE)에 있을 때, 어떤 스레드에 먼저 CPU를 할당할지를 결정하는 핵심 구성 요소입니다. 자바의 스레드는 운영체제의 네이티브 스레드와 1:1로 매핑되기 때문에, 스케줄링 결정은 JVM 내부가 아니라 운영체제 커널 수준의 스레드 스케줄러 정책에 위임됩니다. 이로 인해 자바 프로그램의 실행 흐름은 JVM보다 OS의 스케줄러 정책에 더 직접적인 영향을 받게 됩니다.
대부분의 현대 운영체제는 프리엠티브(선점형, Preemptive) 스케줄링 방식을 사용합니다. 이는 현재 CPU를 점유하고 있는 스레드를 강제로 중단시키고, 다른 스레드로 전환할 수 있는 구조를 의미합니다. 예를 들어, 스레드 A가 실행 중일 때, 스레드 B가 새로 생성되어 RUNNABLE 상태가 되더라도, B가 즉시 실행된다는 보장은 없습니다. 운영체제가 해당 시점에 A의 실행을 유지할 수도, B에게 CPU를 넘길 수도 있으며, 이 결정은 전적으로 커널 스케줄러에 의존합니다. 개발자가 Thread.start()를 호출한다고 해서 해당 스레드가 즉시 실행되거나, 특정 순서대로 처리된다는 것은 보장되지 않습니다.
자바는 Thread.setPriority() 메서드를 통해 스레드 우선순위를 지정할 수 있도록 지원하지만, 이는 스케줄러에 대한 힌트(hint) 수준에 불과하며, 실제 우선순위가 반영될지 여부는 OS마다 다릅니다. 예를 들어, Linux는 Completely Fair Scheduler(CFS)를 채택하여 공정성을 기반으로 CPU를 분배하며, 우선순위를 거의 무시합니다. 반면 Windows는 일정 부분 우선순위를 반영하지만, 기아 상태(starvation)를 방지하기 위한 내부 보정 로직이 존재합니다. macOS 또한 QoS(Quality of Service)와 같은 추가 정책을 반영하여 스레드 실행을 조정합니다. 이처럼 운영체제마다 스케줄링 정책과 우선순위 반영 방식이 다르기 때문에, 자바에서의 스레드 우선순위는 결정적인 실행 제어 도구로 사용할 수 없습니다.
운영체제 | 스케줄러 종류 | 기본 정책 (Scheduling Policy) | 자바의 setPriority() 반영 여부 | 특징 요약 |
Linux | Completely Fair Scheduler (CFS) | 공정성 기반, 가중치에 따른 가상 실행 시간 관리 | ❌ 거의 반영 안 됨 | I/O 바운드 우선, nice 값 간접 조정 가능 |
Windows | Priority-based Preemptive | 우선순위 기반 + 동적 조정 (Priority Boosting) | ⚠ 부분 반영 | GUI, I/O 태스크에 우선순위 부여, starvation 방지 로직 존재 |
macOS | Hybrid Scheduler (CFS + QoS) | 공정성 + QoS 기반 우선순위 스케줄링 | ⚠ 부분 반영 | QoS 클래스에 따라 스레드 우선 순위 결정 |
이러한 이유로, 멀티스레딩 환경에서 개발자가 스레드 실행 순서나 타이밍에 의존한 로직을 설계하는 것은 매우 위험합니다. "이 스레드가 먼저 실행될 것이다", "이 작업이 끝난 뒤 다음 스레드가 실행되겠지"와 같은 가정은 현실에서 자주 깨지며, 이는 동기화 오류, 경쟁 상태(Race Condition), 교착 상태(Deadlock) 등 치명적인 병행성 문제로 이어질 수 있습니다.
따라서 스레드 간 실행 흐름을 명확하게 제어하기 위해서는 join() 을 사용해 특정 스레드가 종료될 때까지 기다리거나, wait()/notify() 패턴을 통해 공유 객체 기반의 협력 처리를 구현해야 합니다. 보다 구조적인 동기화를 위해서는 CountDownLatch, CyclicBarrier, Semaphore 등 java.util.concurrent 패키지에서 제공하는 고수준 동기화 도구를 사용해야 하며, 복잡한 스레드 관리를 피하기 위해 ExecutorService나 ThreadPoolExecutor 를 통한 스레드 풀 기반 작업 관리가 권장됩니다.
결론적으로, 자바에서 스레드 스케줄러는 멀티스레딩의 실행 시점을 결정짓는 핵심 컴포넌트이자, 정확한 실행 순서를 개발자가 직접 제어할 수 없다는 현실을 인식하게 해주는 요소입니다.
따라서 멀티스레드 프로그램을 작성할 때는 OS 스케줄러의 불확실성과 우선순위 미보장을 고려하고, 항상 명시적인 동기화 설계를 통해 실행 흐름의 안정성과 일관성을 확보하는 것이 필수적입니다.