728x90

스레드 정보

Thread 클래스에서 제공하는 메서드를 이용하여 스레드의 정보를 확인할 수 있음

public class ThreadInfoMain {

    public static void main(String[] args) {
        // main 스레드
        Thread mainThread = Thread.currentThread();
        log("mainThread = " + mainThread);
        log("mainThread.threadId()=" + mainThread.threadId());
        log("mainThread.getName()=" + mainThread.getName());
        log("mainThread.getPriority()=" + mainThread.getPriority());
        log("mainThread.getThreadGroup()=" + mainThread.getThreadGroup());
        log("mainThread.getState()=" + mainThread.getState());

        // myThread 스레드
        Thread myThread = new Thread(new HelloRunnable(), "myThread");
        log("myThread = " + myThread);
        log("myThread.threadId()=" + myThread.threadId());
        log("myThread.getName()=" + myThread.getName());
        log("myThread.getPriority()=" + myThread.getPriority());
        log("myThread.getThreadGroup()=" + myThread.getThreadGroup());
        log("myThread.getState()=" + myThread.getState());
    }
}

 

스레드 생명주기

스레드 생명주기

 

스레드 상태

 

상태 설명 상태 전이
NEW 스레드가 생성되었으나 아직 시작되지 않은 상태 • NEW → RUNNABLE: start() 호출
RUNNABLE 스레드가 실행 중이거나 실행될 준비가 된 상태 • RUNNABLE → BLOCKED: 스레드가 lock을 얻지 못한 경우

• RUNNABLE → WAITING: wait() , join() 호출

• RUNNABLE → TIMED_WAITING: sleep(long millis) , wait(long timeout) , join(long millis) 호출

• RUNNABLE → TERMINATED: 스레드 실행 완료
BLOCKED 스레드가 동기화 락을 기다리는 상태 • BLOCKED → RUNNABLE: lock 얻은 경우
WAITING 스레드가 무기한으로 다른 스레드의 작업을 기다리는 상태 • WAITING → RUNNABLE: notify() 또는 notifyAll() 호출, join() 이 완료될 때
TIMED_WATING 스레드가 일정 시간 동안 다른 스레드의 작업을 기다리는 상태 • TIMED_WATING → RUNNABLE: 설정한 시간 경과 후
TERMINATED 스레드의 실행이 완료된 상태  

 

 

join()

특정 스레드가 완료될 때 까지 기다려야 하는 상황에서 사용

  • 특정 스레드의 작업이 완료될 때까지 기다리는 작업을 sleep()을 통해서 구현하면 정확한 종료 시점을 예측할 수 없음
  • join()을 사용하면 특정 스레드가 종료될 때까지 기다릴 수 있음

다만 단순히 join()을 사용하면 다른 스레드가 완료될 때까지 무기한 기다림

  • 이러한 경우 join(long millis)를 사용하여 타임아웃을 지정할 수 있음

 

interrupt(), isInterrupted(), interrupted()

특정 스레드의 작업을 중간에 중단하는 방법

  • 가장 쉬운 방법은 변수로 flag를 설정하여 스레드가 종료되도록 하는 방법
  • 스레드 내부에서 flag를 확인하기 때문에 스레드 내 로직(e.g. sleep()) 또는 flag 검사 주기에 따라 바로 종료되지 않을 수 있음

WAITING, TIMED_WAITING 상태의 스레드를 직접 깨워서 RUNNABLE 상태로 전환시키는 방법

  • 예를 들어 sleep()에 의해 TIMED_WAITING 상태에 있는 스레드를 깨우기 위해 interrupt()를 호출하면 아래 동작 실행
    • InterruptedException 발생
      • interrupt() 를 호출했다고 해서 즉각 InterruptedException 이 발생하는 것은 아님
      • sleep()처럼 InterruptedException을 던지는 메서드가 호출 또는 실행될 때 예외가 발생
    • TIMED_WAITING 상태에서 RUNNABLE 상태로 전이
    • catch 블록 내 코드가 실행 (RUNNABLE 상태여야 catch 블록 내 코드가 실행될 수 있음)
  • interrupt 상태는 interrupt()를 호출 시 true로 변경되고, InterruptedException이 발생하면 false로 변경됨

InterruptedException에 대한 catch 구문으로만 스레드를 중단하는 경우 InterruptedException을 던지지 않는 부분(e.g. log 출력)은 그대로 실행되고 다음 코드로 진행

  • 더욱 빠르게 작업을 중단하고 싶다면 Thread.interrupted()를 통해 인터럽트 상태를 확인하여 스레드를 종료할 수 있음
    • 인터럽트 상태인 경우 true를 반환하고, 인터럽트 상태를 false로 변경
    • 인터럽트 상태가 아닌 경우 false를 반환, 인터럽트 상태 변경 없음
  • 만약 isInterrupted() 값을 통해 스레드의 인터럽트 상태를 확인하여 스레드를 종료하는 경우 InterruptedException이 발생하지 않기 때문에 인터럽트 상태가 변경되지 않음 (isInterrupted() = true)
    • 이 경우 인터럽트 상태이기 때문에 이후 코드에서 sleep()처럼 InterruptedException을 던지는 메서드가 실행되면 해당 위치에서 기대하지 않았던 InterruptedException이 발생할 수 있음
