[Java] 쓰레드 Thread 2 - 쓰레드 상태, 상태 제어

Bam·2024년 3월 17일
0

Java

목록 보기
73/98
post-thumbnail

지난 포스트에서 쓰레드에 대한 기본적인 이야기를 했습니다. 이번 포스트에서는 좀 더 나아가서 쓰레드에 대해 조금 더 자세히 알아보도록 하겠습니다.


쓰레드의 이름

자바의 모든 쓰레드는 식별을 위해 이름을 가지고 있습니다. 가장 중심이 되는 쓰레드는 main 쓰레드라고 지난번에 언급했었죠. 이때 메인 쓰레드가 아닌 작업 쓰레드는 자동적으로 Thread-n이라는 이름을 갖는데 다른 이름을 프로그래머가 지어줄수도 있습니다.

setName() 메소드를 이용해서 쓰레드에 이름을 붙여줄 수 있습니다.

쓰레드객체명.setName("이름");

쓰레드의 이름은 getName()메소드를 이용해서 얻을 수 있습니다.

Thread thread = Thread.currentThread();	//현재 쓰레드 정보 취득
thread.getName();	//쓰레드 이름 취득

다음은 메인 쓰레드, 기본 작업 쓰레드, 이름을 붙여준 작업 쓰레드의 이름을 출력하는 예제코드입니다.

public class Main {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();	//메인 쓰레드 정보 취득

        System.out.println(mainThread.getName() + "실행");	//메인 쓰레드 이름 출력

        Thread thread1 = new Thread() {	//작업 쓰레드1 생성
            @Override
            public void run() {
                System.out.println(getName() + " 실행");
            }
        };
        thread1.start();

        Thread thread2 = new Thread() {	//작업 쓰레드2 생성
            @Override
            public void run() {
                System.out.println(getName() + " 실행");
            }
        };
        thread2.setName("이름 지어준 쓰레드");	//작업 쓰레드2에 이름 붙여주기
        thread2.start();
    }
}


쓰레드 상태

코드 상으로는 쓰레드를 생성하고 start() 메소드를 통해 실행하면 쓰레드가 바로 실행될 것 같지만, 실제로는 그렇지 않습니다.

쓰레드를 생성자를 통해서 생성하면 생성 (NEW) 상태가 됩니다. 이때는 아직 start()가 호출되지 않은 상태입니다.

start() 메소드가 호출되면 쓰레드는 실행 대기(RUNNABLE) 상태가 됩니다. 이 상태의 쓰레드는 대기하고 있다가 CPU 스케쥴링에 따라서 자기 차례에 CPU를 점유하고 run() 메소드가 실행되어 실행이 됩니다. 실행이 되는 상태를 실행(RUNNING)이라고 합니다.

CPU 스케쥴링(CPU Scheduling)
CPU 스케쥴링은 운영체제가 CPU를 이용하고자하는 프로세스의 우선순위를 할당하는 것을 말합니다.

실행 상태에서 다시 실행 대기 상태로 돌아갈 수 있습니다. 그리고 다른 쓰레드와 CPU를 점유했다 대기했다를 반복하면서 run() 메소드 내부의 모든 코드를 실행하게 됩니다.

또한 실행 상태에서 일시 정지가 될 수도 있습니다. 일시 정지는 동기화블록에 의해 실행이 정지된 BLOCKED, 일반 일시 정지 상태인 WAITING, 일정 시간동안 일시 정지하는 TIME_WAITING 상태가 있습니다.

모든 내부 코드를 실행했다면 run() 메소드의 호출을 종료하고 쓰레드의 실행이 멈추게 되는데요. 이 상태를 종료(TERMINATED)라고 부릅니다.

쓰레드 상태설명
NEW쓰레드가 생성되고 start()가 호출되기 이전의 상태
RUNNABLE, RUNNING실행 대기 상태와 실행 중
BLOCKED동기화 블록에 의한 일시 정지 상태
WAITING일시 정지 상태
TIME_WAITING시간이 지정된 일시 정지 상태
TERMINATED작업 종료

일시 정지와 실행 대기

쓰레드는 코드를 실행하다가 특정 메소드가 호출되면 일시 정지가 됩니다. 일시 정지를 시키는 메소드들은 다음 세 가지가 있습니다.

메소드설명
sleep(ms)주어진 밀리초(1/1000) 동안 쓰레드를 일시 정지합니다.
주어진 시간이 지나면 자동으로 실행 대기 상태로 변합니다.
join()join()을 호출한 쓰레드는 일시 정지합니다.
실행 대기 상태가 되기 위해서는 join()을 가진 쓰레드가 종료되어야합니다.
wait()동기화 블록 내에서 쓰레드를 일시 정지합니다.

일시 정지된 쓰레드는 다시 실행 대기 상태로 보내거나 종료를 함으로써 일지 정지 상태에서 벗어날 수 있습니다. 일시 정지된 쓰레드를 조작하는 메소드들은 다음과 같습니다.

메소드설명
interrupt()InterruptedException을 발생시켜 실행 대기 상태 또는 종료 상태로 만든다.
notify(), notifyAll()wait()으로 일시 정지된 쓰레드를 실행 대기 상태로 만든다.

실행 상태에서 실행 대기 상태로 만드는 메소드도 있습니다.

메소드설명
yield()실행 상태에서 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다.

이 중 wait(), notify(), notifyAll() 메소드는 쓰레드 동기화와 관련있는 내용이기 때문에 이 세 메소드는 해당 부분에서 다루도록 하겠습니다.

