본문 바로가기
자바

[Java] 자바 스레드(Thread)의 상태, 상태를 조절하는 메소드들

by 책 읽는 개발자_테드 2021. 1. 25.
반응형

자바 스레드(Thread)의 상태, 상태를 조절하는 메소드들

 

이 글은 자바에서 스레드 상태의 의미와 이를 코드로 조작 하는 방법에 대하여 다룹니다. 

 

학습 목표

 · 스레드의 상태

 · 스레드 상태 제어 

 · 주어진 시간동안 일시 정지 - sleep()

   - 다른 스레드에게 실행 양보 - yield()

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

   - 스레드 간 협업 - wait(), notify(), notifyAll()

   - 스레드의 안전한 종료 - stop 플래그, interrupt()


스레드의 상태

 

스레드 객체를 생성하고, start() 메소드를 호출하면 스레드는 실행 대기 상태가 된다.

  - 실행 대기 상태: 아직 스케줄링이 되지 않아서 실행을 기다리고 있는 상태

 

실행 대기 상태에 있는 스레드 중에서 스레드 스케줄링으로 선택된 스레드가 비로서 CPU를 점유하고 run() 메소드를 실행한다. 이때를 실행(Running) 상태라고 한다.

 

실행 상태의 스레드는 run() 메소드를 모두 실행하기 전에 스레드 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 된다. 이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아가며 자신의 run() 메소드를 조금씩 실행한다. 

 

실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈춘다. 이 상태를 종료 상태라고 한다.

 

스레드가 실행 상태에서 실행 대기 상태로 가지 않고, 일시 정지 상태로 가기도 한다.

   - 일시 정지 상태: 스레드가 실행할 수 없는 상태다.

   - 일시 정지 상태의 종류: WAITING, TIMED_WAITING, BLOCKED

 

이때 스레드가 다시 실행 상태로 가기 위해서는 다시 실행 대기 상태로 가야 한다. 

 

 

자바 5부터 Thread 클래스에 getState() 메소드가 추가되어 스레드의 상태를 코드로 확인할 수 있다. getState()를 스레드 상태에 따라서 Thread.State 열거 상수를 리턴한다.

 

상태 열거 상수 실행
객체 생성 NEW 스레드 객체가 생성 후, 아직 start() 메소드가 호출되지 않은 상태
실행 대기 RUNNABLE 실행 상태로 언제든지 갈 수 있는 상태
일시 정지 WAITING 다른 스레드가 통지할 때까지 기다리는 상태
TIMED_WAITING 주어진 시간 동안 기다리는 상태
BLOCKED 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
종료 TERMINATED 실행을 마친 상태

   - WAITING vs TIMED_WAITING vs BlOCKED

https://blog.fastthread.io/2016/07/20/whats-the-difference-between-blocked-waiting-and-timed_waiting-explained-to-real-life-examples/

 

 

아래 예제는  Thread를 상속하는 TargetThread 클래스와 해당 스레드의 상태를 출력하는 StatePrintThread 클래스다.

 

public class StatePrintThread extends Thread {

   private Thread targetThread;

   /*
    * @param targetThread 상태를 조사할 스레드
    */

   public StatePrintThread(Thread targetThread){

this.targetThread = targetThread;
   }

   public void run(){
   
       while (true){
  Thread.State state = targetThread.getState(); //스레드 상태 얻기
  System.out.println("타켓 스레드 상태: " + state);

           //객체 생성 상태일 경우 실행 대기 상태로 만듦
           if(state ==  Thread.State.NEW){
               targetThread.start();
           }

           //종료 상태일 경우 while문을 종료함
if(state == Thread.State.TERMINATED){
            break;
           }

           try{
               //0.5초간 일시 정지
               Thread.sleep(500);
           }catch (InterruptedException e){}
       }
   }
}

 

public class TargetThread extends Thread{

