OS

Java 스레드 Deep Dive

데일리코딩 2024. 9. 2. 22:23

* 해당 포스팅은 김영한님의 자바 고급편 스레드 강의를 완강 후 해당 내용을 정리한 글입니다.

 

지난 포스팅은 컨텍스트 (문맥전환)에 학습하고 해당 내용을 정리하는 시간을 가졌는데요

오늘은 지난 포스팅을 이어 자바에서 제공하는 스레드 기본정보와 생명주기에 대해서 정리하겠습니다.

 

스레드 기본 정보

자바에는 Thread 클래스가 존재합니다, 해당 클래스를 통해 스레드를 생성하고 관리하는 기능을 제공하는데요

Thread가 제공하는 정보들을 한번 간단하게 정리하겠습니다.

package thread.control;
import thread.start.HelloRunnable;
import static util.MyLogger.log;


public class ThreadInfoMain {
public static void main(String[] args) {
    	// main 스레드 
    	Thread mainThread = Thread.currentThread();
    }
}

현재 main 함수에서 현재 스레드를 가져오면 메인 스레드를 가져오게 됩니다.

 

// 메인 스레드의 아이디를 가져옵니다.
log("mainThread.threadId() = " + mainThread.threadId());
실행 결과 : 09:55:58.713 [ main] mainThread.threadId() = 1
// 스레드의 이름을 가져옵니다.
log("mainThread.getName() = " + mainThread.getName());
실행결과 : 09:55:58.713 [ main] mainThread.getName() = main
// 스레드의 우선순위를 가져옵니다. 총 1 ~ 10 값이 있으며 기본값은 5입니다.
log("mainThread.getPriority() = " + mainThread.getPriority());
실행결과 : 09:55:58.716 [ main] mainThread.getPriority() = 5
// 현재 스레드의 그룹을 가져옵니다.
log("myThread.getThreadGroup() = " + mainThread.getThreadGroup());
실행결과 : 09:55:58.716 [ main] mainThread.getThreadGroup() = java.lang.ThreadGroup[name=main,maxpri=10]
// * 현재 스레드의 상태를 가져옵니다 해당 내용은 중요합니다.
log("mainThread.getState() = " + mainThread.getState());
실행결과 : 09:55:58.716 [ main] mainThread.getState() = RUNNABLE

 

이번에는 메인 스레드에서 새로운 MyThread 라는 스레드를 메인 스레드에서 생성한 뒤에 상태값을 가져와보도록 하겠습니다.

 

package thread.control;
import thread.start.HelloRunnable;
import static util.MyLogger.log;


public class ThreadInfoMain {
public static void main(String[] args) {
        // myThread 스레드
        Thread myThread = new Thread(new HelloRunnable(), "myThread");
        // 스레드 객체를 문자열로 변환하여 출력한다. 
        log("myThread = " + myThread);
        log("myThread.threadId() = " + myThread.threadId());
        // 스레드의 고유 식별자 반환하는 메서드, jvm내에서 각 스레드에 대해 유일한 값입니다.
        log("myThread.getName() = " + myThread.getName());
        // 스레드의 이름을 반환 하는 메서드, 생성자에서 "MyThread"라는 이름을 지정했기에 이 값이 반환됩니다.
        log("myThread.getPriority() = " + myThread.getPriority());
        // 스레드 우선순위 1 (가장 낮음)에서 10(가장 높음)까지 값 설정가능하며 setPriority() 메소드로 변경가능하다
        log("myThread.getThreadGroup() = " + myThread.getThreadGroup());
        // 스레드을 그룹화 하여 관리할 수 있다.
        log("myThread.getState() = " + myThread.getState());
        // 스레드의 상태를 나타내는데 열거형에 정의한 상수중 하나입니다. 주요 상태는 밑에서 정리하겠습니다.
    }
}

