[Java] 쓰레드(Thread) - 기본 기능

최지수·2022년 5월 17일
0

Java

목록 보기
27/27
post-thumbnail

가장 배우고 싶었던 기능이에요 ㅎㅎ. 운영체제 강의를 수강할 때 정리한 쓰레드를 링크 걸며 설명을 스킵할게요. 여기서는 자바java에서 쓰레드thread를 어떻게 사용하는지만 다룰게요.

쓰레드 구현과 실행

자바java에선 2가지 방법으로 쓰레드를 구현할 수 있어요

  1. Thread 클래스 상속
class ThreadEx extends Thread {
	@Override
    public void run() {
    ...
    }
}
  1. Runnable 인터페이스 상속
class ThreadEx implements Runnable {
	@Override
    public void run() {
    ...
    }
}

Thread 클래스를 상속받으면, 자손 클래스에서 조상인 Thread 클래스의 메서드를 직접 호출이 가능하나, Runnable으로 구현하면 Thread 클래스의 static 메서드, curruentThread()를 호출해서 쓰레드에 대한 참조를 얻어와야만 호출이 가능해요.

static Thread currentThread()	// 현재 실행중인 쓰레드 참조 반환
String getName()				// 쓰레드 이름 반환

그래서 Thread로 구현한 클래스는 getName()을 통해 이름을 구할 수 있지만, Runnable로 구현한 클래스는 Thread.currentThread().getName()을 통해 쓰레드로 실행되고 있을 경우에만 불러올 수 있어요.

start()

쓰레드는 start() 함수를 호출해야만 실행시킬 수 있어요.

Thread th1 = new ThreadEx();
th1.start();

물론 호출하자마자 바로 되는 것은 아니고, 운영체제의 스케쥴링 알고리즘에 의거해 본인 차례가 되어야만 실행해요.

그리고 쓰레드는 한번 호출하면 다시 호출할 수 없어요. 두 번 이상 호출을 시도할 경우엔 IllegalThreadStateException 예외를 던져요. 다시 실행시키기 위해선 한번 더 초기화를 실행시켜야 해요.

Thread t1 = new ThreadEx();
t1.start();
// t1.start();			// Exception!
t1 = new ThreadEx();	// 다시 초기화해줘야 수행할 수 있어요.
t1.start();

run() vs. start()

run()은 단순히 함수 호출이고, start()는 쓰레드를 실행시키는 차이가 있어요.

콜 스택Call stack 구조에서 살펴보면 run()은 단순히 메인 프로세스 상에서 차곡히 쌓이고 수행이 완료되면 스택에서 사라지지만, start()의 경우 start()가 호출된 이후에 독립된 작업을 위해 새로운 콜 스택이 생성되어 작업을 수행하고, 종료되면 생성된 콜 스택을 소멸시키는 방식으로 진행되요.

아래 코드에서 t1.start()t1.run()을 번갈아 수행해보시면 이해가 될 거에요.

public class Example2 {
    public static void execute() {
        Example2_1 r = new Example2_1();
        Thread t1 = new Thread(r);
        // 둘 중 하나
        t1.start();
        t1.run();
    }   
}

class Example2_1 implements Runnable{
    @Override
    public void run() {
        throwException();
    }

    private void throwException() {
        try{
            throw new Exception();
        } catch(Exception e){
            e.printStackTrace();
        }
    }
}


start()를 돌린 결과에요. 스택에 main이 없어요. 이는 main과 쓰레드는 따로 진행되고 있음을 알 수 있으며 main은 이미 종료되었음을 알 수 있어요.


반면 run()으로 돌렸을 땐 main이 포함되어 있죠. 쉽게 말하면 이는 쓰레드 방식으로 동작하지 않고, 단순히 메인 콜 스택 상에서 동작함을 알 수 있어요.

쓰레드 우선순위

쓰레드를 수행하는데 우선순위를 정할 수 있어요. 물론 높다고 해서 무조건 우선순위가 제일 높은 쓰레드가 다 끝날 때까지 다른 쓰레드가 CPU를 사용 못하는 것은 아니지만, 빈도의 차이는 있을 수 있어요.

우선순위 범위는 1~10이고, 숫자가 높을 수록 우선순위가 높은 거에요.

void setPriority(int newPriority)
int getPriority()

쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속 받아요. 그래서 main 메서드를 수행하는 쓰레드는 우선순위가 5이므로, main 메서드에서 시작한 쓰레드의 우선순위 값은 5에요.

