스레드 제어 & 생명주기

이동건 (불꽃냥펀치)·2024년 12월 21일
0

스레드 기본 정보

Thread 클래스는 스레드를 생성하고 관리하는 기능을 제공한다.

스레드 생성

스레드를 생성할 때는 실행할 Runnable 인터페이스의 구현체와 스레드의 이름을 전달 할 수 있다.

Thread myThread = new Thread(new HelloRunnable(), "myThread");

스레드 이름: 스레드의 이름을 반환하는 메서드이다. 생성자에서 myThread라는 이름을 지정해 그 값이 반환된다.

log("myThread.getName() = " + myThread.getName());

스레드 우선순위: 스레드의 우선순위를 반환하는 메서드이다. 우선순위는 1(가장 낮음)부터 10(가장 높음)까지의 값으로 설정이 가능하며, 기본값은 5이다. setPriority()메서드를 사용해서 우선순위를 변경할 수 있다.

스레드 그룹 getThreadGroup() : 스레드가 속한 스레드 그룹을 반환하는 메서드이다. 기본적으로 모든 스레드는 부모 스레드와 동일한 스레드 그룹에 속하게 된다.

  • 부모스레드: 새로운 스레드를 생성하는 스레드를 의미한다. 스레드는 기본적으로 다른 스레드에 의해 생긴다. 이러한 생성관계에서 새로 생성된 스레드는 생성한 스레드를 부모로 간주한다. 예를 들어 myThreadmain 스레드에 의해 생성되었으므로 main 스레드가 부모이다.

스레드 상태 geteState(): 스레드의 현재 상태를 반환하는 메서드이다. 반환되는 값은 Thread.State 열거형에 정의된 상수중 하나이다.

  • New:스레드가 아직 시작되지 않은 상태
  • RUNNABLE: 스레드가 실행 중이거나 실행될 준비가 된 상태
  • BLOCKED: 스레드가 동기화 락을 기다리는 상태
  • WAITING: 스레드가 다른 스레드의 특정 작업이 완료되기를 기다리는 상태
  • TIME_WAITING: 일정 시간 동안 기다리는 상태
  • TERMINATED: 스레드가 실행을 마친 상태


스레드의 생명주기

  • NEW(새로운 상태)
    • 스래드가 생성되고 아직 시작되지 않은 상태이다
    • 스레드 객체가 생성되었지만 start() 메서드가 호출되지 않은 상태이다.


  • Runnable(실행 가능한 상태)
    • 스레드가 실행될 준비가 된 상태이다.
    • start()메서드가 호출되면 스레드는 이 상태로 들어간다.
    • 이 상태는 스레드가 실행될 준비가 되어 있음을 나타내며 실제로 CPU에서 실행될 수 있는 상태이다.


  • Blocked(차단 상태)
    • 스레드가 다른 스레드에 의해 동기화 락을 얻기 위해 기다리는 상태이다.
    • 예를들어 synchronized블록에 진입하기 위해 락을 얻어야 하는 경우 이 상태에 들어간다.



  • Waiting(대기상태)
    • 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.
    • wait(),join() 메서드가 호출될때 이 상태이다.
    • 스레드는 다른 스레드가 noify(),notifyAll()을 호출하거나 join()이 완료될 때까지 기다린다.


  • Timed Waiting(시간 제한 대기상태)
    • 스레드가 특정 시간동안 다른 스레드의 작업이 완료되기를 기다리는 상태이다.
    • sleep(long millis) , wait(long timeout) , join(long millis)메서드가 호출될때 이 상태가 된다.


  • Terminated(종료상태)
    • 스레드의 실행이 완료된 상태이다
    • 스레드가 정상적으로 종료되거나 예외가 발생하여 종료된 경우 이 상태로 들어간다.
    • 스레드는 한번 종료되면 다시 시작할 수 없다.



체크 예외 재정의

Runnable 인터페이스의 run()메서드를 구현할 때 체크 예외는 밖으로 던질 수 없다.

 public interface Runnable {
     void run();
}

자바에서 매서드를 재정의 할 때 부모 메서드가 체크 예외를 던지지 않는경우 재정의된 자식 메서드도 체크 예외를 던질 수 없다. 자식 메서드는 부모 메서드가 던지는 예외 하위만 던질 수 있다.

하지만 보시다시피 runnable인터페이스는 예외를 던지지 않아 이를 상속받는 클래스들은 예외를 던질 수 없다.



Join

waiting은 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.