실행결과
//myThread 출력
09:55:58.717 [ main] myThread = Thread[#21,myThread,5,main]
09:55:58.717 [ main] myThread.threadId() = 21
09:55:58.717 [ main] myThread.getName() = myThread
09:55:58.717 [ main] myThread.getPriority() = 5
09:55:58.717 [ main] myThread.getThreadGroup() =
java.lang.ThreadGroup[name=main,maxpri=10]
09:55:58.717 [ main] myThread.getState() = NEW

스레드 상태

 

스레드의 상태 참고로 일시 중지 상태들은 없는 상태이지만 스레드가 기다리는 상태들을 묶어서 쉽게 설명하기 위해 사용한 용어입니다.

스레드의 상태로는

  • New (새로운 상태): 스레드가 생성되었으나 아직 시작되지 않은 상태.
  • Runnable (실행 가능 상태): 스레드가 실행 중이거나 실행될 준비가 된 상태.
  • 일시 중지 상태들 
    1. Blocked (차단 상태): 스레드가 동기화 락을 기다리는 상태.

    2. Waiting (대기 상태): 스레드가 무기한으로 다른 스레드의 작업을 기다리는 상태.
    3. Timed Waiting: (시간 제한 대기 상태): 스레드가 일정 시간 동안 다른 스레드의 작업을 기다리는 상태.
  • Terminated (종료 상태): 스레드의 실행이 완료된 상태

해당 상태에 대해 자세히 설명하자면

1. New (새로운 상태)
스레드가 생성되고 아직 시작되지 않은 상태입니다.

이 상태에서는 Thread 객체가 생성은 되었지만 start() 메서드가 호출되지 않은 상태입니다.

 

2. Runnable (실행 가능 상태)

스레드가 실행될 준비가 된 상태입니다. 이 상태에서 스레드는 실제로 cpu에서 실핼될 수 있습니다.

start() 메서드가 호출되면 스레드는 이 상태로 들어갑니다.

이 상태는 스레드가 실행될 준비가 되어 있음을 나타내며, 실제로 cpu에서 실핼될 수 있는 상태입니다.

그러나 Runnable 상태에 있는 스레드는 스케쥴러의 실행 대기열에 포함되어 있다가 차례로 cpu에서 실행됩니다.

참고로 운영체제 스케줄러의 실행 대기열에 있든, cpu에서 실제 실행되고 있든 모두 Runnable 상태입니다. 

 

3.Blocked (차단 상태)

스레드가 다른 스레드에 의해 동기화 락을 얻기 위해 기다리는 상태입니다.

 

4. Waiting (대기 상태)

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

객체의 기본함수인 wait(), join() 메서드가 호출될 때 이 상태가 됩니다.

스레드는 다른 스레드가 notify(), notifyAll() 메서드를 호출하거나, Join() 이 완료될 때 까지 기다립니다.

 

5. Timed Waiting (시간 제한 대기 상태)

스레드가 특정 시간 동안 다른 스레드의 작업이 완료되기를 기다리는 상태입니다.

sleep(long millis), wait(long timeout), join(long millis) 메서드가 호출될 때 이 상태가 됩니다.

주어진 시간이 경과하거나 다른 스레드가 해당 스레드를 깨우면 이 상태에서 벗어납니다.

 

6. Terminated (종료 상태)

스레드는 스택으로 해당 호출 스택에 들어온 모든 작업을 완료하면 종료 합니다. 

스레드가 정상적으로 종료되거나, 예외가 발생하여 종료된 경우 . 이 상태로 들어갑니다.

스레드는 한번 종료되면 다시 시작할 수 없습니다.

 

이처럼 스레드의 상태는 열거형으로 정의된 값들이 있는데 특정 상황에 맞게 해당 상태로 변경 됩니다. 


스레드의 생명 주기

전체 이미지로 나무가 아닌 숲을 봅시다!

위 이미지 처럼 스레드의 생명주기를 크게 한번 봅시다.

위 스레드 상태에서 설명한 내용처럼 순서대로 한번 정리 해보겠습니다.

스레드 진행 순서

1. new (객체가 생성된 상태) -> Runnable (스케줄러에 있거나, cpu에 의해 실행중인 상태 해당 상태를 정확하게 구분할 순 없음)

2. Runnable -> Blocked/Waiting/Timed Waiting -> Runnable: 스레드가 락을 얻지 못하고나, sleep 상태이면 일시 중지 상태였다가, 스레드가 락을 얻거나, 기다림이 완료되면 다시 Runnable 상태로 변경됩니다.

3. Runnable -> Terminated: 스레드의 run() 메서드가 완료 되면 스레드는 Terminated 상태가 되고 다시 실행할 순 없습니다.

 

* 스택에 대해서.

스택의 이해를 돕기 위한 이미지

우리가 처음에 java Thread 포스팅에 봤던 스택 이미지가 혹시 기억이 날까요?

스택이란 자료구조는 마지막 들어온 작업이 먼저 튀어나오는 자료구조인데요 

push() 함수를 통해 작업을 넣고 pop() 함수를 통해 해당 요소들이 팝! 하고 튀어 나옵니다.

이렇게 작업들을 push (넣고) 하나씩 pop() 하면서 실행하는 거지요

이때 모든 작업 내용이 완료되면 종료가 되는것입니다.


스레드의 생명 주기 - 코드

이번에는 코드를 통해 직접 스레드의 상태변경되는것을 확인 해보도록 하겠습니다.

package thread.control;
import static util.MyLogger.log;
public class ThreadStateMain {
	public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyRunnable(), "myThread");
        log("myThread.state1 = " + thread.getState()); // NEW
        log("myThread.start()");
        thread.start();
        Thread.sleep(1000);
        log("myThread.state3 = " + thread.getState()); // TIMED_WAITING
        Thread.sleep(4000);
        log("myThread.state5 = " + thread.getState()); // TERMINATED
        log("end");
    }
    
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            try {
                log("start");
                log("myThread.state2 = " +
                Thread.currentThread().getState()); // RUNNABLE
                log("sleep() start");
                Thread.sleep(3000);
                log("sleep() end");
                log("myThread.state4 = " +
                Thread.currentThread().getState()); // RUNNABLE
                log("end");
            } catch (InterruptedException e) {
            	throw new RuntimeException(e);
            }
		}
	}
}
```

 

Thread.currentThread()를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있습니다.

Thread.sleep(): 해당 코드를 호출한 스레드는 Timed Waiting 상태가 되면서 특정 시간 만큼 대기합니다.

시간은 밀리초(ms)단위입니다. 1밀리초 = 1/1000 초, 1000밀리초 = 1초입니다.

Thread.sleep()은 InterruptedException이라는 체크 예외를 던지는데, 따라서 체크 예외를 잡아서 처리하거나 던져야 합니다.

run() 메서드 안에서는 체크 예외를 반드시 잡아야합니다. 

 

실행결과
11:40:31.503 [ main] myThread.state1 = NEW
11:40:31.505 [ main] myThread.start()
11:40:31.505 [ myThread] start
11:40:31.505 [ myThread] myThread.state2 = RUNNABLE
11:40:31.505 [ myThread] sleep() start
11:40:32.507 [ main] myThread.state3 = TIMED_WAITING
11:40:34.510 [ myThread] sleep() end
11:40:34.512 [ myThread] myThread.state4 = RUNNABLE
11:40:34.512 [ myThread] end
11:40:36.511 [ main] myThread.state5 = TERMINATED
11:40:36.512 [ main] end

 

시간의 흐름대로 스레드 상태를 표현한 이미지 입니다.

State1 = New

main 스레드를 통해 myThread 객체를 생성합니다. 스레드 객체만 생성하고 아직 start()를 호출하지 않았기 때문에 NEW 상태입니다.

 

State2 = Runnable

myThread.start()를 호출해서 myThread를 실행 상태로 만듭니다. 따라서 Runnable 상태로 변경되고

참고로 실행 상태가 너무 빨리 지나가기 때문에 main 스레드에서 myThread 상태를 확인하기는 어렵습니다. 

대신에 자기 자신의 myThread에서 실행 중인 상태를 확인했습니다.

 

State3 = Timed_Wating

Thread.sleep(3000): 해당 코드를 호출한 스레드는 3000Ms (3초간) 대기합니다. myThread가 해당 코드를 호출했으므로 3초간 대기하면서 Timed_Waiting 상태로 변경됩니다.

 

State4 = Runnable

myThrea는 3초의 대기시간 이후 Time_Wating 상태에서 빠져나와 다시 실행될 수 있는 Runnable 상태로 변경됩니다.

 

State5 = Terminated

myThread가 run() 메서드를 실행 종료하고 나면 Terminated 상태로 변경됩니다.

myThread 입장에서 run()이 스택에 남은 마지막 메서드인데 run() 까지 실행되면 스택이 완전히 비워지면서 종료됩니다.

 

메인 스레드까지 포함된 모든 실행 순서도

 

이렇게 오늘은 스레드에 대해서 기본적인 정보와 상태를 확인하고 해당 상태에 따른 생명주기를 코드를 통해 확인하는 시간을 가졌습니다.

저는 스레드를 공부하면서 스레드를 정말 잘 알고 활용을 할 수 있다면 든든한 직원을 고용한 기분이 드네요

마치 어떤 레스토랑 가게에 비유하자면

메인 스레드는 사장이고 메인 스레드를 통해 생성되고 실행 되는 스레드들은 고용한 직원들이죠

메인 스레드는 그저 고용하고 일을 시킬뿐 (스레드 객체를 생성하고 start()메서드를 호출할 뿐)이죠

그 시간에 메인 스레드는 다른 작업을 할 수 있고 병렬적으로 많은 일을 짧은 시간에 해낼 수 있습니다.

 

앞으로 저와 함께 스레드에 더 공부하면서 훌륭한 엔지니어로 거듭나시죠!! 화이팅!