Dico

[Java] Thread의 실행 제어

  • 민갤

쓰레드 Thread

프로세스의 자원을 이용해서 실제로 작업을 수행하는 것.

독립적인 작업을 수행하기 위해 개별적인 메모리 공간(호출스택)을 필요로 한다. 

 → 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정된다.

실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.

쓰레드의 실행 제어

정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비 없이 잘 사용하도록 프로그래밍한다.

    쓰레드의 스케줄링과 관련된 메서드

    반환 타입이름설명
      void  interrupt()  sleep()이나 join()에 의해 일시 정지 상태인 쓰레드를 깨워서 실행 대기 상태로 만든다.
      해당 쓰레드에서는 InterruptedException이 발생함으로써 일시 정지 상태를 벗어나게 된다.
      final void
      join()
      join(long millis)
      join(long millis, int nanos)
      지정된 시간 동안 쓰레드가 실행되도록 한다.
      지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
      final void  resume()  suspend()에 의해 일시 정지 상태에 있는 쓰레드를 실행 대기 상태로 만든다.
      static void
      sleep(long millis)
      sleep(long millis, int nanos)
      지정된 시간(1/1000초 단위) 동안 쓰레드를 일시 정지시킨다.
      지정한 시간이 지나고 나면, 자동적으로 다시 실행 대기 상태가 된다.
      final void  stop()  쓰레드를 즉시 종료시킨다
      final void  suspend()  쓰레드를 일시 정지 시킨다. resume()을 호출하면 다시 실행 대기 상태가 된다.
      static void  yield()  실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행 대기 상태가 된다.

    resume(), stop(), suspend()는 쓰레드를 교착상태로 만들기 쉽기 때문에 deprecated되었다.

    Thread.State

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

    sleep(long millis)

    일정 시간 동안 쓰레드를 멈추게 한다.

    millis(1/1000s)와 nanos(1/1억s)의 시간 단위로 세밀하게 값을 지정할 수 있지만 어느 정도의 오차가 발생할 수 있다.

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

    따라서 항상 try-catch문으로 예외를 처리해줘야 한다.

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

      • Example
      public class Test {
          public static final Logger logger = Logger.getLogger(Test.class.getName());
      
          public static void main(String[] args) {
              logger.info("Main Start");
      
              ThreadEx1 t1 = new ThreadEx1();
              ThreadEx2 t2 = new ThreadEx2();
              
              t1.start();
              t2.start();
      
              try {
                  t1.sleep(5 * 1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
              logger.info("Main End");
          }
      }
      
      class ThreadEx1 extends Thread {
      
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  if (i == 0)
                      Test.logger.info("ThreadEx1 Start");
              }
      
              Test.logger.info("ThreadEx1 End");
          }
      }
      
      class ThreadEx2 extends Thread {
          @Override
          public void run() {
      
              for (int i = 0; i < 100; i++) {
                  if (i == 0) {
                      Test.logger.info("ThreadEx2 Start");
                  }
              }
              Test.logger.info("ThreadEx2 End");
          }
      }
      3월 06, 2018 8:32:16 오후 blog.Test main
      정보: Main Start
      3월 06, 2018 8:32:16 오후 blog.ThreadEx1 run
      정보: ThreadEx1 Start
      3월 06, 2018 8:32:16 오후 blog.ThreadEx2 run
      정보: ThreadEx2 Start
      3월 06, 2018 8:32:16 오후 blog.ThreadEx1 run
      정보: ThreadEx1 End
      3월 06, 2018 8:32:16 오후 blog.ThreadEx2 run
      정보: ThreadEx2 End
      3월 06, 2018 8:32:21 오후 blog.Test main
      정보: Main End

      sleep()은 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에 't1.sleep(5 * 1000)'과 같이 호출했어도

      실제로 영향을 받는 것은 main 메서드(onCreate)를 실행하는 main 쓰레드다.

      따라서 sleep()은 static으로 선언되어 있으며, 참조변수를 이용하기 보다는 'Thread.sleep(5 * 1000)'과 같이 사용해야 한다.

      - yield()도 이와 같은 이유로 static으로 선언되어 있다.

      interrupt()와 interrupted()

      쓰레드의 작업을 취소한다.

      • void interrupt()

           쓰레드에게 작업을 멈추라고 요청한다.

           쓰레드를 강제로 종료시키지 못한다.

           쓰레드의 interrupted 상태를 false에서 true로 바꾼다.

           sleep(), wait(), join()에 의해 일시 정지 상태(WAITING)에 있을 때, 해당 쓰레드에 대해 interrupt()를 호출하면,

           sleep(), wait(), join()에서 InterruptedException이 발생하고 쓰레드는 실행대기 상태(RUNNABLE)로 바뀐다.

      • static boolean interrupted()

           현재 쓰레드에 대해 interrupt()가 호출되었는지 알려준 후 false로 초기화된다.

      • boolean isInterrupted()

           쓰레드의 interrupt()가 호출되었는지 확인한다. 

           쓰레드의 interrupted 상태를 초기화하지 않는다.

      public class Test {
          public static final Logger logger = Logger.getLogger(Test.class.getName());
      
          public static void main(String[] args) {
              ThreadEx t = new ThreadEx();
              t.start();
      
              try {
                  Thread.sleep(3 * 1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
              t.interrupt();
          }
      }
      
      class ThreadEx extends Thread {
          @Override
          public void run() {
              int i = 10;
      
              while (i != 0 && !isInterrupted()) {
                  Test.logger.info(i-- + "");
      
                  try {
                      sleep(1000);
                  } catch (InterruptedException e) {
                      interrupt();
                  }
              }
      
              Test.logger.info("isInterrupted : " + isInterrupted());
          }
      }
      3월 06, 2018 8:49:13 오후 blog.ThreadEx run
      정보: 10
      3월 06, 2018 8:49:14 오후 blog.ThreadEx run
      정보: 9
      3월 06, 2018 8:49:15 오후 blog.ThreadEx run
      정보: 8
      3월 06, 2018 8:49:16 오후 blog.ThreadEx run
      정보: isInterrupted : true

      ThreadEx의 catch 블럭에 interrupt()를 추가하지 않을 경우, 

      sleep(1000)에 의해 쓰레드가 잠시 멈춰있을 때, t.interrupt()를 호출하면 InterruptedException이 발생되고 

      쓰레드의 interrupted 상태가 false로 자동 초기화되어 while문이 종료되지 않는다.

      suspend(), resume(), stop()

      쓰레드의 실행을 제어하는 가장 손쉬운 방법이지만,

      suspend()와 stop()이 교착상태를 일으키기 쉽게 작성되어 있어 사용이 권장되지 않는다.

      그래서 이 메서드들은 모두 Deprecated 되었다.

      yield()

      다른 쓰레드에게 양보한다.

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

      public class Test {
          public static final Logger logger = Logger.getLogger(Test.class.getName());
      
          public static void main(String[] args) {
              ThreadEx t1 = new ThreadEx("*");
              ThreadEx t2 = new ThreadEx("**");
              ThreadEx t3 = new ThreadEx("***");
      
              t1.start();
              t2.start();
              t3.start();
      
              try {
                  Thread.sleep(2 * 1000);
                  t1.suspend();
                  Thread.sleep(2 * 1000);
                  t2.suspend();
                  Thread.sleep(2 * 1000);
                  t1.resume();
                  Thread.sleep(3 * 1000);
                  t1.stop();
                  t2.stop();
                  Thread.sleep(2 * 1000);
                  t3.stop();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
          }
      }
      
      class ThreadEx implements Runnable {
          boolean suspended = false;
          boolean stopped = false;
      
          Thread t;
      
          ThreadEx(String name) {
              t = new Thread(this, name);
          }
      
          @Override
          public void run() {
              String name = t.getName();
      
              while (!stopped) {
                  if (!suspended) {
                      Test.logger.info(name);
      
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          Test.logger.info(name + " - interrupted");
                      }
                  } else {
                      Thread.yield();    // 1.
                  }
              }
      
              Test.logger.info(name + " - stopped");
          }
      
          public void suspend() {
              suspended = true;
              t.interrupt();    // 2.
              Test.logger.info(t.getName() + " - interrupt() by suspend()");
          }
      
          public void resume() {
              suspended = false;
          }
      
          public void stop() {
              stopped = true;
              t.interrupt();    // 2.
              Test.logger.info(t.getName() + " - interrupt() by stop()");
          }
      
          public void start() {
              t.start();
          }
      }
      정보: ***
      정보: *
      정보: **
      정보: **
      정보: *
      정보: ***
      정보: * - interrupt() by suspend()
      정보: * - interrupted
      정보: **
      정보: ***
      정보: **
      정보: ***
      정보: ** - interrupt() by suspend()
      정보: ** - interrupted
      정보: ***
      정보: ***
      정보: *
      정보: ***
      정보: *
      정보: ***
      정보: *
      정보: ***
      정보: * - interrupted
      정보: * - stopped
      정보: * - interrupt() by stop()
      정보: ** - interrupt() by stop()
      정보: ** - stopped
      정보: ***
      정보: ***
      정보: *** - interrupt() by stop()
      정보: *** - interrupted
      정보: *** - stopped

      1. 코드의 whlie문에서 만일 suspended의 값이 true라면, 

          쓰레드는 주어진 실행 시간을 그저 while문을 의미 없이 돌면서 낭비하게 될 것이다. (바쁜 대기 상태. Busy-waiting)

          이 때, yield()를 호출하면 남은 실행 시간을 다른 쓰레드에게 양보하게 되므로 더 효율적이다.

      2. stop()이 호출되었을 때 Thread.sleep(1000)에 의해 쓰레드가 일시 정지 상태에 머물러 있는 상황이라면,

          stopped의 값이 true로 바뀌었어도 쓰레드가 정지될 때까지 최대 1초의 시간지연이 생길 것이다.

          이 때, interrupt()를 호출하면, sleep()에서 InterruptedException이 발생하여 즉시 일시 정지 상태에서 벗어나게 되므로 응답성이 좋아진다.

      join()

      자신의 작업 중간에 다른 쓰레드의 작업을 참여시킨다.

      작업 중에 다른 쓰레드의 작업이 먼저 수행되어야 할 필요가 있을 때 사용한다.

      interrupt()에 의해 대기상태에서 벗어날 수 있으며, try-catch문으로 감싸야 한다.

      현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static 메서드가 아니다.

      public class Test {
          public static void main(String[] args) {
              ThreadEx1 t1 = new ThreadEx1();
              ThreadEx2 t2 = new ThreadEx2();
      
              t1.start();
              t2.start();
      
              long startTime = System.currentTimeMillis();
      
              try {
                  t1.join();
                  t2.join();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
              System.out.print(System.currentTimeMillis() - startTime + "");
          }
      }
      
      class ThreadEx1 extends Thread {
      
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  System.out.printf("-");
              }
          }
      }
      
      class ThreadEx2 extends Thread {
          @Override
          public void run() {
      
              for (int i = 0; i < 100; i++) {
                  System.out.printf("|");
              }
          }
      }
      |||||||||||||||||||||||||||||||||||-----------------------------------------------------------|-------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----------47

      join()을 사용하여 쓰레드 t1, t2의 작업을 마칠 때까지 main 쓰레드가 기다리도록 했다.

      join()을 사용하지 않았다면 아래와 같이 main 쓰레드가 바로 종료된다.

      0||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||-||||||||||||||||||||||||||||||||||||---------------------------------------------------------------------------------------------------    

      참고 서적: 자바의 정석 3판 2