public class JoinMainV0 {
     public static void main(String[] args) {
         log("Start");
         Thread thread1 = new Thread(new Job(), "thread-1");
         Thread thread2 = new Thread(new Job(), "thread-2");
         thread1.start();
         thread2.start();
         log("End");
     }
     static class Job implements Runnable {
         @Override
         public void run() {
			log("작업 시작"); sleep(2000); log("작업 완료");
		} 
	}
}

위 구문 결과값을 출력하면 가끔 메인 메서드가 스레드의 결과값이 나오기도 전에 종료되는 경우가 있다.

메인 스레드는 다른 스레드에게 작업지시만 할 뿐이지 그들의 작업이 끝날때까지 기다리지 않는 점을 기억하자.

하지만 메인 스레드가 무조건 다른 스레드의 결과값이 나온 후에 종료할려면 join을 사용해야한다.

 int sum = 0;
 for (int i = 1; i <= 100; i++) {
		sum += i;
}

이 코드는 스레드를 하나만 사용하기 때문에 CPU 코어도 하나만 사용할 수 있다. 코어를 더 효율적으로 사용하기 위해서는 여러 스레드로 나눠 계산하면 된다.

public class JoinMainV1 {
    public static void main(String[] args) {
        log("Start");
        SumTask task1 = new SumTask(1, 50);
        SumTask task2 = new SumTask(51, 100);
        Thread thread1 = new Thread(task1, "thread-1");
        Thread thread2 = new Thread(task2, "thread-2");
        thread1.start();
        thread2.start();
        log("task1.result = " + task1.result);
        log("task2.result = " + task2.result);
        int sumAll = task1.result + task2.result;
        log("task1 + task2 = " + sumAll);
        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("작업 시작");
            sleep(2000);
            int sum = 0;
            for (int i = startValue; i <= endValue; i++) {
            sum += i; }
            result = sum;
            log("작업 완료 result=" + result);
            } 
      }
}

좀 더 효율적이 될것이라 믿고 코드를 돌려보면 5050이 아닌 0이 나온다.

0이 나온 이유는 메인 스레드가 다른 스레드의 계산 결과를 기다리지않고 스레드를 종료시켜버려서 0이 나온 것이다.

이 문제를 해결하기 위해서는 스레드가 실행 후 sleep을 걸어 timed-waiting 상태로 만들어 버릴 수도 있지만 이것보다 join()메서드를 활용하면 문제가 더 쉽게 해결된다.

public class JoinMainV3 {
     public static void main(String[] args) throws InterruptedException {
         log("Start");
         SumTask task1 = new SumTask(1, 50);
         SumTask task2 = new SumTask(51, 100);
         Thread thread1 = new Thread(task1, "thread-1");
         Thread thread2 = new Thread(task2, "thread-2");
         thread1.start();
         thread2.start();
        // 스레드가 종료될 때 까지 대기
        log("join() - main 스레드가 thread1, thread2 종료까지 대기"); thread1.join();
        thread2.join();
        log("main 스레드 대기 완료");
         log("task1.result = " + task1.result);
         log("task2.result = " + task2.result);
         int sumAll = task1.result + task2.result;
         log("task1 + task2 = " + sumAll);
         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("작업 시작");
             sleep(2000);
             int sum = 0;
             for (int i = startValue; i <= endValue; i++) {
sum += i; }
result = sum;
log("작업 완료 result = " + result);
		} 
	}
}

main에서 다음 코드를 실행하면 메인 스레드는 1,2 스레드가 종료될때 까지 기다린다. 이때 메인 스레드는 waiting상태가 된다.

Waiting (대기 상태)

스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.

join() 을 호출하는 스레드는 대상 스레드가 TERMINATED 상태가 될 때 까지 대기한다.

대상 스레드가 TERMINATED 상태가 되면 호출 스레드는 다시 RUNNABLE 상태가 되면서 다음 코드를 수행한다.

이렇듯 특정 스레드가 완료될 때 까지 기다려야 하는 상황이라면 join() 을 사용하면 된다

  • join() : 호출 스레드는 대상 스레드가 완료될 때 까지 무한정 대기한다.
  • join(ms) : 호출 스레드는 특정 시간 만큼만 대기한다. 호출 스레드는 지정한 시간이 지나면 다시 RUNNABLE
    상태가 되면서 다음 코드를 수행한다.

interrupt

