상태 및 실행제어

HyeonWoo·2021년 2월 18일
0
post-thumbnail

쓰레드 상태

상태설명
NEW쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
RUNNABLE실행 중 또는 실행 가능한 상태
BLOCKED동기화블럭에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태
WAITING, TIMED_WAITING쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은 (unrunnalbe) 일시정지 상태. TIMED_WAITING은 일시정지시간이 지정된 경우를 의미.
TERMINATED쓰레드의 작업이 종료된 상태

  1. 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐(queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.

  2. 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.

  3. 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.

  4. 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다. I/O block은 입출력작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데, 이런 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기 상태가 된다.

  5. 지정된 일시정지시간이 다되거나 (time-out), notify(), resume(), interrupt()가 호출되면 일시정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.

  6. 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸한다.


쓰레드 실행 제어

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

  • 얼마 동안 일시 정지 상태로 있을 것인지, 밀리세컨드(1/1000)단위로 지정

  • 일시 정지상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면, InterruptedException이 발생되어 잠에서 깨어나 실행대기 상태가 된다.

  • 따라서 sleep()을 호출할 때는 다음과 같이 try-catch문으로 예외를 처리해줘야 한다.

try {
  Thread.sleep(1000);
} catch(InterruptedException e) {
  //interrupt() 메소드가 호출되면 실행
}

interrupt(), interrupted() - 쓰레드의 작업을 취소한다.

  • ex) 큰 파일을 다운로드받을 때 시간이 너무 오래 걸리면 중간에 다운로드를 포기하고 취소할 수 있어야 함.

  • interrupt()는 쓰레드에게 작업을 멈추라고 요청함.

  • 단지 멈추라고 요청만 하는 것일 뿐, 쓰레드를 강제로 종료시키지는 못한다.

  • interrupted()는 쓰레드에 대해 interrupt()가 호출 되었는지 알려줌.

  • 쓰레드가 sleep(), wait(), join()에 의해 '일시정지 상태(WAITING)'에 있을 때, 해당 쓰레드에 대해 interrupt()를 호출하면, sleep(), wait(), join()에서 InterruptedException이 발생하고 쓰레드는 '실행대기 상태(RUNNABLE)'로 바뀜
    => 멈춰있던 쓰레드를 꺠워서 실행가능한 상태로 만듬.

suspend(), resume(), stop()

  • suspend()는 쓰레드를 멈추게 함.

  • resume()은 정지된 쓰레드를 다시 실행대기 상태로 만듬

  • stop()은 호출되는 즉시 쓰레드가 종료된다.

  • 이 메서드들은 모두 'deprecated(앞으로 사용하지 않을 것을 권장)'됨
    (교착상태를 일으키기 쉽게 작성되어있음.)

yield() - 다른 쓰레드에게 양보

  • yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보한다.

  • 예를들어, 스케쥴러에 의해 1초의 실행시간을 할당받은 쓰레드가 0.5초의 시간동안 작업한 상태에서 yield()가 호출되면, 나머지 0.5초는 포기하고 다시 실행대기상태가 된다.

  • yield()와 interrupt()를 적절히 사용하면, 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능하게 할 수 있다.


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

  • 쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 join()을 사용함.

  • 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다.

  • 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야할 필요가 있거나, 다른 쓰레드의 연산 결과가 필요할때 join()을 사용.

  • join()도 sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있기 때문에 try-catch문으로 감싸야 한다.


//ThreadA
threadB.start();
threadB.join();

//THREADB
run() {
...
}

쓰레드 A에서 쓰레드B의 start()를 호출하고 join()메소드를 호출하면 쓰레드는 A는 쓰레드B의 run()메소드의 실행이 종료 될 때까지 일시 정지 상태가 된다.


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

  • 이 메소드들은 쓰레드가 가지고 있는 메소드가 아니다.

  • 동기화 메소드 또는 동기화 블록에서만 호출 가능한 Object 메소드 이다.

  • 보다 효율적인 동기화를 가능하게 한다.

  • 2개의 쓰레드가 교대로 번갈아 가며 실행해야 할 경우에 주로 사용.

  • 동기화 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출하여 쓰레드가 락을 반납하고 기다리게 함.

  • 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있게 된다. 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서, 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 한다.

ex) 생산자 소비자 쓰레드
생산자 쓰레드가 공유 객체에 데이터를 저장하고, 소비자 쓰레드에서 데이터를 읽는 작업을 수행할 때를 생각해보자. 생산자 쓰레드에서 공유객체에 저장한 데이터를 소비자 쓰레드에서 두번 중복해서 처리하면 비 효율적인 처리 작업이 된다. 따라서 생산자 쓰레드에서 data필드에 값을 넣고 notify()를 호출해서 소비자 쓰레드가 data를 읽게 하고, data를 읽은 소비자 쓰레드는 다시 공유 객체를 비우고 notify()를 호출해서 생산자 쓰레드가 data필드에 값을 넣을 수 있도록 해준다.

  • Thread를 상속받은 ThreadB클래스
public class ThreadB extends Thread{
       // 해당 쓰레드가 실행되면 자기 자신의 모니터링 락을 획득
       // 5번 반복하면서 0.5초씩 쉬면서 total에 값을 누적
       // 그후에 notify()메소드를 호출하여 wiat하고 있는 쓰레드를 깨움 
        int total;
        
        @Override
        public void run(){
            synchronized(this){
                for(int i=0; i<5 ; i++){
                    System.out.println(i + "를 더합니다.");
                    total += i;
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                notify();
            }
        }
    }
  • ThreadB를 사용하며 wait하는 클래스
    public class ThreadA {
        public static void main(String[] args){
            // 앞에서 만든 쓰레드 B를 만든 후 start 
            // 해당 쓰레드가 실행되면, 해당 쓰레드는 run메소드 안에서 자신의 모니터링 락을 획득
            ThreadB b = new ThreadB();
            b.start();

            // b에 대하여 동기화 블럭을 설정
            // 만약 main쓰레드가 아래의 블록을 위의 Thread보다 먼저 실행되었다면 wait를 하게 되면서 모니터링 락을 놓고 대기       
            synchronized(b){
                try{
                    // b.wait()메소드를 호출.
                    // 메인쓰레드는 정지
                    // ThreadB가 5번 값을 더한 후 notify를 호출하게 되면 wait에서 깨어남
                    System.out.println("b가 완료될때까지 기다립니다.");
                    b.wait();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }

                //깨어난 후 결과를 출력
                System.out.println("Total is: " + b.total);
            }
        }
    }
    

-실행 결과

   b가 완료될때까지 기다립니다.
   0를 더합니다.
   1를 더합니다.
   2를 더합니다.
   3를 더합니다.
   4를 더합니다.
   Total is: 10
   

참고자료

https://programmers.co.kr/learn/courses/9/lessons/278,
자바의 정석

profile
학습 정리, 자기 개발을 위한 블로그

0개의 댓글