public class Main {

  public static void main(String[] args) {
    MyTask task = new MyTask();
    Thread thread = new Thread(task, "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
      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("작업 종료");
    }
  }
}

 

yield

스레드의 상태를 유지(RUNNABLE 상태)하면서 다른 스레드에게 CPU를 양보(OS에 힌트를 제공)하는 메서드

  • sleep()을 사용하는 경우 RUNNABLETIMED_WAITINGRUNNABLE로 상태가 전이되는 과정을 거치고, 특정 시간동안 스레드가 실행되지 않음
  • 반면 yield()를 사용하는 경우 RUNNABLE 상태로 유지(스케줄링 큐 대기)되고, 다른 스레드에게 CPU를 양보하고 다시 스케줄링 큐에 들어가서 CPU를 할당받음

만약 무의미한 작업이 반복돼서 CPU를 점유하는 경우 yield()를 호출을 통해 다른 스레드에게 CPU를 양보하여 성능을 향상시킬 수 있음

728x90

'Java > 멀티스레드' 카테고리의 다른 글

[JAVA] 스레드 생성과 실행  (1) 2024.09.18
728x90

스레드 생성

  • Thread 클래스를 상속받아 run() 메서드를 오버라이딩하여 스레드를 생성
  • Runnable 인터페이스를 구현하여 스레드를 생성

 

Thread 클래스 상속

스레드 클래스 정의

public class HelloThread extends Thread {

    @Override
    public void run() {
        doSomething();
    }
}

 

스레드 생성 및 실행

  • start() 메서드를 호출하여 스레드를 실행
    • main 스레드와 별도의 스레드를 실행
  • run() 메서드를 직접 호출하면 main 스레드에서 실행
    • main 스레드는 HelloThread 인스턴스에 있는 run() 메서드를 호출하는 형태
public class HelloThreadMain {

    public static void main(String[] args) {
        HelloThread helloThread = new HelloThread();
        helloThread.start();
    }
}

 

 

데몬 스레드

스레드는 사용자(user) 스레드와 데몬(daemon) 스레드로 구분

차이는 JVM 종료 시점

  • 모든 user 스레드가 종료되면 JVM도 종료
  • 데몬 스레드는 user 스레드가 모두 종료되면 데몬 스레드도 종료 (why? JVM이 종료돼서)

setDaemon(true) 메서드를 호출하여 데몬 스레드로 설정

  • 데몬 스레드 여부는 start() 실행 전에 결정 (이후에 수정 불가)
public class DaemonThreadMain {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": main() start");
        DaemonThread daemonThread = new DaemonThread();
        daemonThread.setDaemon(true); // 데몬 스레드 여부
        daemonThread.start();
        System.out.println(Thread.currentThread().getName() + ": main() end");
    }

  static class DaemonThread extends Thread {

    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName() + ": run()");
      try {
        Thread.sleep(10000); // 10초간 실행
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }

      System.out.println(Thread.currentThread().getName() + ": run() end");
    }
  }
}

 

 

run() 메서드 안에서 Thread.sleep() 를 호출할 때 checked exception 인 InterruptedException 을 밖으로 던질 수 없고 반드시 잡아야 함

  • why? run() 메서드는 부모 클래스 Thread의 인터페이스인 Runnable 인터페이스에서 throws를 선언하지 않고 있음
    • 자바 오버라이드 규칙에 따라 부모 메서드 또는 인터페이스가 체크 예외를 던지지 않는 경우, 재정의된 자식 메서드도 체크 예외를 던질 수 없음
  • InterruptedException은 checked exception 이기 때문에 try-catch 블록으로 예외를 잡아야 함

 

 

Runnable 인터페이스 구현

Runnable 인터페이스를 구현하는 방식으로 스레드를 생성

public interface Runnable {
     void run();
}
 public class HelloRunnable implements Runnable {
     @Override
     public void run() {
         System.out.println(Thread.currentThread().getName() + ": run()");
     }
}

 

Thread 상속 vs Runnable 구현

스레드 사용할 때는 Thread 를 상속 받는 방법보다 Runnable 인터페이스를 구현하는 방식을 사용하자

 

  • 자바는 다중 상속이 안 된다 -> Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없음
  • 코드의 분리가 안 된다 -> 굳이 Thread 클래스를 상속받아서 디펜던시 및 추가적인 메모리를 할당 할 필요가 없음
  • 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리를 효율적으로 할 수 있다 -> Runnable 인스턴스를 여러 스레드에서 사용할 수 있음 (개별 스레드에서 run() 참조 값이 같음)
728x90

'Java > 멀티스레드' 카테고리의 다른 글

[JAVA] 스레드 제어와 생명주기  (0) 2024.09.18

+ Recent posts