interrupt를 사용하면 Waiting,Timed_Waiting 같은 대기 상태의 스레드를 직접 깨워서 작동하는 Runnable 상태로 만들 수 있다.

  • 인터럽트가 발생하면 해당 스레드에 InterruptedException이 발생한다.
  • 이때 인터럽트를 받은 스레드는 대기 상태에서 깨어나 Runnable상태가 되고 코드를 정상 수행한다.
  • InterruptedExceptioncatch로 잡아 정상 흐름으로 변경하면 된다.


자바에서는 스레드가 인터럽트 상태인지 알려주는 isInterrupted() 메서드를 제공해 주기도 한다.

  • 인터럽트를 실행 한 후 스레드의 상태를 알리는 isInterrupted는 인터럽트의 상태를 변경하는 것이 아닌 단순히 인터럽트의 상태를 확인만 한다.


스레드의 인터럽트 상태를 직접 체크해서 사용할 때는 Thread.interrupted()를 사용하면 된다.

  • 스레드가 인터럽트 상태라면 true를 반환하고 해당 스레드의 상태를 false로 변경한다.
  • 스레드가 인터럽트 상태가 아니라면 false를 반환하고 해당 스레드의 상태를 변경하지 않는다.



인터럽트 예제

public static void main(String[] args) throws InterruptedException {
    Printer printer = new Printer();
    Thread printerThread = new Thread(printer, "printer");
    printerThread.start();
    Scanner userInput = new Scanner(System.in);
    while (true) {
log("프린터할 문서를 입력하세요. 종료 (q): "); 
String input = userInput.nextLine(); if (input.equals("q")) {
            printer.work = false;
break;
}
        printer.addJob(input);
    }
}

static class Printer implements Runnable {
    volatile boolean work = true;
    Queue<String> jobQueue = new ConcurrentLinkedQueue<>();
    @Override
    public void run() {
        while (work) {
            if (jobQueue.isEmpty()) {
                continue; 
                }
                String job = jobQueue.poll();
                log("출력 시작: " + job + ", 대기 문서: " + jobQueue); 
                sleep(3000); //출력에 걸리는 시간
                log("출력 완료: " + job);
                }
                log("프린터 종료"); }
                    public void addJob(String input) {
                        jobQueue.offer(input);
}
  • main 스레드: 사용자의 입력을 받아서 Printer 인스턴스의 jobQueue에 담는다.
  • Printer 스레드: jobQueue에 내용이 있으면 poll()메서드를 이용해 내용을 꺼낸다.
  • 여기에서 문제는 q를 입력한 후 바로 종료되지 않는다는 점이다.
  • 인터럽트를 활용해서 반응성이 느린 문제를 해결해보겠다.


  public static void main(String[] args) throws InterruptedException {
         Printer printer = new Printer();
         Thread printerThread = new Thread(printer, "printer");
         printerThread.start();
         Scanner userInput = new Scanner(System.in);
         while (true) {
			System.out.println("프린터할 문서를 입력하세요. 종료 (q): "); 
            String input = userInput.nextLine();
			if (input.equals("q")) {
                 printerThread.interrupt();
				break; 
                }
             printer.addJob(input);
         }
          }
    static class Printer implements Runnable {
        Queue<String> jobQueue = new ConcurrentLinkedQueue<>();
        @Override
        public void run() {
            while (!Thread.interrupted()) {
                if (jobQueue.isEmpty()) {
					continue; 
                    }
				try {
					String job = jobQueue.poll();
					log("출력 시작: " + job + ", 대기 문서: " + jobQueue); 
                    Thread.sleep(3000); //출력에 걸리는 시간
					log("출력 완료: " + job);
					
					} catch (InterruptedException e) { 
						log("인터럽트!");
						break; 
                    }
			}
			log("프린터 종료"); 
  }
  • Thread.interrupted()메서드를 이용하면 해당 스레드가 인터럽트 상태인지 아닌지 확인할 수 있다.



Yield 양보하기

어떤 스레드를 얼마나 실행할지는 운영체제가 스케줄링을 통해 결정한다.그런데 특정 스레드가 바쁘지 않은 상황이어서 다른 스레드에 CPU 실행 기회를 양보하고 싶을 수 있다. 이렇게 양보하면 스큐줄링 큐에 대기 중인 다른 스레드가 CPU 실행기회를 더 빨리 얻을 수 있다.








출처: https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%9E%90%EB%B0%94-%EA%B3%A0%EA%B8%89-1/dashboard

profile
자바를 사랑합니다

0개의 댓글

관련 채용 정보