5. 스레드 제어와 생명 주기2

임대일·2025년 5월 12일

Thread

목록 보기
5/13
post-thumbnail

1. 스레드 작업 중단: runFlag 방식 vs interrupt() 방식

기본적인 작업 중단 방식 (runFlag)

volatile boolean runFlag = true;

while (runFlag) {
    log("작업 중");
    Thread.sleep(3000); // 작업 대기
}
  • main 스레드에서 runFlag = false로 설정해 중단을 지시합니다.
  • 하지만, sleep() 중에는 runFlag를 확인할 수 없어 즉시 반응하지 않습니다.
  • 결과적으로 반응이 느려서 실시간 제어에 부적합합니다.

문제점: sleep()이 끝날 때까지 대기해야 하므로 반응성이 낮습니다.

runFlag 예제 코드

package thread.control.interrupt;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class ThreadStopMainV1 {
  public static void main(String[] args) {
    MyTask task = new MyTask();
    Thread thread = new Thread(task, "work");
    thread.start();

    sleep(4000);
    log("작업 중단 지시 runFlag=false");
    task.runFlag = false;
  }

  static class MyTask implements Runnable {

    volatile boolean runFlag = true;

    @Override
    public void run() {
      while (runFlag) {
        log("작업 중");
        sleep(3000);
      }
      log("자원 정리");
      log("작업 종료");
      
    }
  }
}
14:58:27.520 [     work] 작업 중
14:58:30.525 [     work] 작업 중
14:58:31.510 [     main] 작업 중단 지시 runFlag=false
14:58:33.532 [     work] 자원 정리
14:58:33.533 [     work] 작업 종료

main 스레드가 runFlag를 false로 변경해도, work 스레드는 sleep(3000)을 통해 3초간 잠들어 있습니다. 3초간의 잠이 깬 다음에 while(runFlag) 코드를 실행해야, runFlag를 확인하고 작업을 중단할 수 있습니다. 참고로 runFlag를 변경한 후 2초라는 시간이 지난 이후에 작업이 종료되는 이유는 work 스레드가 3초에 한 번씩 깨어나서 runFlag를 확인하는데, main 스레드가 4초에 runFlag를 변경했기 때문입니다. work 스레드 입장에서 보면 두 번째 sleep()에 들어가고 1초 후 main 스레드가 runFlag를 변경합니다. 3초간 sleep()이므로 아직 2초가 더 있어야 깨어납니다.

interrupt()로 작업 중단

thread.interrupt(); // 다른 스레드에 인터럽트 발생

while (true) {
    log("작업 중");
    Thread.sleep(3000); // 여기서 InterruptedException 발생
}
  • interrupt() 호출 시 sleep() 상태에서 즉시 InterruptedException 발생합니다.
  • 예외를 통해 빠르게 종료 루틴으로 진입 가능합니다.
catch (InterruptedException e) {
    log("자원 정리");
}

장점: sleep() 상태라도 즉시 반응 가능 → 빠른 종료 처리


인터럽트 상태 직접 확인 (isInterrupted(), Thread.interrupted())

while (!Thread.currentThread().isInterrupted()) {
    log("작업 중");
}
  • isInterrupted()는 상태만 확인합니다. (변경 ❌)
  • Thread.interrupted()는 상태를 확인 + 초기화 (truefalse로)

자바는 인터럽트 예외가 한 번 발생하면, 스레드의 인터럽트 상태를 다시 정상(false)으로 돌립니다. 스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생하게 됩니다. 인터럽트의 목적을 달성하면 인터럽트 상태를 다시 정상으로 돌려두어야 합니다.

인터럽트의 상태를 직접 체크해서 사용하는 경우 Thread.interrupted()를 사용하면 이런 부분이 해결됩니다. 참고로 isInterrupted()는 특정 스레드의 상태를 변경하지 않고 확인할 때 사용합니다. 물론 꼭 이것만이 정답은 아닙니다. 예를 들어 너무 긴급한 상황이어서 자원 정리도 하지 않고, 최대한 빨리 스레드를 종료
해야 한다면 해당 스레드를 다시 인터럽트 상태로 변경하는 것도 방법입니다.