   public void run(){

       for(long i=0; i<1_000_000_000; i++){}

       try{

           //1.5초간 일시 정지

           Thread.sleep(1500);

       }catch (InterruptedException e){}



       for(long i=0; i<1_000_000_000; i++){}

   }

}

 

public class ThreadStateExam {

   public static void main(String[] args){
   
       StatePrintThread statePrintThread = new StatePrintThread(new TargetThread());
       statePrintThread.start();
   }
}

ThreadStateExam 결과

 

TargetThread가 객체로 생성되면 New 상태를 가지고, run() 메소드가 종료되면 TERMINATED 상태로 변한다.

 

스레드 상태 제어

 

실행 중인 스레드의 상태를 변경하는 것을 스레드 상태 제어라고 한다. 아래 그림은 상태 변화를 가져오는 메소드의 종류를 보여준다. 취소선(-)을 가진 메소드는 스레드의 안전성을 해치어 더 이상 사용하지 않도록 권장되는 Deprecated 메소드들이다.

 

메소드 설명
interrupt() 일시 정지 상태의 스레드에 InterruptedException 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 한다.
notify()
notifyAll()
동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다.
resume() suspend() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다.
- Deprecated (notify(), notifyAll()로 대체)
sleep(long millis)
sleep(long millis, int nanos)
주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다.
join()
join(long millis)
join(long millis, int nanos)
join() 메소드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태로 가려면 join() 메소드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나야 한다.
wait()
wait(long millis)
wait(long millis, int nanos)
동기화(synchronized) 블록 내에서 스레드를 일시 정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify(), notifyAll() 메소드에 의해 실행 대기 상태로 갈 수 있다.
suspend() 스레드를 일시 정지 상태로 만든다. resume() 메소드를 호출하면 다시 실행 대기 상태가 된다. - Deprecated(wait()로 대체)
yield() 실행 중에 우선순위가 동일한 다른 스레드에게 실행을 양보하고, 실행 대기 상태가 된다.
stop() 스레드를 즉시 종료한다. - Deprecated

 

위 표에서 wait(), notify(), notifyAll()은 Object 클래스의 메소드이고, 그 이외의 메소드는 모두 Thread 클래스의 메소드들이다.

   - notify() vs notifyAll() https://www.tutorialspoint.com/difference-between-notify-and-notifyall-in-java

 

 

주어진 시간동안 일시 정지 - sleep()

Thread.sleep() 메소드를 호출한 스레드는 주어진 시간 동안 일시 정지 상태가 되고, 다시 실행 대기 상태로 돌아간다.

 

public class SleepExam {

   public static void main(String[] args){

       Toolkit toolkit = Toolkit.getDefaultToolkit();

       for(int i=0; i<0; i++){
           toolkit.beep();
           
           try{
               Thread.sleep(3000);
           }catch(InterruptedException e){}
           
       }
   }
}

 

다른 스레드에게 실행 양보 - yield()

 

스레드가 처리하는 작업은 반복적인 실행을 위해 for문, while문을 포함하는 경우가 많다. 이때 무의미한 반복을 하는 경우도 있다.

 

public void run(){

while(true){
if(work){
System.out.println(“ThreadA 작업 내용”);
}
}
}

 

위 코드는 스레드가 시작되어 run() 메소드를 실행하면 while 블록이 무한 반복된다. 만약 work 값이 false이고, 해당 값이 false에서 true로 변경되는 시점이 불명확하면, while문은 어떠한 실행문도 실행하지 않고 무의미한 반복을 한다.

 

이것보다는 yield() 메소드를 사용하여 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다. yield() 메소드를 호출한 스레드는 실행 대기 상태로 돌아가고 동일한 우선순위 또는 높은 우선순위를 갖는 다른 스레드가 실행 기회를 가질 수 있다.

 

아래 예제는 ThreadA와 ThreadB를 실행하는 예제다. ThreadA의 yiel() 메소드를 호출하여 ThreadB가 더 많은 실행 기회를 얻게 하는 것이 집중하여 살펴보자.

 

