가장 배우고 싶었던 기능이에요 ㅎㅎ. 운영체제 강의를 수강할 때 정리한 쓰레드를 링크 걸며 설명을 스킵할게요. 여기서는 자바java
에서 쓰레드thread
를 어떻게 사용하는지만 다룰게요.
자바java
에선 2가지 방법으로 쓰레드를 구현할 수 있어요
Thread
클래스 상속class ThreadEx extends Thread { @Override public void run() { ... } }
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()
함수를 호출해야만 실행시킬 수 있어요.
Thread th1 = new ThreadEx();
th1.start();
물론 호출하자마자 바로 되는 것은 아니고, 운영체제의 스케쥴링 알고리즘에 의거해 본인 차례가 되어야만 실행해요.
그리고 쓰레드는 한번 호출하면 다시 호출할 수 없어요. 두 번 이상 호출을 시도할 경우엔 IllegalThreadStateException
예외를 던져요. 다시 실행시키기 위해선 한번 더 초기화를 실행시켜야 해요.
Thread t1 = new ThreadEx();
t1.start();
// t1.start(); // Exception!
t1 = new ThreadEx(); // 다시 초기화해줘야 수행할 수 있어요.
t1.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
도 사실 쓰레드에서 동작해요. 이를 메인 쓰레드라고 해요.
쓰레드의 보조역할을 하는 쓰레드에요. 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료되요. 이점을 제외하면 일반 쓰레드와 다르지 않아요.
대표적인 예로 가비지 컬랙터, 워드프로세서 자동 저장 기능이 있어요.
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()
이랑 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
는 정확하겐 진행 중인 쓰레드의 작업을 멈추라고 요청하는 기능이에요. 쓰레드를 통해 다운로드를 진행 중인데 너무 시간이 오래 걸리니까 중도에 포기할 때 사용하는 걸 예로 들 수 있어요.
단지 멈추라고 요청만 하는 것이라 쓰레드를 강제로 종료시키진 않아요. 그저 interrupted
변수를 true
로 바꿔줄 뿐이에요. isInterrupted
는 boolean
반환 타입이며, 쓰레드의 인터럽트 여부를 확인해요.
그리고 interrupted()
는 정적 메서드로 현재 활성화 중인 쓰레드의 인터럽트 여부를 반환하고, 이를 false
상태로 변경시켜줘요.
다른 쓰레드의 작업을 일정 시간동안 기다리는 메서드에요. 매개변수가 없는 경우, 즉 시간을 지정하지 않으면 join
한 쓰레드가 모두 작업을 마칠때까지 기다리게 되요. 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야할 필요가 있을 때 join()
을 사용해요.
void join();
void join(long millis);
void join(long millis, int nanos);
join
도 마찬가지로 interrupt
를 통해 대기 상태에서 벗어날 수 있으며, InterruptedException
을 날리기 때문에 try-cach
문에서 실행되어야 해요.
💡 sleep vs. join
대기한다는 점은 둘 다 동일해요. 하지만sleep
은 현재 쓰레드를 기준으로,join
은 특정 쓰레드를 기준으로 대기한다는 점에서 차이가 존재해요!
기본적인 사용법은 이정도만 다루고, 쓰레드의 꽃인 동기화 관련 기능은 다음에 다룰게요.