본문 바로가기

JAVA/혼공자

[혼공자] Chapter 12-2 스레드 제어

 

출처 : https://codedragon.tistory.com/5375

▶ 스레드 상태
스레드 객체를 생성하고 start( ) 메소드를 호출하면 스레드는 실행 대기 상태(Runnable)가 된다. 
운영체제가 실행 대기 상태에 있는 스레드 중 하나를 선택하고, CPU가 run( ) 메소드를 실행하면 비로소 실행(Running) 상태가 된다. 
실행 중인 스레드는 sleep( ), wait( ) 메소드 혹은 입출력 요청에 의해 일시 정지 상태(Blocked)로 가기도 하는데, 
일시 정지 상태에서는 바로 실행 상태로 돌아갈 수 없고, 실행 대기 상태로 간 후 실행 상태로 가야 한다. 
interrupt( ), notify( ), notifyAll( ) 메소드를 통해 일시 정지 상태의 스레드를 실행 대기 상태로 만들수 있다.
혹은 sleep( )을 통해 스레드를 대기상태로 만든 경우, 설정한 시간이 지나면 실행 대기상태로 돌아간다.
실행 상태에서 run( ) 메소드가 종료되면, 스레드는 종료 상태(Treminated)가 된다.


▶ 스레드 상태 제어
실행 중인 스레드의 상태를 변경하는 것을 스레드 상태 제어라고 한다. 
멀티 스레드 프로그램을 만들기 위해서는 정교한 스레드 상태 제어가 필요하다. 
Ex. 스레드를 일시 정지 시키는 sleep( ) 메소드, 스레드를 안전하게 종료하는 interrupt( ) 메소드 등
cf. 스레드를 즉시 종료시키는 stop 메소드도 있었지만, 불안정한 종료를 유발하므로 더이상 추천되지 않는다.


▷ sleep( ) 메소드
실행 중인 스레드를 일정 시간 동안 일시 정지 시키는 메소드이다. 
Thread 클래스의 정적(static) 메소드이기 때문에, Thread.sleep( )로 사용할 수 있다. 
이때 정지할 시간을 밀리 세컨드 단위의 매개변수로 전달할 수 있다. 
Thread.sleep( )를 호출한 스레드는 일시 정지 상태로 있다가 설정한 시간이 끝나면 실행 대기 상태로 돌아간다.
※ sleep은 [실행 중 → 일시 정지 → 실행 대기] 를 만들어 주는 메소드
일시 정지 상태에서 주어진 시간이 되기 전에 interrupt( ) 메소드가 호출되면, 
InterruptedException이 발생하기 때문에 예외처리가 필요하다.


▷ interrupt( ) 메소드
스레드가 일시 정지 상태일 때 InterruptesException을 발생시키는 역할을 한다.
interrupt( ) 메소드는 예외를 발생시켜 try문을 벗어나게 하는 것이 목적인 메소드이다.
이를 통해 run( ) 메소드를 정상 종료할 수 있기 때문이다.
interrupt( ) 메소드는 인스턴스 메소드이므로, 스레드 객체에 도트 연산자를 사용해 호출한다.

예를들어, try { }문 안의 while(true) 문에서 Thread.sleep( ) 가 호출되어 [실행 → 일시 정지 → 실행]이 반복되고 있다 하자.
이때 interrupt( ) 메소드가 호출되고 InterruptedException이 발생하여 
try문의 while(true)를 빠져나와 catch 문으로 이동하게 하여 run( ) 메소드를 빠져나올 수 있다.
즉, 스레드를 안전하게 종료시킬 수 있다.

public class PrintThread2 extends Thread {
	public void run() {
		try {
			while(true) {
				System.out.println("실행 중");
				Thread.sleep(1); // 실행 대기 상태로 만들어 줌
			}
		} catch(InterruptedException e) {} 
		// 예외가 발생하면 무한루프를 탈출하게 되므로 run()메소드 종료됨
        
		System.out.println("실행 종료");
	}
}
public class InterruptExample {
	public static void main(String[] args) {
		Thread thread = new PrintThread2();
		thread.start();
		
		try { Thread.sleep(1000); // 메인스레드 1초 대기
		} catch (InterruptedException e) {}
		
		thread.interrupt(); 
		// 실행 대기 상태에서 interrupt 호출 -> InterruptedException 발생 -> try문 탈출
	}
}



주목할 점은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt( ) 메소드가 호출되면 InterruptesException이 바로 발생하지 않고, 
스레드가 미래에 일시 정지 상태가 되면 그제서야 InterruptesException이 발생한다는 것이다. 
따라서 스레드가 일시 정지 상태가 되지 않으면, interrupt 메소드는 아무런 의미가 없다.
위 예제에서 PrintThread2의 run( ) 메소드에서 sleep(1)을 없애면, InterruptesException이 발생하지 않으므로
무한 루프에 빠지게 된다.


이런 interrupt의 특성 때문에, 스레드를 0.001초나마 일시 정지시키는 Thread.sleep(1)메소드를 사용하기도 한다. 
하지만, 일시 정지시키지 않고도 interrupt 호출 여부를 판단하여 run 메소드를 빠져나오게 할 수도 있다. 
interrupted( )와 isInterrupted( ) 메소드는 현재 스레드가 interrupted되었는지를 boolean로 리턴해주는 메소드이다.
예외를 던지는게 아니라, 참거짓을 판단하는 것이므로 if문안에 break를 넣어 구현할 수 있다.
interrupted( ) 는 정적 메소드이고 isInterrupted( )는 인스턴스 메소드라는 차이는 있지만, 둘 중 어떤것을 사용해도 무방하다.

public class PrintThread2 extends Thread {
	public void run() {
    		// sleep가 없으므로 예외처리 안해줘도 됨
		while(true) {
			System.out.println("실행 중");
			if(Thread.interrupted()) { // interrupted 조사
			break;
		}
		System.out.println("실행 종료");       
	}
}



▶ 데몬 스레드
일반적인 스레드는 메인 스레드의 종료와 상관 없이 자신의 run( ) 메소드가 종료되지 않았다면, 종료되지 않는다. 
하지만 데몬 스레드는 메인 스레드가 종료되면, 강제적으로 종료되는 스레드이다. 
메인 스레드를 돕는 보조적인 역할을 하므로 메인 스레드가 종료되면 그 존재 의미가 사라지기 때문이다. 
이점을 제외하면 데몬 스레드는 일반 스레드와 차이가 없다.

데몬 스레드는 스레드 객체의 인스턴스 메소드인 setDaemon(true)를 통해 만들 수 있다. 
이때 start( ) 후에 setDaemon를 호출하면 이미 대기상태에 들어간 후에 설정을 바꾸는 것이므로 예외가 발생하기 때문에 
반드시 start( ) 호출 전에 setDaemon(true)를 호출 해야 한다.

public class DaemonThread extends Thread{
	public void run() {
		try {
			while(true) {
				save();
				Thread.sleep(1000); // 출력 후 1초 sleep 무한 반복
			}
		} catch(InterruptedException e) { }
	}
	
	public void save() {
		System.out.println("저장합니다.");
	}
}
public class DeamonThreadExample {
	public static void main(String[] args) {
		Thread daemonThread = new DaemonThread();
		
		daemonThread.setDaemon(true); // 데몬 스레드로 만듦
		daemonThread.start();
		
		try {
			Thread.sleep(3000); // 메인 스레드 3초 sleep
		} catch(InterruptedException e) {}
		
		System.out.println("메인 스레드 종료");
	}
}

=> 1초 간격으로 출력하는 무한 루프가 메인 스레드 종료와 동시에 종료된 것을 볼 수 있다.