일정 시간동안 쓰레드 일시 정지 sleep()

sleep()은 지정된 밀리초(1/1000)만큼 쓰레드를 일시 정지하는 메소드입니다. 다음 코드는 5초 마다 메세지를 출력하는 쓰레드입니다.

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("메세지 출력");

                    try {
                        Thread.sleep(5000);	//5000밀리초(= 5초) 동안 일시 정지
                    }
                    catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        thread.start();
    }
}

위 코드를 실행해보면 5번의 출력이 5초에 한 번 씩 수행되는 것을 볼 수 있습니다.

다른 쓰레드의 종료 대기 join()

join()은 호출한 쓰레드가 join()을 가진 쓰레드가 실행을 마칠때까지 기다립니다. 예를들어 A쓰레드에서 B의 join()을 호출하면, B가 실행종료될 때까지 A는 일시 정지가 됩니다.

다음 코드는 메인에서 메세지 한 줄, 작업 쓰레드에서 메세지를 다섯 줄 출력하는 코드입니다. 대부분의 상황에서 main 쓰레드는 작업 쓰레드의 실행 종료를 기다리지 않습니다.

public class Main {
    public static void main(String[] args) {

        Thread thread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("작업 쓰레드에서 메세지 출력");
                }
            }
        };

        thread.start();
        System.out.println("메인 쓰레드에서 메세지 출력");
    }
}

이 코드를 join()을 이용해서 메인 쓰레드가 작업 쓰레드의 종료를 기다린 후 실행되도록 만들어보겠습니다.

public class Main {
    public static void main(String[] args) {

        Thread thread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("작업 쓰레드에서 메세지 출력");
                }
            }
        };

        thread.start();

        try {
            thread.join();	//main은 thread가 전부 실행될 때까지 일시 정지
        }
        catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("메인 쓰레드에서 메세지 출력");
    }
}

실행 양도 yield()

yield()는 다른 쓰레드에게 실행을 양보하고 자신은 실행 대기 상태가 되는 메소드입니다. 다음 코드는 CPU 스케쥴링에 따라서 정해진 순서대로 작업을 합니다.

생성자로 전달한 기호를 계속해서 찍어내는 작업 쓰레드 코드입니다.

public class T extends Thread {
    public boolean flag = true;
    public String sign;
    public T(String sign) {
        this.sign = sign;
    }

    @Override
    public void run() {
        while(true) {
            if (flag) {
                System.out.println(sign);
            }
            else {
                Thread.yield();
            }
        }
    }
}

동작을 제어하는 메소드입니다. 두 쓰레드를 실행하면 *, ㅁ가 번갈아 가면서 찍히다가 2초 뒤 t1.flag가 false가 되면 t1은 t2에게 실행을 양도합니다. 따라서 만 계속해서 찍히게 되죠. 그러다가 5초 후에는 다시 번갈아가면서 출력되게 됩니다.

public class Main {
    public static void main(String[] args) {
        T t1 = new T("*");
        T t2 = new T("ㅁ");

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

        try {
            Thread.sleep(2000);
        }
        catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t1.flag = false;	//실행 후 2초 후에 t1.flag를 false로

        try {
            Thread.sleep(5000);
        }
        catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t1.flag = true;	//다시 5초 후에 t1.flag를 true로
    }
}

쓰레드 종료 interrupt()

일반적으로는 run()의 코드가 모두 실행되면 쓰레드가 종료되지만 경우에 따라서는 코드를 통해 강제 종료할 필요가 있습니다. 메신저에서 사진을 보내다가 취소하는 경우가 그런 예시이죠.

기존엔 stop()이라는 메소드가 있었지만, 이 메소드는 종료를 해도 자원이 온전하게 반환되지 않는다는 문제로 인해서 Deprecated되었습니다. 사용하지 마세요.

이런 경우 쓰레드를 안전하게 종료하기 위해서 interrupt() 메소드를 사용합니다. interrupt() 메소드는 호출이 되면 일시 정지에 들어간 쓰레드에 InterruptException 예외를 발생시켜서 try ~ catchcatch 블록으로 이동하게 만들어줍니다. 이 과정에서 쓰레드가 점유하던 자원들을 반환하고 정상 종료되는 효과를 이끌어내게 됩니다.

다음 코드는 interrupt()를 사용한 예제 코드인데요. 무한 루프를 돌면서 계속해서 콘솔에 메세지를 출력하다가 interrupt()를 만나면 쓰레드를 종료시키게 되는 코드입니다.

이때, 일시 정지 상태를 취득하고 예외 처리를 위해 무한 루프 내부에서 sleep를 이용했습니다.

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    while (true) {
                        System.out.println("쓰레드 실행중");
                        Thread.sleep(1);	//일시 정지 상태를 주기 위한 sleep
                    }
                } catch (InterruptedException e) {
                    System.out.println("쓰레드 실행 종료");
                }
            }
        };
        thread.start();
        try {
            Thread.sleep(5000);
        }
        catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        thread.interrupt();	//5초후 thread.inturrpt() 실행
    }
}

무한 실행을 하다가 5초 정도 지나면 실행이 종료가 됩니다.

또는 위처럼 조건문을 사용할 경우 boolean flag같은 것을 이용해서 조건식으로 루프를 빠져나가 종료되도록 만들수도 있습니다.

0개의 댓글