프로그래밍 언어/Java

[Java] Thread#4, 스레드의 상태제어 메소드 및 데몬 스레드 개념 및 예제

코딩하는흑구 2019. 9. 5. 13:11
스레드 상태제어

- 실행중인 스레드의 상태를 변경하는 것.

- 메소드로 주로 제어

- interrupt(), sleep(), join(), wait(), yield() notify(), notifyAll() 등의 메소드가 존재.

- 이중 notify(), notifyAll(), wait() 메소드는 Object 클래스의 메소드이고 나머지는 Thread 클래스의 메소드.

 

일정시간동안 일시정지 : sleep()

- 실행중인 스레드를 일시정지.

- 매개값으로 밀리초를 넣어주면 해당 시간동안 sleep() 메소드를 만나는 스레드는 일시정지함.

- 일시정지 상태에서 interrupt() 메소드를 호출할 경우 InterruptedException이 발생됨.

try{
	Thread.sleep(1000); //1초간 일시정지(밀리초 : 1000 -> 1초)
}catch(InterruptedException){
	//interrupt() 메소드가 호출되면 실행.
}

 

타 스레드에 실행 양보 : yield() 

- 스레드가 처리하는 반복작업을 위해 for문이나 while문을 사용하는 경우가 많음.

public void run(){
	while(true){
    	if(work){
        	System.out.println("ThreadA 작업 내용");
        }
    }
}

-이때 while문은 boolean 타입 work 변수가 false 일경우에는 쓸데없는 루프를 돌게됨.

- yield() 메소드를 호출하면 호출한 스레드는 실행대기상태로 돌아가고 동일한 우선순위 혹은 높은 우선순위를 갖는 다른 스레드가 실행 기회를 갖게됨.

public class ThreadA extends Thread{
	public boolean stop = false; //종료 플래그
	public boolean work = true; // 작업진행여부
	
	public void run() {
		while(!stop) {
			if(work) {
				System.out.println("ThreadA 작업 내용");
			}else {
				Thread.yield();
			}
		}
		System.out.println("ThreadA 종료");
	}
}

- ThreadA가 else 문으로 yield 메소드를 만나면 현재 Thread 객체인 ThreadA의 run 메소드는 실행대기 상태가 되고 ThreadB가 실행기회를 갖는다.

 

다른 스레드의 종료를 기다림 : join()

- 다른 스레드가 종료되어야 실행해야하는 스레드가 존재

- 계산작업이 그 예인데, 계산하여 결과를 return 하는 스레드가 존재하면 그것을 출력하는 스레드가 필요한데

- 그 때 출력스레드가 먼저 수행되면 오류임.

public class SumThread extends Thread{
	private long sum;

	public long getSum() {
		return sum;
	}

	public void setSum(long sum) {
		this.sum = sum;
	}

	public void run() {
		for(int i =1; i<=100; i++) {
			sum+=i;
		}
	}
	
	@Override
	public String toString() {
		return "SumThread [sum=" + sum + "]";
	}
}

public class JoinExample {
	public static void main(String[] args) {
		SumThread sumThread = new SumThread();
		sumThread.start();
		
		try {
			sumThread.join();//현재 스레드 기준 (이부분을 주석처리해서 결과를 비교해보세요)
		} catch (Exception e) {
		}
		System.out.println("1~100 합 : "+sumThread.getSum());
	}
}

- 여기서 출력스레드는 메인스레드가 담당한 것임.

 

스레드간 협력 : wait(), notify(), notifyAll()

- 두개의 스레드를 번갈아가면서 실행

- 핵심은 공유객체의 활용

- 두 스레드가 작업할 내용을 동기화 메소드로 구분.

- 스레드1 작업 완료 -> notify() 메소드 호출 -> (일시정지)스레드 2 실행대기상태로 변경 -> 스레드 1은 wait() (일시정지 상태)

- 이들 메소드는 동기화 메소드 혹은 동기화 블록에서만 사용가능.

//공유객체
public class WorkObject {
	public synchronized void methodA() {
		System.out.println("ThreadA의 methodA() 작업 실행");
		notify(); //일시정지 상태에 있는 ThreadB를 실행대기 상태로 만듬.
		try {
			wait();//ThreadA를 일시정지 상태로 만듬.
		} catch (Exception e) {
		}
	}
	
	public synchronized void methodB() {
		System.out.println("ThreadB의 methodB() 작업 실행");
		notify(); //일시정지 상태에 있는 ThreadA를 실행대기 상태로 만듬.
		try {
			wait();//ThreadB를 일시정지 상태로 만듬.
		} catch (Exception e) {
		}
	}
}

//Thread A
public class ThreadA extends Thread{
	private WorkObject workObject;
	
	public ThreadA(WorkObject workObject) {
		this.workObject = workObject;
	}
	
	@Override
	public void run() {
		for(int i =0; i<10; i++) {
			workObject.methodA();
		}
	}
}