public class ThreadA extends Thread{

   public boolean stop = false;    //종료 플래그
   public boolean work = true;     //작업 진행 여부 플래그

   public void run(){
       while(!stop){    //stop이 true가 되면 while 종료
           if(work){
               System.out.println("ThreadA 작업 내용");
           }else{
               Thread.yield(); //work가 false가 되면 다른 스레드에게 실행 양보
           }
       }
   }
}
public class ThreadB  extends Thread{

   public boolean stop = false;    //종료 플래그
   public boolean work = true;     //작업 진행 여부 플래그

   public void run(){
       while(!stop){    //stop이 true가 되면 while 종료
           if(work){
               System.out.println("ThreadB 작업 내용");
           }else{
               Thread.yield(); //work가 false가 되면 다른 스레드에게 실행 양보
           }
       }
   }
}
public class YieldExam {

   public static void main(String[] args){

       ThreadA threadA = new ThreadA();
       ThreadB threadB = new ThreadB();

       threadA.start();
       threadB.start();

       try{Thread.sleep(3000);}catch (InterruptedException e){}
       threadA.work = false; //ThreadB만 실행

       try{Thread.sleep(3000);}catch (InterruptedException e){}
       threadA.work = true; //ThreadA, ThreadB 모두 실행

       try{Thread.sleep(3000);}catch (InterruptedException e){}
       
       threadA.stop = true; // ThreadA, ThreadB 모두 종료
       threadB.stop = true;
   }
}

 

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

 

다른 스레드가 종료될 때까지 기다렸다가 스레드를 실행할 경우 join() 메소드를 사용한다. 

 

예를 들어 ThreadA가 ThreadB의 join() 메소드를 호출하면 ThreadA는 ThreadB가 종료할 때까지 일시 정지 상태가 된다. ThreadB의 run() 메소드가 종료되면 비로소 ThreadA는 일시 정지에서 풀려 다음 코드를 실행한다.

 

다음 예제는 메인 스레드에서 SumThread가 계산 작업을 모두 마칠 때까지 일시 정지 상태에 있다가 SumThread가 종료하면 결과값을 받아 출력한다.

 

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;
       }
   }
}

public class Main {

   public static void main(String[] args){

       SumThread sumThread = new SumThread();
       sumThread.start();
       
       try{
           sumThread.join(); //sumThread가 종료할 때까지 메인 스레드를 일시 정지
       }catch (InterruptedException e){
       }
       System.out.println("1-100 합: "+sumThread.getSum());
   }
}

Main의 결과

 

sumThread.join() 코드를 제거하고 실행하면, sumThread가 작업을 완료하지 않은 상태에서 합을 먼저 출력한다.

 

public class Main {

   public static void main(String[] args){

       SumThread sumThread = new SumThread();
       sumThread.start();
       System.out.println("1-100 합: "+sumThread.getSum());
   }
}

join()을 제거한 Main의 결과

 

스레드 간 협업 - wait(), notify(), notifyAll()

 

두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만들어야한다. 이 방법의 핵심은 공유 객체에 있다.

 

공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 구분해 놓는다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.

 

 

만약 wait() 대신 wait(long timeout)이나, wait(long timeout int nanos)를 사용하면 notify()를 호출하지 않아도 지정된 시간이 지나면 스레드가 자동적으로 실행 대기 상태가 된다.

 

 notify() 메소드와 동일한 역할을 하는 notifyAll() 메소드도 존재한다. notify()는 wait()에 의해 일시 정지된 스레드 중 한 개를 실행 대기 상태로 만들고, notifyAll 메소드는 wait()에 의해 일시 정지된 모든 스레드들을 실행 대기 상태로 만든다. 

 

이 메소드들은 Thread 클래스가 아닌 Object 클래스에 선언된 메소드이므로 모든 공유 객체에서 호출이 가능하다. 주의할 점은 이 메소드들은 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다.

 