주의: 인터럽트가 발생해도 처리하지 않으면 이후 sleep() 등에서 또 예외 발생 가능
→ 필요 시 상태를 직접 초기화하거나, 이미 초기화된 상태로 while 탈출

요약 비교

방식반응성특징
runFlag느림sleep() 중 대기
interrupt()빠름sleep(), wait() 중 즉시 예외 발생
isInterrupted()확인만상태 그대로 유지
Thread.interrupted()확인 후 초기화인터럽트 상태 해제됨

2. 프린터 예제로 보는 인터럽트와 yield() 실전 적용

MyPrinterV1: 기본 구조 (runFlag 방식)

  • main 스레드: 사용자 입력을 받아 큐에 추가
  • printer 스레드: 큐가 비어있지 않으면 출력 (3초 대기)
while (work) {
    if (jobQueue.isEmpty()) continue;
    String job = jobQueue.poll();
    Thread.sleep(3000); // 출력 시간
}

문제점: q를 입력해도 sleep() 중이라 즉시 종료되지 않음

  • volatile: 여러 스레드가 동시에 접근하는 변수에는 volatile 키워드를 붙어주어야 안전합니다. 여기서는 main 스레드, printer 스레드 둘다 work 변수에 동시에 접근할 수 있습니다. volatile에 대한 자세한 내용은 뒤에서 설명합니다.
  • ConcurrentLinkedQueue : 여러 스레드가 동시에 접근하는 경우, 컬렉션 프레임워크가 제공하는 일반적인 자료구조를 사용하면 안전하지 않습니다. 여러 스레드가 동시에 접근하는 경우 동시성을 지원하는 동시성 컬렉션을 사용해야 합니다. Queue의 경우 ConcurrentLinkedQueue를 사용하면 됩니다. 동시성 컬렉션의 자세한 내용은 뒤에서 설명합니다. 여기서는 일반 큐라고 생각하면 됩니다.

MyPrinterV2: 인터럽트 도입

if (input.equals("q")) {
    printer.work = false;
    printerThread.interrupt(); // 인터럽트 사용
}
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    break; // 즉시 종료
}

효과: sleep() 중에도 인터럽트로 즉시 종료 가능
반응성 향상

MyPrinterV3: work 제거, Thread.interrupted() 활용

while (!Thread.interrupted()) {
    if (jobQueue.isEmpty()) continue;
    ...
}
  • interrupted()는 상태 확인 + 초기화
  • 더 이상 work 변수 필요 없음 → 코드 간결해짐

MyPrinterV4: yield() 도입

while (!Thread.interrupted()) {
    if (jobQueue.isEmpty()) {
        Thread.yield(); // CPU 양보
        continue;
    }
    ...
}

이점:

  • 큐가 비어 있을 때 불필요한 CPU 점유 줄이기
  • 다른 스레드에게 CPU 사용 기회 양보

yield vs sleep vs busy-wait

방법CPU 사용특징
continue높음쉴 틈 없이 while 반복 (busy waiting)
sleep()낮음일정 시간 대기, 응답성 떨어질 수 있음
yield()낮음잠깐 CPU 양보, 다음 스케줄러에 따라

결론: 프린터 예제를 통해 얻은 교훈

  • 단순 플래그(runFlag) 방식은 반응성이 낮습니다.
  • interrupt()를 활용하면 즉각적인 종료 제어 가능합니다.
  • Thread.interrupted()를 쓰면 상태 확인 + 초기화로 깔끔하게 종료가 가능합니다.
  • yield()는 불필요한 CPU 낭비를 막고 양보의 힌트를 줄 수 있습니다.

3. yield() 실험: 스레드 양보의 실제 효과

실험 개요

for (int i = 0; i < 1000; i++) {
    new Thread(() -> {
        for (int j = 0; j < 10; j++) {
            System.out.println(Thread.currentThread().getName() + " - " + j);
            // ① 아무 것도 안 함
            // ② sleep(1)
            // ③ Thread.yield()
        }
    }).start();
}

각 스레드는 0~9까지 출력
실험을 통해 스케줄링 방식에 따라 실행 순서와 효율이 어떻게 달라지는지 비교

① 아무것도 안 한 경우 (Empty)

// 아무 것도 호출하지 않음

