[Java] Thread#4, 스레드의 상태제어 메소드 및 데몬 스레드 개념 및 예제
스레드 상태제어
- 실행중인 스레드의 상태를 변경하는 것.
- 메소드로 주로 제어
- 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이 발생하여 종료하는 것은 아닌 것 같다. 아시는분 댓글로 알려주시면 감사하겠습니다!