[Java] 36. 스레드 제어와 생명 주기(2)

2025. 7. 1. 16:05·Java/멀티스레드&동시성

 

스레드 제어와 생명 주기(2)

#Java/멀티스레드


정리

작업 중단 - 1. 변수

특정 스레드의 작업을 중간에 중단하려면 어떻게 해야할까?

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("작업 종료");
        }
    }
}
  • 특정 스레드의 작업을 중단하는 가장 쉬운 방법은 변수를 사용하는 것이다.
  • 여기서는 runFlag 를 사용해서 work 스레드에 작업 중단을 지시할 수 있다.

 


변수를 이용한 인터럽트 방식의 문제점
main 스레드가 runFlag=false 를 통해 작업 중단을 지시해도, work 스레드가 즉각 반응하지 않는다.

while (runFlag) {
    log("작업 중");
    sleep(3000);
}

main 스레드가 runFlag 를 false 로 변경해도, work 스레드는 sleep(3000) 을 통해 3초간 잠들어 있다. 3초간의 잠이 깬 다음에 while(runFlag) 코드를 실행해야, runFlag 를 확인하고 작업을 중단할 수 있다.


어떻게 하면 sleep() 처럼 스레드가 대기하는 상태에서 스레드를 깨우고, 작업도 빨리 종료할 수 있을까?


작업 중단 2. 인터럽트를 통해 처리

인터럽트를 사용하면, WAITING, TIMED_WAITING 같은 대기 상태의 스레드를 직접 깨워서, 작동하는 RUNNABLE 상태로 만들 수 있다.


MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start(); // 스레드는 3초 단위로 sleep
sleep(4000);
log("작업 중단 지시 thread.interrupt()");
thread.interrupt();
log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());

// MyTask
@Override
public void run() {
	try {
		while (true) {
			log("작업 중");
            Thread.sleep(3000);
		}
	} catch (InterruptedException e) {
		log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
		log("interrupt message=" + e.getMessage());
		log("state=" + Thread.currentThread().getState());
	}
	log("자원 정리");
	log("작업 종료");
}

  • 특정 스레드의 인스턴스에 interrupt() 메서드를 호출하면, 해당 스레드에 인터럽트가 발생한다.
  • 인터럽트가 발생하면 해당 스레드에 InterruptedException 이 발생한다.
    • 이때 인터럽트를 받은 스레드는 대기 상태에서 깨어나 RUNNABLE 상태가 되고, 코드를 정상 수행한다.
    • 이때 InterruptedException 을 catch 로 잡아서 정상 흐름으로 변경하면 된다.
  • 참고로 interrupt() 를 호출했다고 해서 즉각 InterruptedException 이 발생하는 것은 아니다. 오직 sleep() 처럼 InterruptedException 을 던지는 메서드를 호출하거나 또는 호출 중일 때 예외가 발생한다.
    • 예를 들어서 위 코드에서 while(true), log("작업 중") 에서는 InterruptedException 이 발생하지 않는다.
    • Thread.sleep() 처럼 InterruptedException 을 던지는 메서드를 호출하거나 또는 호출하며 대기 중일 때 예외가 발생한다.

정리:인터럽트 상태인 스레드가 Thread.sleep()을 만나거나, Thread.sleep() 상태에서 인터럽트 상태가 되면, 예외가 터지고, 다시 인터럽트 상태가 false가 된다.


헷갈리지 말자. 인터럽트가 걸리면 true였다가 catch로 가면서 false로 바뀐다. 가끔 로그 순서가 바뀌어서 나오는거는, 각 스레드에서 출력 처리 시간이 다르기 때문이다.


작업 중단 3. 인터럽트 - 개선

while (true) { //인터럽트 체크 안함
    log("작업 중");
    Thread.sleep(3000); //여기서만 인터럽트 발생
}

while의 조건문에서도 인터럽트의 상태를 바로 확인할 수 있다면 더 빨리 반응할 수 있을 것이다. 또한, Sleep()이 없어도 인터럽트 상태를 직접 확인하기 때문에 while문을 빠져나갈 수 있다.

while (인터럽트_상태_확인) { //여기서도 체크
    log("작업 중");
}

