포스트

스레드 제어와 생명 주기

스레드 제어와 생명 주기

스레드 기본 정보

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ThreadInfoMain {

  public static void main(String[] args) {

    // main 스레드
    Thread mainThread = Thread.currentThread();
    log("main mainThread = " + mainThread);

    // 고유 식별자
    log("mainThread.threadId() = " + mainThread.threadId());

    // 스레드 이름
    log("mainThread.getName() = " + mainThread.getName());

    // 우선 순위 1 ~ 10까지 있음
    log("mainThread.getPriority() = " + mainThread.getPriority());

    // 스레드가 속한 그룹
    // 기본적으로 모든 스레드는 부모 스레드와 동일한 그룹에 속함
    log("mainThread.getThreadGroup() = " + mainThread.getThreadGroup());

    // 스레드 상태
    log("mainThread.getState() = " + mainThread.getState());
  }
}

스레드 생명 주기

스레드 상태

  • New: 스레드가 생성되었으나 아직 시작되지 않은 상태
  • Runnable: 스레드가 실행 중이거나 실행될 준비가 된 상태
  • Blocked: 스레드가 동기화 락을 기다리는 상태
  • Waiting: 스레드가 무기한으로 다른 스레드의 작업을 기다리는 상태
  • Timed Waiting: 스레드가 일정 시간 동안 다른 스레드의 작업을 기다리는 상태
  • Terminated: 스레드의 실행이 완료된 상태

스레드의 작업이 끝나기를 기다리기 - Join

예시

1 ~ 100까지 더하는 작업을 스레드1이 1~50까지 더하고 스레드2가 51~100을 더해서 결과로 나온 값을 더하는 로직이 필요하다고 가정해보겠습니다.

join() 사용 안하고 해결

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class JoinMainV2 {

  public static void main(String[] args) {
    log("start");

    SumTask sumTask1 = new SumTask(1, 50);
    SumTask sumTask2 = new SumTask(51, 100);

    Thread thread1 = new Thread(sumTask1, "thread-1");
    Thread thread2 = new Thread(sumTask2, "thread-2");

    thread1.start();
    thread2.start();

    // 정확한 타이밍을 맞추어 기다리기 어려움
    log("main thread sleep");
    sleep(3000);
    log("main thread 깨어남");

    log("task1.result = " + sumTask1.result);
    log("task2.result = " + sumTask2.result);

    log("end");

  }

  static class SumTask implements Runnable {

    int startValue;
    int endValue;
    int result = 0;

    public SumTask(int startValue, int endValue) {
      this.startValue = startValue;
      this.endValue = endValue;
    }

    @Override
    public void run() {
      log("작업 시작");
      int sum = 0;
      for (int i = startValue; i <= endValue; i++) {
        sum += i;
      }
      sleep(2000);
      result = sum;
      log("작업 완료 result = " + result);

    }
  }
}

각 작업이 2초 정도의 시간이 걸린다고 가정하겠습니다. 이런 경우 두 작업이 다 끝나고 나서 결과 값을 조회해야 하므로 main 스레드에서 3초 정도 대기 후 조회 하도록 해야 합니다.

실행 시간이 2초라고 가정했지만, 실제로는 각 작업이 얼마나 걸릴지 모르는 병렬 처리가 필요한 순간이 더 많습니다.

이럴 때 join() 메서드를 사용하여 처리하면 효율적으로 처리 할 수 있습니다.

