새소식

Programming/Java

[Java] Thread의 실행 제어 - interrupt()

 

 

Thread의 스케줄링

 

스레드의 스케줄링 관련 메서드로는

sleep(), join(), interrupt(), yield(), stop(), suspend(), resume()이 있는데,

 

이 중 stop(), suspend(), resume()

스레드를 교착상태(dead-lock)로 만들기 쉽기 때문에 deprecated 되어 사용하지 않는다.

 

 

Multithreading 프로세스에서

여러 스레드가 같은 프로세스 내의 자원을 공유하며 생기는 문제점을 방지하기 위해

한 스레드가 진행 중인 작업을 다른 스레드가 간섭하지 못하게 막고,

이를 스레드의 동기화(synchronization)라 한다.

 

하지만 두 개 이상의 스레드가 동시에 같은 데이터에 접근하고 데이터를 변경하면

원하는 결과를 얻지 못하는 경우가 발생할 수 있다.

 

wait()notify()는 스레드의 동기화를 통해

공유되는 데이터를 보호하면서 하나의 스레드가 작업을 끝낸 뒤 다른 스레드가 작업을 하도록 만든다. 

 

 

 

이러한 Thread와 관련된 메서드를 정리하면 다음과 같다.(deprecated 메서드 제외)

 

 static void sleep(long millis)
 - 지정된 시간(millisecond)동안 스레드 일시정지

 - 지정된 시간이 지나면 실행대기 상태로 자동 전환

 void join()
 void join(long millis)

 - 스레드 자신이 하던 작업을 멈추고 다른 스레드가 지정된 시간동안
   작업을 수행하도록 함

 - 시간을 지정하지 않으면 해당 스레드가 작업을 모두 마칠 때까지 일시정지

 void wait()
 void wait(long timeoutMillis)

 - 임계영역 내 코드를 수행하다가 일시정지 후 notify()의 호출을 기다림

 - 지정된 시간이 있는 경우 해당 시간동안만 기다림

 - Object class에 정의되어 있음

 - 동기화블럭 내에서만 사용 가능

 void notify()

 void notifyAll()

 - 코드 수행이 완료되었음을 알려 다른 스레드에게 lock을 넘김

 - waiting pool에서 기다리고 있는 임의의 스레드 하나에게 알림

 - notifyAll()은 기다리고 있는 모든 스레드에게 알림
   (lock은 하나의 스레드만 얻을 수 있음)

 - Object class에 정의되어 있음

 - 동기화블럭 내에서만 사용 가능

   * waiting pool은 객체마다 존재함
   * notifyAll()이 호출된 객체의 waiting pool에서 대기 중인 스레드만
     대상으로 함

 static void yield()
 - 스레드 자신에게 주어진 실행시간을 다음 차례의 스레드에게 양보함

 void interrupt()
 - 스레드의 interrupted 상태를 false에서 true로 변경

 boolean isInterrupted()
 - 스레드의 interrupted 상태를 반환

 static boolean interrupted()
 - 현재 스레드의 interrupted 상태를 반환한 후 false로 변경

 

 

여러 가지 메서드 중 이번에 살펴보고자 하는 내용을 요약하면 이렇게 정리할 수 있다.

  • sleep() / join() / wait() : 스레드를 일시정지 상태로 변경
  • interrupt() : sleep(), join(), wait()에 의해 일시정지된 스레드를 실행대기 상태로 변경

 

 

 

그중에서도 interrupt()의 작동 방식을 조금 더 자세히 정리해보고자 한다.

 

 

Interrupt()

 

 - sleep(), join(), wait()에 의해 일시정지 상태인 스레드를 실행대기 상태로 전환
     // InterruptedException이 발생하여 일시정지 상태 해제

 

 - sleep(), join(), wait() 중 하나의 호출로 인해 블로킹된 경우,

    해당 스레드의 interrupted 상태가 초기화(false)되고 InterruptedException 발생

     // 상태 초기화의 이유: interrupt()가 여러 번 발생하는 상황일 때

         interrupted 상태를 감지하려면 false 상태여야 가능

     // catch블럭 내 코드를 수행 후 try - catch 구문을 빠져나감

     // 일시정지 상태에서 interrupt()를 사용하면 interrupt signal은 바로 보내지만, 

         실제로 이 signal을 받고 예외를 발생시키는 건 해당 스레드가 스케줄러의 선택을 받고 실행되는 시점임

 

 - interrupted()가 호출되었을 때 스레드가 일시정지 상태가 아니라면 아무 일도 발생하지 않음

 

 

 

예시

 

조금 복잡해 보이지만 예시를 통해 살펴보면 이해가 쉽다.

 

0부터 1씩 3초 간격으로 증가하는 th1 스레드가 있고, 메인은 실행 후 6초 뒤 종료되는 코드이다.

th1을 interrupt 하여 스레드를 정지시키고자 한다.

public class ThreadEx {
	public static void main(String[] args) {
		ThreadNumAdd th1 = new ThreadNumAdd();
		th1.start();
		
		try {
			Thread.sleep(6000);
		} catch (InterruptedException e) {}
		
		th1.interrupt();
		System.out.println("main stopped");
	}
}

class ThreadNumAdd extends Thread {
	public void run() {
		for (int i = 0; !isInterrupted(); i++) {
			System.out.println(i);
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				System.out.println("thread stopped");
			}
		}
	}
}

 

Result

0
1
2
main stopped
thread stopped
3
4

 

하지만 결과를 보면 스레드를 정지시킨 이후에도 계속해서 실행되고 있는데,

이 코드의 문제는 스레드가 잠들어 있을 때 interrupt 되어 예외가 발생되었다 해도

이후 반복 실행 조건을 확인하는 과정에서 !isInterrupted() == true로 처리되어

무한반복에 빠진다는 것이다.

 

 

이 문제는 다음과 같이 해결할 수 있다.

 

1. catch블럭에 return;을 작성하여 예외 발생 시 반복문을 종료하도록 함

class ThreadNumAdd extends Thread {
	public void run() {
		for (int i = 0; !isInterrupted(); i++) {
			System.out.println(i);
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				System.out.println("thread stopped");
				return;
			}
		}
	}
}

 

 

2. 반복문을 try블럭 안에 넣어 예외발생 시 바로 반복문을 탈출하도록 함

class ThreadNumAdd extends Thread {
	public void run() {
		int i = 0;
		try {
			while (!isInterrupted()) {
				System.out.println(i++);
				Thread.sleep(3000);
			}
		} catch (InterruptedException e) {
			System.out.println("thread stopped");
		}
	}
}

 

 

3. 기존 코드에서 catch블럭 안에 interrupt();를 추가하여 !isInterrupted == false로 만듦

class ThreadNumAdd extends Thread {
	public void run() {
		for (int i = 0; !isInterrupted(); i++) {
			System.out.println(i);
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				interrupt();
				System.out.println("thread stopped");
			}
		}
	}
}

 

세 가지 방법 모두 "thread stopped" 출력 이후 프로그램이 종료된다.

 

 

 

공부자료: 자바의 정석

Contents

Copied URL!

Liked this Posting!