가장 핵심인 쓰레드 제어이다. 멀티쓰레드 프로그래밍이 어려운 이유는 쓰레드 간의 스케줄링을 다룰 때 매우 세심하고 조심스럽게 다뤄야 하기 때문이다.
0. Thread Life Cycle
가장 먼저 쓰레드의 라이프사이클을 살펴보자. 쓰레드의 라이프사이클은 OS 수업에서 배운 프로세스의 라이프사이클과 굉장히 흡사하다.
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
public State getState() {
return sun.misc.VM.toThreadState(threadStatus);
}
상태 | 설명 |
NEW | 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태 |
RUNNABLE | 실행 중 또는 실행 가능한 상태 |
BLOCKED | 동기화 블럭에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태) |
WAITING, TIMED_WAITING | 쓰레드의 작업이 종료되지는 않았지만, 실행가능하지 않은 일시정지 상태 |
TERMINATED | 쓰레드의 작업이 종료된 상태 |
- 가장 먼저 쓰레드를 생성하면 NEW 상태가 된다.
- 그리고 start()를 호출하면 RUNNABLE 상태가 된다.
- 그리고 바로 실행되는 것이 아니라 Queue에 들어가서 자신의 차례가 될 때 까지 기다린다
- 그러다가 자신의 차례가 되면 실행 상태가 된다.
- 주어진 시간이 다 되거나 yield()를 만나면 다시 대기상태가 되고 다음 차례의 쓰레드가 자신의 차례가 된다.
- 또한, 실행 중 suspend(), wait(), sleep(), join()에 의해서도 일시정지 상태가 될 수 있다
- 자신의 일시정지 기간이 끝나거나, notify(), resume() 등이 호출되면 다시 대기열에 저장돼 차례를 기다린다
- 실행을 모두 마치거나 stop()을 호출하면 쓰레드는 소멸한다.
1. sleep()
일정 시간동안 쓰레드를 멈추게 한다. 또한, TimeUnit을 사용하면 조금 더 명시적으로 쓰레드를 sleep할 수 있다.
1) 주의할점 : InterruptedException
sleep에 의해 일시정지된 쓰레드가 인터럽트를 받으면 InterruptedException이 걸린다
class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("interrupt service routine");
System.out.println("인터럽트 걸릴 때, 대체 작업 수행");
}
});
t.start();
Thread.sleep(5000);
t.interrupt();
}
}
0
1
2
3
4
interrupt service routine
인터럽트 걸릴 때, 대체 작업 수행
Process finished with exit code 0
2) 주의할 점 2 : sleep은 인스턴스 메소드가 아니라 클래스 메소드이다.
class Main {
public static void main(String[] args) throws InterruptedException {
Thread aThread = new ThreadA();
Thread bThread = new ThreadB();
aThread.start();
bThread.start();
aThread.sleep(2000);
for (int i = 0; i < 100; i++) {
System.out.print("M");
}
System.out.print("<MAIN 종료>");
}
}
class ThreadA extends Thread {
@Override public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("A");
}
System.out.print("<A 종료>");
}
}
class ThreadB extends Thread {
@Override public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("B");
}
System.out.print("<B 종료>");
}
}
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBB<A 종료><B 종료>MMMMMMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
MMMMMMMMMMMMMMM<MAIN 종료>
이렇게 짜면 Thread A가 B보다 2초 정도 늦게 끝날 것 같지만 거의 동시에 끝나고 MAIN이 2초 끝난다. Thread.sleep은 클래스 메소드이기 때문에 인스턴스에서 호출하면 안된다. 이 경우, sleep이 실행된 MAIN쪽의 쓰레드가 2초 멈추게 된다.
2. interrupt() / interrupted()
- interrupt() : 현재 쓰레드에서 작업중인 업무를 종료하고, 즉시 다른 작업을 시킬 수 있음 (interrupt = true)
- isInterrupted() : 현재 쓰레드의 인터럽트 상태 반환
- interrupted() : 인터럽트에 걸렸었는지 출력하고, 다시 interrupt = false로 만듬
쓰레드가 interrupted에 걸리게 되면, sleep(), join(), wait() 등의 메소드에 의해 Waiting하고 있다고 하더라도, 다시 살아나서 Runnable 상태가 된다. 즉, 기다리고 있는 쓰레드를 바로 깨워서 쓸 수 있다.
class Main {
public static void main(String[] args) throws InterruptedException {
InterruptServiceRoutine routine = new InterruptServiceRoutine();
routine.start();
Thread.sleep(5000);
routine.setMsg("mouse");
routine.interrupt();
Thread.sleep(5000);
routine.setMsg("hdd");
routine.interrupt();
Thread.sleep(5000);
routine.setMsg("exit");
routine.interrupt();
}
}
class InterruptServiceRoutine implements Runnable {
private String msg = "";
private int process = 0;
private Thread thread = new Thread(this);
public void start() {
thread.start();
}
public void interrupt() {
thread.interrupt();
}
public void setMsg(String msg) {
this.msg = msg.toLowerCase();
}
@Override public void run() {
try {
for (; process < 100; process++) {
System.out.println("작업중 : " + process + "%");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
switch (msg) {
case "mouse":
System.out.println("마우스 움직임");
break;
case "hdd":
System.out.println("HDD접근");
break;
case "exit":
System.out.println("프로그램 종료");
System.exit(0);
default:
System.out.println("잘못된 인터럽트");
break;
}
thread = new Thread(this);
thread.start();
}
}
}
작업중 : 0%
작업중 : 1%
작업중 : 2%
작업중 : 3%
작업중 : 4%
마우스 움직임
작업중 : 4%
작업중 : 5%
작업중 : 6%
작업중 : 7%
작업중 : 8%
HDD접근
작업중 : 8%
작업중 : 9%
작업중 : 10%
작업중 : 11%
작업중 : 12%
프로그램 종료
Process finished with exit code 0
만약 sleep과 같이 Interrupt Exception을 지원하지 않는 작업(for long x : 0 -> 50...0L)을 진행하다가 인터럽트를 걸고 싶다면 아래처럼 if 분기로 검사해야줘야한다.
class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
counter.start();
Thread.sleep(3000);
counter.interrupt();
Thread.sleep(3000);
counter.interrupt();
}
}
class Counter extends Thread {
public void run() {
int i = 10;
while (i != 0) {
System.out.println(i--);
for (long x = 0; x < 5000000000L; x++) ;
if(Thread.interrupted()){
System.out.println("인터럽트 발생");
}
}
}
}
10
9
8
7
6
인터럽트 발생
5
4
3
2
1
Process finished with exit code 0
interrupted()를 사용하면 interrupt 상태가 false로 바뀌어서 "인터럽트 발생" 부분이 다시 실행되지 않지만 만약 interrupted가 아닌, isInterrupted()를 사용한다면 아래와 같이 출력된다.
10
9
8
7
6
5
4
인터럽트 발생
3
인터럽트 발생
2
인터럽트 발생
1
인터럽트 발생
Process finished with exit code 0
while문의 조건에 인터럽트 여부를 걸어버리는 방법도 꽤 유용하다
class Main {
public static void main(String[] args) throws InterruptedException {
Locker locker = new Locker();
locker.start();
Thread.sleep(5000);
locker.interrupt();
}
}
class Locker extends Thread {
public void run() {
while (!isInterrupted()) {
System.out.println("lock");
for (long x = 0; x < 5000000000L; x++) ;
}
System.out.println("unlock");
}
}
lock
lock
lock
lock
lock
unlock
Process finished with exit code 0
'자바' 카테고리의 다른 글
자바 쓰레드 (4) - 데몬 쓰레드 (Daemon Thread) (0) | 2020.04.19 |
---|---|
자바 쓰레드 (3) - 쓰레드 그룹 (Thread Group) (0) | 2020.04.19 |
자바 쓰레드 (2) - start() vs run() (0) | 2020.04.19 |
자바 쓰레드 (1) - 쓰레드란 무엇인가? (0) | 2020.04.19 |