static class MyTask implements Runnable {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) { // 인터럽트 상태 변경 X
                log("작업 중");
            }
            log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
            try {
                log("자원 정리 시도");
                Thread.sleep(1000);
                log("자원 정리 완료");
            } catch (InterruptedException e) {
                log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
                log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
            }
            log("작업 종료");
        }
}

주요 실행 순서

  • main 스레드는 interrupt() 메서드를 사용해서, work 스레드에 인터럽트를 건다.
  • work 스레드는 인터럽트 상태이다. isInterrupted() 의 결과는 true 가 된다.
  • 이때 다음과 같이 while 조건이 false 가 되면서 while문을 탈출한다.
    • while (!Thread.currentThread().isInterrupted())
    • while (!true)
    • while (false)

여기까지 보면 아무런 문제가 없어 보인다. 하지만 이 코드에는 심각한 문제가 있다. 바로 work 스레드의 인터럽트 상태가 true 로 계속 유지된다는 점이다. 앞서 인터럽트 예외가 터진 경우 스레드의 인터럽트 상태는 false 가 된다.
반면에 isInterrupted() 메서드는 인터럽트의 상태를 변경하지 않는다. 단순히 인터럽트 상태를 확인만 한다.
이로 인해 위 예제에서는 자원을 정리하는 코드를 실행하는데에 인터럽트가 발생해서 catch문으로 이동해버린다.


스레드의 인터럽트 상태를 정상으로 돌리지 않으면 이후에도 계속 인터럽트가 발생하게 된다.
인터럽트의 목적을 달성하면 인터럽트 상태를 다시 정상으로 돌려두어야 한다.


이 문제를 해결하기 위해서는 while(인터럽트_상태_확인) 같은 곳에서 인터럽트의 상태를 확인한 다음에, 만약 인터럽트 상태(true)라면 인터럽트 상태를 다시 정상(false)으로 돌려두면 된다.


이를 위한 메서드가 존재한다.


인터럽트 - Thread.interrupted()

Thread.interrupted()
스레드의 인터럽트 상태를 단순히 확인만 하는 용도라면 isInterrupted() 를 사용하면 된다.
하지만 직접 체크해서 사용할 때는 Thread.interrupted() 를 사용해야 한다.
이 메서드는 다음과 같이 작동한다.

  • 스레드가 인터럽트 상태라면 true 를 반환하고, 해당 스레드의 인터럽트 상태를 false 로 변경한다.
  • 스레드가 인터럽트 상태가 아니라면 false 를 반환하고, 해당 스레드의 인터럽트 상태를 변경하지 않는다.

static class MyTask implements Runnable {
        @Override
        public void run() {
            while (!Thread.interrupted()) { //인터럽트 상태 변경O
                log("작업 중");
            }
            log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
            try {
                log("자원 정리 시도");
                Thread.sleep(1000);
                log("자원 정리 완료");
            } catch (InterruptedException e) {
                log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
                log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
            }
            log("작업 종료");
        }
    }

주요 실행 순서

  • main 스레드는 interrupt() 메서드를 사용해서, work 스레드에 인터럽트를 건다.
  • work 스레드는 인터럽트 상태이다. Thread.interrupted() 의 결과는 true 가 된다.
    • Thread.interrupted() 는 이때 work 스레드의 인터럽트 상태를 정상(false)으로 변경한다.
  • 이때 다음과 같이 while 조건이 false 가 되면서 while문을 탈출한다.
    • while (!Thread.interrupted())
    • while (!true)
    • while (false)

Thread.interrupted()와 Thread.isInterrupted()를 상황에 맞게 잘 사용하자.


Yield

기본적으로 어떤 스레드를 얼마나 실행할지는 운영체제가 스케줄링을 통해 결정한다.
그런데 특정 스레드가 크게 바쁘지 않은 상황이라서 다른 스레드에 CPU 실행 기회를 양보하고 싶을 수 있다.


EX) 1000개 스레드를 생성하고 실행. 각 스레드는 0 ~ 9를 실행
이와 관련된 3가지 방식을 살펴보자.


