쓰레드 프로그래밍은 동기화(synchronization)와 스케줄링(scheduling) 때문에 난이도가 어려움
멀티 쓰레딩을 잘 구현하기 위해서는 자원과 시간의 낭비가 발생하지 않도록 정교한 스케줄링 스킬이 필요
따라서 쓰레드의 상태 및 스케줄링 관련 메서드를 잘 알아둬야함
상태 | 설명 |
---|---|
NEW | 쓰레드가 생성되고 아직 start() 가 호출되지 않은 상태 |
RUNNABLE | 실행 중, 또는 실행 가능한 상태 |
BLOCKED | 동기화 블럭에 의해서 일시 정지된 상태 |
WAITNG, TIMED_WAITING | 쓰레드의 작업이 종료되진 않았지만 실행 가능하지 않은 일시 정지 상태. TIMED_WAITING은 일시 정지 시간이 지정된 경우 |
TERMINATED | 쓰레드의 작업이 종료된 상태 |
일정 시간 동안 쓰레드를 멈추게 함.
static void sleep(long ms)
static void sleep(long ms, int ns)
ms는 밀리세컨드(1000분의 1초), ns는 나노세컨드(10억 분의 1초)
try {
Thread.sleep(1, 50000); // 0.0015초 동안 정지
} catch (InterruptedException e) {}
지정된 시간이 다 되거나, interrupt()
가 호출되면 깨어나서 실행 대기 상태가 됨.
그런데 interrupt()
는 InterruptedException
를 발생시키므로 항상 try-catch
문으로 예외 처리를 해주어야 함.
class SleepExample {
public static void main(String[] args) throws Exception {
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
t1.start();
t2.start();
try {
t1.sleep(2000);
} catch (InterruptedException e) {}
System.out.println("main 쓰레드 종료");
}
}
class Thread1 extends Thread {
@Override
public void run() {
System.out.println("t1 쓰레드 종료");
}
}
class Thread2 extends Thread {
@Override
public void run() {
System.out.println("t2 쓰레드 종료");
}
}
실행 결과)
t1 쓰레드 종료
t2 쓰레드 종료
main 쓰레드 종료
t1
은 분명 2초간 정지시켰으니까 가장 마지막에 끝나야 하는 것 아닌가?
그런데도 가장 먼저 종료된 이유는, sleep()
이 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문
sleep()
이 호출된 곳은main
메서드를 실행 중인main
쓰레드임
그래서 t1.sleep(2000)
처럼 참조 변수로 호출하지 않고, Thread.sleep(2000)
처럼 스태틱 메서드 호출 방식을 사용해야함.
void interrupt()
: 진행 중인 쓰레드의 작업을 취소함.
boolean interrupted()
: 쓰레드에 대해 interrupt()
가 호출되었는지(interrupted 상태) 알려주고, interrupted 상태를 false로 변경
static boolean isInterrupted()
: 쓰레드에 대해 interrupt()
가 호출되었는지 알려줌
class InterruptExample {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
t1.start();
String input = JOptionPane.showInputDialog("Insert any value");
System.out.println("Your input is: " + input);
t1.interrupt();
System.out.println(t1.isInterrupted());
System.out.println("main thread terminated");
}
}
class Thread1 extends Thread {
@Override
public void run() {
int i = 10;
while (i != 0 && !interrupted()) {
System.out.println(i--);
for (long x = 0; x < 10000000000L; x++); // 시간 지연을 위한 빈 for문
}
System.out.println("t1 thread terminated");
}
}
실행 결과)
10
9
8
Your input is: hello
true
main thread terminated
t1 thread terminated
t1
쓰레드를 실행시키면 while
문에 의해 10부터 카운트 다운이 시작되는데, main
쓰레드에서 입력값을 받아와 출력하면 이어서 interrupt()
가 호출됨.
interrupt()
가 호출되면 while
문의 두번째 조건식이 false
가 되면서 반복을 빠져나오고, 쓰레드가 종료됨.
여기서 시간 지연을 위한 for
문 대신에
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
를 사용하면 아래의 결과가 나옴
10
9
8
7
Your input is: hello
true
main thread terminated
6
5
4
3
2
1
t1 thread terminated
카운트 다운이 멈추지 않은 이유는 sleep()
에서 InterruptedException
이 발생했기 때문.
예외가 발생되면 쓰레드의 interrupted 상태가 false
로 초기화 되기 때문에 while
문의 조건식이 true
로 유지된 것.
쓰레드가
sleep()
,wait()
,join()
에 의해 "일시정지 상태(WAITING)"에 있을 때,interrupt()
를 호출하면
sleep()
,wait()
,join()
에서InterruptedException
이 발생하면서 쓰레드는 "실행 대기 상태(RUNNABLE)"가 됨.(RUNNABLE일 때의 interrupted 상태는 당연히 false)
예외 발생 후에도 interrupted 상태를 true
로 유지하고 싶으면 catch
블럭 안에 interrupt()
를 한번 더 호출해주면 됨.
suspend()
: sleep()
처럼 쓰레드를 일시정지 시킴.
resume()
: suspend()
로 멈춰진 쓰레드를 다시 실행 대기 상태로 돌려놓음.
stop()
: 쓰레드를 종료시킴.
이 3가지 메서드는 쓰레드를 교착 상태(dead lock)로 만들기 쉽다는 문제 때문에 deprecated 이므로 사용하지 말자.
자신에게 주어진 실행 시간을 다른 쓰레드에게 양보하고 실행 대기 상태가 됨.
class YieldExample {
public static void main(String[] args) {
YieldingThread yt1 = new YieldingThread("A");
YieldingThread yt2 = new YieldingThread("B");
YieldingThread yt3 = new YieldingThread("C");
yt1.start();
yt2.start();
yt3.start();
try {
Thread.sleep(2000);
yt1.suspend();
Thread.sleep(2000);
yt2.suspend();
Thread.sleep(3000);
yt1.resume();
Thread.sleep(3000);
yt1.stop();
yt2.stop();
Thread.sleep(2000);
yt3.stop();
} catch (InterruptedException e) {}
}
}
class YieldingThread implements Runnable {
boolean suspended = false;
boolean stopped = false;
Thread t;
YieldingThread (String name) {
t = new Thread(this, name);
}
@Override
public void run() {
String name = t.getName();
while (!stopped) {
if (!suspended) {
System.out.println(name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(name + ": interrupted");
}
} else {
Thread.yield(); // (1)
}
}
System.out.println(name + ": stopped");
}
public void suspend() {
suspended = true;
t.interrupt();
System.out.println(t.getName() + ": interrupt() executed by suspend()");
}
public void stop() {
stopped = true;
t.interrupt(); // (2)
System.out.println(t.getName() + ": interrupt() executed by stop()");
}
public void resume() { suspended = false; }
public void start() { t.start(); }
}
실행 결과
A B C A C B A: interrupt() executed by suspend() A: interrupted C B C B B: interrupt() executed by suspend() B: interrupted C C C A C A C A C A: interrupted A: interrupt() executed by stop() B: interrupt() executed by stop() A: stopped B: stopped C C C: interrupt() executed by stop() C: interrupted C: stopped
실행 환경에 따라 결과는 다를 수 있음
이 코드는 interrupt()
와 yield()
를 적재적소에 배치해서 프로그램의 응답성을 높이고 바람직한 스케줄링을 구현함.
✏️ 주석 1번:
만약 else
문으로 yield()
호출을 안했더라면 suspended
가 true
일 때, 해당 쓰레드는 자신에게 주어진 실행 시간을 채우기 위해 남은 시간 동안 의미 없이 while
문을 돌아야함*. yield()
를 넣음으로써 같은 상황에서 즉시 자신의 실행 시간을 다른 쓰레드에게 양보해서 프로그램이 낭비 없이 돌아가게 함.
*이런 상태를 "바쁜 대기 상태(busy-waiting)"이라 부름.
✏️ 주석 2번:
만약 stop()
안에 interrupt()
를 호출하지 않았다면, stopped
가 true
가 되었어도 반복문의 조건식으로 되돌아가기까지 1초의 시간을 기다려야만 함. 그러나 interrupt()
를 넣음으로써 즉시 InterruptedException
을 발생시키고 이 불필요한 1초 정지 상태에서 바로 벗어날 수 있게 됨.
자신이 하던 작업은 잠시 멈추고 다른 쓰레드가 지정된 시간 동안 작업을 수행하게끔 함.
void join
()
void join
(long ms)
void join
(long ms, int ns)
첫번째처럼 시간을 지정하지 않는 경우, 다른 쓰레드가 작업을 모두 마칠 때까지 기다려야함.
try {
t1.join(); // 현재 실행 중인 쓰레드에서 작업을 중단하고 t1 쓰레드의 작업이 끝날 때까지 기다림
} catch (InterruptedException e) {}
보다시피 sleep()
처럼 interrupt()
에 의해서 대기 상태에서 즉시 벗어날 수 있으며, 이 때 예외가 발생하므로 try-catch
문으로 감싸줘야함.
sleep()
과는 다르게 static 메서드가 아니므로 주의
class JoinExample {
public static void main(String[] args) {
MyGC myGc = new MyGC();
myGc.setDaemon(true);
myGc.start();
int requiredMemory = 0;
for(int i = 0; i < 10; i++) {
requiredMemory = (int)(Math.random() * 10) * 40;
boolean isAvailable = myGc.availableMemory() > requiredMemory && myGc.availableMemory() > myGc.getMaxMemory() * 0.3;
if (!isAvailable) {
myGc.interrupt();
try {
myGc.join(100);
} catch (InterruptedException e) {}
}
myGc.usedMemory += requiredMemory;
System.out.println("Occupied: " + myGc.usedMemory);
}
}
}
class MyGC extends Thread {
final static int MAX_MEMORY = 1000;
int usedMemory = 0;
@Override
public void run() {
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Awaken by interrupt()");
}
collectGarbage();
System.out.println("Garbage collected. Occupied: " + usedMemory);
}
}
public void collectGarbage() {
usedMemory -= 300;
if (usedMemory < 0) usedMemory = 0;
}
public int getMaxMemory() { return MAX_MEMORY; }
public int availableMemory() { return MAX_MEMORY - usedMemory; }
}
실행 결과)
Occupied: 0
Occupied: 360
Occupied: 400
Occupied: 400
Occupied: 720
Awaken by interrupt()
Garbage collected. Occupied: 420
Occupied: 500
Occupied: 860
Awaken by interrupt()
Garbage collected. Occupied: 560
Occupied: 720
Awaken by interrupt()
Garbage collected. Occupied: 420
Occupied: 620
Occupied: 860
MyGC
라는 이름의 매우 단순화된 Garbage Collector를 구현
최대 메모리 1000을 가지고 있는 프로그램에 계속해서 랜덤한 규모의 메모리를 사용하게 만들고, 특정 조건이 만족할 때마다 자고 있던 myGc
쓰레드를 깨워 메모리를 청소해주는 코드
만약 join()
이 사용되지 않았다면, myGc
가 collectGarbage()
를 호출하여 자신의 역할을 다하기도 전에 main
쓰레드가 계속해서 메모리를 사용하게 됨.
join()
을 통해 main
쓰레드의 진행을 잠시 멈추고, myGc
쓰레드가 메모리 비우는 작업을 할 시간을 벌어준 뒤, 그 작업이 끝나면 다시 main
이 하려던 일을 이어감.