쓰레드의 우선순위 값은 start() 호출 전에 바꿔줘야 적용되요.

💡main도 쓰레드에요
자바의 엔트리 지점인 main도 사실 쓰레드에서 동작해요. 이를 메인 쓰레드라고 해요.

데몬 쓰레드(Daemon Thread)

쓰레드의 보조역할을 하는 쓰레드에요. 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료되요. 이점을 제외하면 일반 쓰레드와 다르지 않아요.

대표적인 예로 가비지 컬랙터, 워드프로세서 자동 저장 기능이 있어요.

boolean isDaemon()			// 데몬 여부
void setDaemon(boolean on)	// 데몬 쓰레드 또는 사용자 쓰레드로 변경해요

public class ThreadEx10 implements Runnable {
    static boolean autoSave = false;

    public static void execute() {
        Thread t = new Thread(new ThreadEx10());

        t.setDaemon(true);
        t.start();

        for (int i = 1; i <= 10; ++i) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println(i);
            if (i == 5) {
                autoSave = true;
            }
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
            }

            if (autoSave) {
                autoSave();
            }
        }
    }

    public void autoSave() {
        System.out.println("파일이 자동 저장되었어요.");
    }
}

run() 코드를 보면 쓰레드는 while(true)로 인해 무한 반복되는 것을 보실 수 있는데, 여기서 쓰레드를 데몬daemon으로 만들었기 때문에 프로세스가 종료되면서 쓰레드도 같이 종료되는 것을 확인하실 수 있어요.

sleep()

해당 메서드가 선언된 쓰레드를 일정 시간동안 멈추게 할 수 있어요. 그리고 정적 메서드이므로 특정 스레드를 대상으로 멈추게 하는 용도로 사용할 수 없어요. 쉽게 말하면 {쓰레드 변수}.sleep()이랑 Thread.sleep()은 동일하게 작동해요.

Thread.sleep(long millis);				// 밀리, 1초 = 1000
Thread.sleep(long millis, int nanos); 	// 밀리, 나노(0 ~ 999999)
										// 0.0015초 = 1, 500000

Sleep으로 인해 일시정지 상태된 쓰레드는 시간이 다 되거나 interrupt() 메서드가 호출되면 다시 재개해요. interrupt()InterruptedException 예외가 던져지니까 반드시 try-catch 문에서 작동되어야 해요(물론 메서드 자체로 throws를 써도 되긴 하죠 ㅎㅎ).

interrupt(), isInterrupted(), interrupted()

잠들어 있는 쓰레드를 다시 재개시키는 용도로 쓰였지만, interrupt는 정확하겐 진행 중인 쓰레드의 작업을 멈추라고 요청하는 기능이에요. 쓰레드를 통해 다운로드를 진행 중인데 너무 시간이 오래 걸리니까 중도에 포기할 때 사용하는 걸 예로 들 수 있어요.

단지 멈추라고 요청만 하는 것이라 쓰레드를 강제로 종료시키진 않아요. 그저 interrupted 변수를 true로 바꿔줄 뿐이에요. isInterruptedboolean 반환 타입이며, 쓰레드의 인터럽트 여부를 확인해요.

그리고 interrupted()는 정적 메서드로 현재 활성화 중인 쓰레드의 인터럽트 여부를 반환하고, 이를 false 상태로 변경시켜줘요.

join()

다른 쓰레드의 작업을 일정 시간동안 기다리는 메서드에요. 매개변수가 없는 경우, 즉 시간을 지정하지 않으면 join한 쓰레드가 모두 작업을 마칠때까지 기다리게 되요. 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야할 필요가 있을 때 join()을 사용해요.

void join();
void join(long millis);
void join(long millis, int nanos);

join도 마찬가지로 interrupt를 통해 대기 상태에서 벗어날 수 있으며, InterruptedException을 날리기 때문에 try-cach 문에서 실행되어야 해요.

💡 sleep vs. join
대기한다는 점은 둘 다 동일해요. 하지만 sleep은 현재 쓰레드를 기준으로, join은 특정 쓰레드를 기준으로 대기한다는 점에서 차이가 존재해요!

기본적인 사용법은 이정도만 다루고, 쓰레드의 꽃인 동기화 관련 기능은 다음에 다룰게요.

profile
#행복 #도전 #지속성

0개의 댓글