//ThreadB
public class ThreadB extends Thread{
	private WorkObject workObject;
	
	public ThreadB(WorkObject workObject) {
		this.workObject = workObject;
	}
	
	@Override
	public void run() {
		for(int i =0; i<10; i++) {
			workObject.methodB();
		}
	}
}

//main 스레드
public class WaitNotifyExample {
	public static void main(String[] args) {
		WorkObject shareObject = new WorkObject(); //공유객체 생성
		
		ThreadA threadA = new ThreadA(shareObject);
		ThreadB threadB = new ThreadB(shareObject);//ThreadA와 ThreadB 생성
		
		threadA.start();
		threadB.start();
				
	}
}

- 메인 스레드에서 공유객체를 생성

- 각각의 스레드의 멤버변수로 초기화. 공유 객체의 methodA와 methodB를 사용

- methodA와 methodB는 번갈아가면서 실행되어야함.

- 이 협력개념에서 발전하여 유명한 자바 디자인 패턴인 생산자 소비자 패턴으로 연결됨.

 

스레드의 안전종료 : interrupt()

- run() 메소드가 모두 실행되면 스레드는 종료됨.

- 기존의 stop() 이란 메소드가 제공되었으나 deprecated 되었다. -> 문제

- 왜? -> 스레드가 사용하던 자원이 문제가 될 가능성( 자원이란 파일, 네트워크 연결 등)

- interrupt() 메소드를 이용하여 자원도 해제하며 안전하게 종료할 수 있음.

 

public class PrintThread2 extends Thread{
	public void run() {
		try {
			while(true) {
				System.out.println("실행 중");
				Thread.sleep(1);
				//if(Thread.interrupted()) {
				//if(Thread.currentThread().isInterrupted()) {
					//break;
				//}
			}
		} catch (InterruptedException e) {
			System.out.println("interrupt() 실행");
		}
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}

//메인 스레드
public class InterruptExample {
	public static void main(String[] args) {
		Thread thread = new PrintThread2();
		thread.start();
		
		try {
			Thread.sleep(1000);
		} catch (Exception e) {
			
		}
		thread.interrupt();
	}
}

- 메인스레드에서 interrupt() 메소드가 호출할 때 PrintThread2 스레드는 InterruptedException이 발생하기 위해서는 일시정지상태에 있어야 한다. 그렇지 않으면 아무의미가 없다.

 

- 그래서 Thread.sleep(1) 코드로 한번 일시정지 상태를 만들어주고 메인스레드에서 interrupt() 메소드를 실행하고 먼저 종료하였기 때문에 이후 PrintThread2 스레드는 자원을 정리하는 코드를 실행하며 안전하게 종료하게 된다.

 

- 주석처리된 Thread.interrupted() 메소드와 isInterrupted() 메소드는 모두 interrupt() 메소드가 실행됬는지 여부를 반환하는 boolean 값이다. 참조객체를 보면알겠지만 Thread.interrupted()는 static 메소드이고, isInterrupted() 메소드는 인스턴스 메소드이다. 둘중 어떤것을 사용해도 좋다.

 

데몬스레드

- 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드.

- 주 스레드가 종료되면 데몬스레드는 강제적으로 자동 종료.

- Java의 Garbage Collector가 대표적인 데몬 스레드라고 함. - > JVM이 종료되면 같이 종료되니까.

- 현재 스레드에서 다른 스레드를 데몬스레드로 만들기 위해서는 데몬 스레드가될 스레드의 참조객체에서 setDaemon(true)를 호출해주면 된다.

- 주의점은 데몬스레드의 스레드가 이미 start() 메소드를 호출한 상태라면 IllegalThreadStateException이 발생하기 때문에 start() 메소드를 호출하기 존에 setDaemon(true)를 실행해야한다.

 

public class AutoSaveThread extends Thread{
	public void save() {
		System.out.println("작업 내용을 저장함");
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				System.out.println("1");//여기실행안됨. exception 발생은 아님
				e.printStackTrace();
				break;
			}
			save();
		}
	}
}

//메인 스레드
public class DaemonExample {
	public static void main(String[] args) {
		AutoSaveThread autoSaveThread = new AutoSaveThread();
		autoSaveThread.setDaemon(true);
		autoSaveThread.start();
		
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {

		}
		System.out.println("메인 스레드 종료");
	}
}

- 코드를 실행해보면 기존의 스레드는 메인 스레드가 죽어도 반복작업을 하는 경우는 작업스레드는 살아있어 프로그램이 죽지 않았는데 위 예제는 메인 스레드가 죽으면서 데몬스레드도 같이 죽어서 프로그램을 종료시킨다.

 

- 데몬스레드가 종료하는 이유를 알고싶어서 Exception이 발생하는 것인가 확인차 catch 문에 프린트 소스를적어봤으나 Exception이 발생하여 종료하는 것은 아닌 것 같다. 아시는분 댓글로 알려주시면 감사하겠습니다!