Empty: sleep(1), yield() 없이 호출한다. 운영체제의 스레드 스케줄링을 따른다.

  • 특정 스레드가 쭉~ 수행된 다음에 다른 스레드가 수행된다.
  • 참고로 실행 환경에 따라 결과는 달라질 수 있다. 다른 예시보다 상대적으로 하나의 스레드가 쭉~ 연달아 실행되다가 다른 스레드로 넘어간다.
  • 이 부분은 운영체제의 스케줄링 정책과 환경에 따라 다르지만 대략 0.01초(10ms)정도 하나의 스레드가 실행되고, 다른 스레드로 넘어간다.

sleep(1): 특정 스레드를 잠시 쉬게 한다.

  • sleep(1) 을 사용해서 스레드의 상태를 1밀리초 동안 아주 잠깐 RUNNABLE → TIMED_WAITING 으로 변경한다. 이렇게 되면 스레드는 CPU 자원을 사용하지 않고, 실행 스케줄링에서 잠시 제외된다. 1밀리초의 대기 시간이 지난 이후 다시 TIMED_WAITING → RUNNABLE 상태가 되면서 실행 스케줄링에 포함된다.
  • 결과적으로 TIMED_WAITING 상태가 되면서 다른 스레드에 실행을 양보하게 된다.
  • 하지만 이 방식은 RUNNABLE → TIMED_WAITING → RUNNABLE 로 변경되는 복잡한 과정을 거치고, 또 특정 시간 만큼 스레드가 실행되지 않는 단점이 있다.
    • 예를 들어서 양보할 스레드가 없다면, 차라리 나의 스레드를 더 실행하는 것이 나은 선택일 수 있다.

yield(): yield() 를 사용해서 다른 스레드에 실행을 양보한다.

  • 자바의 스레드가 RUNNABLE 상태일 때, 운영체제의 스케줄링은 다음과 같은 상태들을 가질 수 있다.
    • 실행 상태(Running): 스레드가 CPU에서 실제로 실행 중이다.
    • 실행 대기 상태(Ready): 스레드가 실행될 준비가 되었지만, CPU가 바빠서 스케줄링 큐에 대기 중이다.
  • Thread.yield() 메서드는 현재 실행 중인 스레드가 자발적으로 CPU를 양보하여 다른 스레드가 실행될 수 있도록 한다.
  • yield() 메서드를 호출한 스레드는 RUNNABLE 상태를 유지하면서 CPU를 양보한다. 즉, 이 스레드는 다시 스케줄링 큐에 들어가면서 다른 스레드에게 CPU 사용 기회를 넘긴다.
  • 자바에서 Thread.yield() 메서드를 호출하면 현재 실행 중인 스레드가 CPU를 양보하도록 힌트를 준다. 이는 스레드가 자신에게 할당된 실행 시간을 포기하고 다른 스레드에게 실행 기회를 주도록 한다.
  • 참고로 yield() 는 운영체제의 스케줄러에게 단지 힌트를 제공할 뿐, 강제적인 실행 순서를 지정하지 않는다. 그리고 반드시 다른 스레드가 실행되는 것도 아니다.
  • yield() 는 RUNNABLE 상태를 유지하기 때문에, 쉽게 이야기해서 양보할 사람이 없다면 본인 스레드가 계속 실행될 수 있다.

참고) 김영한의 실전 자바 - 고급 1편

'Java > 멀티스레드&동시성' 카테고리의 다른 글

[Java] 38. 동기화 - synchronized  (0) 2025.07.01
[Java] 37. 메모리 가시성  (0) 2025.07.01
[Java] 35. 스레드 제어와 생명 주기(1)  (0) 2025.07.01
[Java] 34. 스레드 생성과 실행  (0) 2025.07.01
[Java] 33. 프로세스와 스레드  (0) 2025.07.01
'Java/멀티스레드&동시성' 카테고리의 다른 글
  • [Java] 38. 동기화 - synchronized
  • [Java] 37. 메모리 가시성
  • [Java] 35. 스레드 제어와 생명 주기(1)
  • [Java] 34. 스레드 생성과 실행
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (462)
      • 개발 일지 (0)
        • Performance (0)
        • TroubleShooting (0)
        • Refactoring (0)
        • Code Style, Convetion (0)
        • Architecture (0)
      • Software Engineering (36)
        • Test (8)
        • 이론 (18)
        • Clean Code (10)
      • Java (72)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (13)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (16)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (130)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (6)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (76)
        • Kotlin (0)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Java] 36. 스레드 제어와 생명 주기(2)
상단으로

티스토리툴바