특징

  • 운영체제의 기본 스케줄링만 적용됨
  • 어떤 스레드는 한 번에 몰아서 실행되기도 함

효과

  • 선점형 스케줄러의 정책에 따라 불균형하게 실행됨

Thread.sleep(1)

Thread.sleep(1);

특징

  • 스레드를 1ms 동안 TIMED_WAITING 상태로 만듦
  • 이후 다시 RUNNABLE 상태로 복귀

효과

  • 명확하게 양보
  • 하지만 스레드 실행이 느려짐 → 부하 많은 작업엔 부적절

Thread.yield()

Thread.yield();

특징

  • 현재 스레드가 자발적으로 CPU 양보
  • RUNNABLE 상태는 유지됨 (단지 다시 대기열로 들어감)

효과

  • 다른 스레드에게 실행 기회를 주지만, 보장되지 않음
  • 스케줄러에 따라 같은 스레드가 계속 실행될 수도 있음

참고로 최근에는 10코어 이상의 CPU도 많기 때문에 스레드 10개 정도만 만들어서 실행하면, 양보가 크게 의미가 없습니다. 양보해도 CPU 코어가 남기 때문에 양보하지 않고 계속 수행될 수 있습니다. CPU 코어 수 이상의 스레드를 만들어야 양보하는 상황을 확인할 수 있습니다. 그래서 이번 예제에서 1000개의 스레드를 실행한 것 입니다.

참고: log()가 사용하는 기능은 현재 시간도 획득해야 하고, 날짜 포멧도 지정해야 하는 등 복잡합니다. 이 사이에 스레드의 컨텍스트 스위칭이 발생하기 쉽습니다. 이런 이유로 스레드의 실행 순서를 일정하게 출력하기 어렵습니다. 그래서 여기서는 단순한 System.out.println()을 사용했습니다.

yield vs sleep 요약 비교

항목yield()sleep()
상태 변화RUNNABLE 유지TIMED_WAITING으로 진입
양보 확실성보장 안됨 (힌트)시간만큼 확실히 양보
성능 영향적음약간 있음 (대기 시간 발생)
언제 쓰나?부하 적고 민감할 때정확한 시간 제어 필요할 때

실전 팁

  • 양보할 스레드가 없는 상황이라면 yield()는 의미가 없다 → 본인이 계속 실행됨
  • CPU 코어 수보다 스레드 수가 많을 때만 yield()의 효과가 드러남
  • 과도한 yield() 또는 sleep()은 오히려 성능 저하 유발 가능

Thread.yield() 정리

개념

  • Thread.yield()현재 실행 중인 스레드가 CPU 사용을 자발적으로 양보하는 메서드입니다.
  • 스레드는 RUNNABLE 상태를 유지한 채 스케줄링 큐의 뒤로 물러나며, 다른 스레드가 실행될 기회를 얻게 합니다.

작동 방식

  • 현재 스레드가 CPU를 양보하고 동일 우선순위의 다른 스레드에게 기회를 주도록 힌트를 줍니다.
  • 단, 강제성은 없습니다. 스케줄러가 무시할 수도 있습니다.
Thread.yield();

특징 요약

항목설명
상태여전히 RUNNABLE 상태 유지
스케줄링스케줄러에게 힌트 제공 (필수 아님)
목적다른 스레드에게 실행 기회 양보
반환void, 실행 결과를 확인할 수 없음
효과CPU 자원을 덜 소모할 수 있음 (busy loop 방지 등)

언제 사용하나?

  • while 루프에서 큐 상태를 polling할 때, CPU 낭비 방지를 위해
  • 다른 스레드에게 작업 처리 기회를 빠르게 주고 싶을 때
  • 인터럽트 전까지 계속 체크해야 하는 반복 루프 안
while (!Thread.interrupted()) {
    if (jobQueue.isEmpty()) {
        Thread.yield(); // CPU 양보
        continue;
    }
    ...
}

주의사항

  • 스레드 수가 적거나 CPU 코어 수보다 적으면 yield()는 별 효과 없음
  • 양보할 스레드가 없으면 본인이 계속 실행될 수 있음
  • 실시간 제어나 정밀 스케줄링에는 적합하지 않음
profile
🤔오늘의 복습은❓

0개의 댓글