join() 사용 하여 해결

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class JoinMainV3 {

  public static void main(String[] args) throws InterruptedException {
    log("start");

    SumTask sumTask1 = new SumTask(1, 50);
    SumTask sumTask2 = new SumTask(51, 100);

    Thread thread1 = new Thread(sumTask1, "thread-1");
    Thread thread2 = new Thread(sumTask2, "thread-2");

    thread1.start();
    thread2.start();

    // 스레드가 종료 될 때 까지 대기
    log("join() main 스레드가 thread1, thread2 종료까지 대기");
    thread1.join();
    thread2.join();
    log("main 스레드 대기 완료");

    log("task1.result = " + sumTask1.result);
    log("task2.result = " + sumTask2.result);

    log("end");

  }

  static class SumTask implements Runnable {

    int startValue;
    int endValue;
    int result = 0;

    public SumTask(int startValue, int endValue) {
      this.startValue = startValue;
      this.endValue = endValue;
    }

    @Override
    public void run() {
      log("작업 시작");
      int sum = 0;
      for (int i = startValue; i <= endValue; i++) {
        sum += i;
      }
      sleep(2000);
      result = sum;
      log("작업 완료 result = " + result);

    }
  }
}
  • start() 호출 시 스레드 시작
    • thread1, thread2 둘 다 start() 호출 했으므로 병렬로 실행 됨
  • join() 호출 시, main 스레드는 해당 스레드가 끝나기를 대기 (Wating` 상태)
    • thread1.join()이 끝나면 thread2.join() 실행
  • 작업이 끝나면 main 스레드 다시 실행

잘못된 사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class JoinTest1Main {
  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new MyTask(), "t1");
    Thread t2 = new Thread(new MyTask(), "t2");
    Thread t3 = new Thread(new MyTask(), "t3");

    t1.start();
    t1.join();

    t2.start();
    t2.join();

    t3.start();
    t3.join();
    System.out.println("모든 스레드 실행 완료");
  }

  static class MyTask implements Runnable {

    @Override
    public void run() {
      for (int i = 1; i <= 3; i++) {
        log(i);
        sleep(1000);
      }
    }
  }
}

위 처럼 사용 시, 스레드 시작하고 작업이 끝나면 다음 스레드를 시작하고 하므로 총 9초의 시간이 걸립니다.

이는 스레드를 병렬로 사용하는 방법이 아닙니다.

올바른 사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class JoinTest2Main {
  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new MyTask(), "t1");
    Thread t2 = new Thread(new MyTask(), "t2");
    Thread t3 = new Thread(new MyTask(), "t3");

    t1.start();
    t2.start();
    t3.start();

    t1.join();
    t2.join();
    t3.join();
    System.out.println("모든 스레드 실행 완료");
  }

  static class MyTask implements Runnable {

    @Override
    public void run() {
      for (int i = 1; i <= 3; i++) {
        log(i);
        sleep(1000);
      }
    }
  }
}

위 처럼 실행하면 3개의 스레드가 동시에 실행되고, 스레드 별로 끝나기를 기다리므로 3초만에 끝나게 됩니다.

특정 시간만큼만 기다리기

join() 메서드는 기본적으로 해당 작업이 끝날 때 까지 무한정 기다립니다. join() 메서드에 인자로 ms를 넘겨주면 해당 시간만큼만 기다리고 다시 main 스레드를 돌아와 실행하도록 할 수도 있습니다.

1
thread1.join(5000) // 5초만 기다리기 (Timed Waitng 상태)

스레드 작업을 중간에 중단하기 - Interrupt

예시

변수를 이용하여 중단하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ThreadStopMainV1 {

  public static void main(String[] args) {

    MyTask myTask = new MyTask();
    Thread thread = new Thread(myTask, "work");
    thread.start();

    sleep(4000);
    log("작업중단 지시 runFlag = false");
    myTask.runFlag = false;
  }

  static class MyTask implements Runnable {

    volatile boolean runFlag = true;

    @Override
    public void run() {

      while (runFlag) {
        log("작업 중");
        sleep(3000);
      }
      log("자원 정리");
      log("작업 종료");
    }
  }
}

위 방법은 runFlag의 값을 변경하여 작업을 종료하는 방법입니다. 하지만 이 방법은 바로 종료 되지 않고 sleep()(대기 상태)이 끝난 다음에 종료되게 됩니다.

interrupt()를 이용하여 중단하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ThreadStopMainV2 {

  public static void main(String[] args) {

    MyTask myTask = new MyTask();
    Thread thread = new Thread(myTask, "work");
    thread.start();

    sleep(4000);
    log("작업중단 지시 thread.interrupt()");
    thread.interrupt(); // interrupt로 해당 스레드 중단
    log("work스레드 인터럽트 상태1 = " + thread.isInterrupted());

  }

  static class MyTask implements Runnable {

    @Override
    public void run() {

      try {
        while (true) {
          log("작업 중");
          Thread.sleep(3000);
        }
      } catch (InterruptedException e) {
        log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
        log("interrupt message = " + e.getMessage());
        log("state = " + Thread.currentThread().getState());
      }
      log("자원 정리");
      log("작업 종료");
    }
  }
}

위 처럼 코드를 변경하면 InterruptedException 예외가 발생하여 해당 스레드가 종료 됩니다.

thread.interrupt()를 실행했다고 해서 바로 InterruptedException 예외가 발생하는 것은 아닙니다.

InterruptedException 예외를 발생시키는 로직을 만났을 때 (여기선 sleep()) 예외가 발생하게 됩니다.

interrupt 상태 확인하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ThreadStopMainV3 {

  public static void main(String[] args) {

    MyTask myTask = new MyTask();
    Thread thread = new Thread(myTask, "work");
    thread.start();

    sleep(100); // 시간을 줄임
    log("작업중단 지시 thread.interrupt()");
    thread.interrupt();
    log("work스레드 인터럽트 상태1 = " + thread.isInterrupted());

  }

  static class MyTask implements Runnable {

    @Override
    public void run() {

      // 코드 수정
      while (!Thread.currentThread().isInterrupted()) { // 인터럽트 상태 변경 X
        log("작업 중");
      }

      log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());

      try {
        log("자원 정리");
        Thread.sleep(1000);
        log("자원 종료");
      } catch (InterruptedException e) {
        log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
        log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
      }
      log("작업 종료");
    }
  }
}

위 로직은 인터럽트 상태를 확인하여 로직을 실행할 지 판단하도록 수정하였습니다.

하지만 인터럽트 상태가 true이면 InterruptedException 예외를 발생 시키는 로직을 만나야 InterruptedException 예외를 발생시키고 인터럽트 상태를 false로 변경하는데 위 while문에서는 상태를 “확인만” 하므로 상태 값이 여전히 true입니다.

따라서 sleep()을 만나야 InterruptedException 예외가 발생하게 되고, 자원 종료를 정상적으로 하지 못하게 됩니다.

Thread.interrupted를 이용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ThreadStopMainV4 {

  public static void main(String[] args) {

    MyTask myTask = new MyTask();
    Thread thread = new Thread(myTask, "work");
    thread.start();

    sleep(100); // 시간을줄임
    log("작업중단 지시 thread.interrupt()");
    thread.interrupt();
    log("work스레드 인터럽트 상태1 = " + thread.isInterrupted());

  }

  static class MyTask implements Runnable {

    @Override
    public void run() {

      // 코드 수정
      while (!Thread.interrupted()) { // 인터럽트 상태 변경 O
        log("작업 중");
      }
      log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());

      try {
        log("자원 정리");
        Thread.sleep(1000);
        log("자원 종료");
      } catch (InterruptedException e) {
        log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
        log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
      }
      log("작업 종료");
    }
  }
}

Thread.interrupted() 메서드는 상태를 확인하면서 상태 값을 false 로 변경합니다.

따라서 sleep() 로직을 만나도 InterruptedException 예외가 발생하지 않고 정상적으로 자원을 종료합니다.

yield

어떤 스레드를 얼마나 실행 할지는 스케줄링을 통해 결정합니다. yield를 사용하면 다른 스레드에게 실행 기회를 양보할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class YieldMain {

  static final int THREAD_COUNT = 1000;

  public static void main(String[] args) {

    for (int i = 0; i < THREAD_COUNT; i++) {

      Thread thread = new Thread(new MyRunnable());
      thread.start();
    }
  }

  static class MyRunnable implements Runnable {

    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        // 1. empty
        System.out.println(Thread.currentThread().getName() + " - " + i);

        // 2. sleep(1)
        //                sleep(1);

        // 3. yield
        Thread.yield();
      }
    }
  }
}
  • sleep() 사용
    • TIMED_WAITING 상태가 되어 다른 스레드에게 실행 기회 양보
    • 하지만 RUNNABLETIMED_WAITINGRUNNABLE 로 복잡한 과정을 거치게 됨
  • yield 사용
    • 다른 스레드에게 실행 기회를 넘기고 다시 스케줄러로 들어감 (RUNNABLERUNNABLE)
    • 따라서 sleep() 보다 효율적

참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.