다음은 두 스레드의 작업을 WorkObject의 methodA()와 methodB()에 정의해 두고, 두 스레드 ThreadA와 ThreadB가 교대로 methodA()와 methodB()를 호출하는 예제다.

 

public class WorkObject {

   public synchronized void methodA(){

       System.out.println("ThreadA의 methodA() 작업 실행");

       notify(); //일시 정지 상태의 ThreadB를 실행 대기 상태로 만듦

       try{

           wait(); //ThreadA를 일시 정지 상태로 만듦

       }catch (InterruptedException e){}

   }

  

   public synchronized void methodB(){

       System.out.println("ThreadB의 methodB() 작업 실행");

       notify(); //일시 정지 상태의 ThreadA를 실행 대기 상태로 만듦

       try{

           wait(); //ThreadB를 일시 정지 상태로 만듦

       }catch (InterruptedException e){}

   }

}

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++){ //공유 객체의 methodA()를 10번 반복 호출

          workObject.methodA();

      }

  }

}
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++){ //공유 객체의 methodB()를 10번 반복 호출

           workObject.methodB();

       }

   }

}

 

public class Main {

   public static void main(String[] args){

       WorkObject sharedObject = new WorkObject();

       Thread threadA = new ThreadA(sharedObject);
       Thread threadB = new ThreadB(sharedObject);


       //ThreadA와 ThreadB를 실행
       threadA.start();
       threadB.start();
   }
}

 

다음은 데이터를 저장하는 스레드(생산자 스레드)가 데이터를 저장하면, 데이터를 소비하는 스레드(소비자 스레드)가 데이터를 읽고 처리하는 교대 작업을 구현한다.

public class DataBox {

   private String data;

   public synchronized String getData(){

       //data 필드가 null이면 소비자 스레드를 일시 정지 상태로 만듦
       if(this.data == null){
           try{
               wait();
           } catch (InterruptedException e){}
       }

       String returnValue = data;
       System.out.println("ConsumerThread가 읽은 데이터: " + returnValue);

       //data 필드를 null로 만들고 생산자 스레드를 실행 대기 상태로 만듦
       data = null;
       notify();
       return returnValue;
   }

   public synchronized void setData(String data){
   
       //data 필드가 null이 아니면 생산자 스레드를 일시 정지 상태로 만듦
       if(this.data != null){
           try{
               wait();
           }catch (InterruptedException e){}
       }

       //data 필드에 값을 저장하고 소비자 스레드를 실행 대기 상태로 만듦
       this.data = data;
       System.out.println("ProducerThread가 생성한 데이터: "+data);
       notify();
   }
}

 

public class ProducerThread extends Thread {

   private DataBox dataBox;

   public ProducerThread(DataBox dataBox){
       this.dataBox = dataBox;
   }

   @Override
   public void run(){
       for(int i=1; i<=3; i++){
           String data = "Data-"+i;
           dataBox.setData(data);
       }
   }
}

 

public class ConsumerThread extends Thread{

   private DataBox dataBox;

   public ConsumerThread(DataBox dataBox){
       this.dataBox = dataBox;
   }

   @Override
   public void run(){
       for(int i=1; i<=3; i++){
           String data = dataBox.getData();
       }
   }
}

 

public class Main {

   public static void main(String[] args){

       DataBox dataBox = new DataBox();

       ProducerThread producerThread = new ProducerThread(dataBox);
       ConsumerThread consumerThread = new ConsumerThread(dataBox);

       producerThread.start();
       consumerThread.start();
   }
}

 

 

스레드의 안전한 종료 - stop 플래그, interrupt()

 

스레드는 자신의 run() 메소드가 모두 실행되는 자동 종료된다. 하지만 실행 중인 스레드의 즉시 종료가 필요할 수 있다. 

 

Thread 클래스를 스레드를 즉시 종료시키기 위해 stop() 메소드를 제공하지만, 스레드가 사용 중이던 자원(파일, 네트워크 연결)을 불안전한 상태로 남기는 문제가 있어 dreprecated 되었다.

 

이러한 문제를 해결하고 스레드를 즉시 종료시키는 방법으로 stop 플래그를 이용하는 방법, interrupt() 메소드를 이용하는이 있다.

 

stop 플래그를 이용한 스레드 종료

스레드는 run() 메소드가 끝나면 자동적으로 종료되므로, run() 메소드가 정상적으로 종료되도록 유도하는 것이 최선의 방법이다. 다음 코드는 stop 플래그를 이용해서 run() 메소드의 종료를 유도한다.

 

public class StopFlagExample {

   public static void main(String[] args){

       PrintThread1 printThread1 = new PrintThread1();
       printThread1.start();
       try {Thread.sleep(1000);}catch (InterruptedException e){}

       //스레드를 종료시키기 위해 stop 필드를 true 변경
       printThread1.setStop(true);
   }
}

 

public class PrintThread1 extends Thread{

   private boolean stop;

   public void setStop(boolean stop){
       this.stop = stop;
   }

   public void run(){
       while (!stop){
           System.out.println("실행 중");
       }

       System.out.println("자원 정리");
       System.out.println("실행 종료");
   }
}

 

 

interrupt() 메소드를 이용한 스레드 종료

 

interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다. 이것을 이용하면 run() 메소드를 정상 종료시킬 수 있다.

 

다음과 같이 ThreadA가 ThreadB의 interrupt() 메소드를 실행하면, ThreadB가 sleep() 메소드로 일시 정지 상태가 될 때 ThreadB에서 InterruptedException()이 발생하여 예외 처리(catch) 블록으로 이동한다.

아래 예제는 PrintThread2를 실행 1초 후에 PrintThread2를 멈추도록 interrupt() 메소드를 호출한다.

 

public class PrintThread2 extends Thread {

   public void run(){

       try{
           while (true){
               System.out.println("실행 중");
               Thread.sleep(1);
           }
       }catch (InterruptedException e){}

       System.out.println("자원 정리");
       System.out.println("실행 종료");
   }
}



public class InterruptExam {

   public static void main(String[] atgs){

       Thread thread = new PrintThread2();
       thread.start();

       try{Thread.sleep(1000);}catch (InterruptedException e){}

       thread.interrupt(); //스레드를 종료시키기 위해 InterruptedException을 발생시킴
   }
}

 

위 방법은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 InterruptedException 예외가 발생하지 않는다. 스레드가 미래에 일시 정지 상태가 되어야 InterruptedException 예외가 발생한다. 그래서 일시 정지를 위해 Thread.sleep(1)을 사용한 것이다.

 

일시 정지를 만들지 않고도 interrupt() 호출 여부를 알 수 있는 방법이 있다. interrupt() 메소드가 호출되었다면 스레드의 interrupted(), isInterrupted() 메소드는 true를 리턴한다. interrupted()는 정적 메소드로 현재 스레드가 interrupted되었는지 확인하고, isInterrupted()는 인스턴스 메소드로 현재 스레드가 interrupted되었는지 확인할 때 사용한다.

 

boolean status = Thread.interrupted();

boolean status = objThread.isInterrupted();

 

다음 예제는 위에서 작성한 PrintThread2를 수정하여 Thread.sleep(1)을 사용하지 않고, Thread.interrupted()를 사용해서 PrintThread2의 interrupt()가 호출되었는지 확인한다.

 

public class PrintThread2 extends Thread {

   public void run(){

           while (true){
               System.out.println("실행 중");
               if(Thread.interrupted()){
                   break;

               }
           }
      
       System.out.println("자원 정리");
       System.out.println("실행 종료");
   }
}

 

출처

이것이 자바다

반응형

댓글