쓰레드의 실행제어
쓰레드 프로그래밍이 어렵다고 하는 이유는 동기화와 스케줄링 때문이다. 앞서 우선순위를 통해 쓰레드간의 스케줄링을 하는 방법을 제시하였지만, 사실 이것만으로는 부족하다.
효율적인 멀티쓰레드 프로그램을 만들기 위해서는 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.
1. 쓰레드의 스케줄링과 관련된 메서드
2. 쓰레드의 상태
3. 쓰레드의 생성부터 소멸까지의 모든 과정
1 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐(queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.
2 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.
3 주어진 실행시간이 다되거나 yeild()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.
4 실행 중에 suspecd(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다. I/O block은 입출력작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데, 이런 경 우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기상태가 된다.
5 지정된 일시정지시간이 다되거나(time-out), notify(), resume(), interrupt()가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.
6 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.
cf.) 1부터 6까지 번호를 붙였으나 번호의 순서대로 쓰레드가 수행되는 것은 아니다.
class ThreadEx13 {
static long startTime = 0;
public static void main(String args[]) {
A th1 = new A();
B th2 = new B();
th1.start();
th2.start();
startTime = System.currentTimeMillis();
try {
th1.join(); // th1의 작업이 끝날 때까지 기다린다.
th2.join(); // th2의 작업이 끝날 때까지 기다린다.
} catch(InterruptedException e) {}
System.out.print("소요시간:" + (System.currentTimeMillis() - ThreadEx13.startTime));
} // main
}
class A extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print("-");
}
} // run()
}
class A extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print("|");
}
} // run()
}
실행결과)
------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||소요시간:13
join()을 사용하지 않으면 main쓰레드는 바로 종료되지만, join()을 사용해서 쓰레드 th1과 th2의 작업을 마칠 때 까지 main 쓰레드가 기다리도록 했다.
만약 join(long millis)이나 join(long millis, int nanos)를 사용하면 지정된 시간만큼만 main 쓰레드가 기다리도록 할 수 있다. 이처럼 한 쓰레드의 작업의 중간에 다른 쓰레드의 작업이 필요할 때 join()을 사용한다.
class ThreadEx14 {
public static void main(String args[]) {
A th1 = new A();
B th2 = new B();
th1.start();
try {
th1.join();
// join(): 두 개 이상의 쓰레드가 동작할 시 하나의 쓰레드에 대해서 지속을 거는 것. 두 개의 쓰레드가 진행하고 있는데 한 쓰레드에 대해서 join 걸면 // 그 쓰레드가 끝날때 까지 기다려준다.
// cf.) sleep()은 전체 쓰레드에 대해 지연을 건다. 하지만 join()은 특정 쓰레드에 대해 지연을 건다.
} catch(InterruptedException e) {}
th2.start();
}
}
class A extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print("-");
}
}
}
class B extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print("|");
}
}
}
실행결과)
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
join()이 사용된 부분을 주석처리하고 실행한 결과이다.
두 쓰레드가 번갈아 가며 실행되지 않고, 순차적으로 실행해야할 때 join()을 사용해서 해결하는 방법을 보여주는 예제이다.
cf.) 바로 위 예제에 join() 부분을 주석처리한 경우
/*try {th1.join();} catch(InterruptedException e) {}*/
실행결과)
--|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||------------------------------------------------------------------------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
두 쓰레드가 순차적으로 실행되지 않고 번갈아 가며 실행되는 모습이다.
class ThreadEx15{
public static void main(String args[]) {
A th1 = new A();
B th2 = new B();
th1.start();
th2.start();
try {
th1.sleep(5000);
// sleep(): 작업 흐름 대기시간 설정한다. 5초동안 대기시간 갖은 후에 다음 문자의 실행흐름을 이어 나간다.
} catch(InterruptedException e) {}
System.out.print("<<main 종료>>");
} // main
}
class A extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print("-");
}
System.out.print("<<th1 종료>>");
} // run()
}
class B extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print("|");
}
System.out.print("<<th2 종료>>");
} // run()
}
실행결과)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||-------------------------------------<<th1 종료>>||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||<<th2 종료>><<main 종료>>
결과를 보면 쓰레드 th1의 작업이 가장 먼저 종료되었고, 그 다음이 th2, main의 순인 것을 알 수 있다.
th1과 th2에 대해 start()를 호출하자마자 th1.sleep(5000)을 호출하여 쓰레드 th1이 5초동안 작업을 멈추고 일시정지상태에 있도록 하였으니까 쓰레드 th1이 가장 늦게 종료되어야 하는데 결과에서는 제일 먼저 종료되었다. 그 이유는 sleep()이 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에 th1.sleep(5000)과 같이 호출하였어도 실제로 영향을 받는 것은 main 메서드를 실행하는 main 쓰레드이다.
그래서 sleep()은 static으로 선언되었으며 참조변수를 이용해서 호출하기 보다는 Thread.sleep(5000);과 같이 해야 한다. yield() 또한 이와 같은 이유에서 static으로 선언되어 있어서 항상 현재
실행 중인 쓰레드에 대해 동작하며 Thread.yield()와 같이 호출해야 한다.
cf.) 위의 예제 중 th1.sleep(5000) 대신 Thread.sleep(5000)
try {Thread.sleep(5000);} catch(InterruptedException e) {}
실행결과)
||||||||||||||||||||||--------------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----------------|||||||||||||||||||||||--------------------------------------------------------------------------------------------------------------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||---------------------|||||||||||||||-------------------------------------------------------------------------------<<th2 종료>>------------------------------------<<th1 종료>><<main 종료>>
class ThreadEx19 {
public static void main(String args[]) {
MyThreadEx19 th1 = new MyThreadEx19("*");
MyThreadEx19 th2 = new MyThreadEx19("**");
MyThreadEx19 th3 = new MyThreadEx19("***");
th1.start();
th2.start();
th3.start();
try {
Thread.sleep(2000);
th1.suspend();
Thread.sleep(2000);
th2.suspend();
Thread.sleep(3000);
th1.resume();
Thread.sleep(3000);
th1.stop();
th2.stop();
Thread.sleep(2000);
th3.stop();
} catch (InterruptedException e) {}
}
}
class MyThreadEx19 implements Runnable {
boolean suspended = false;
boolean stopped = false;
Thread th;
MyThreadEx19(String name) {
th = new Thread(this, name); // Thread(Runnable r, String name)
}
public void run() {
String name = Thread.currentThread().getName();
while(!stopped) {
if(!suspended) {
System.out.println(name);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
System.out.println(name + " - interrupted");
}
} else {
Thread.yield();
}
}
System.out.println(name + " - stopped");
}
public void suspend() {
suspended = true;
th.interrupt();
// interrupt(): 특정 객체를 멈추고자 할때 사용, 쓰레드의 권한 중지
System.out.println("interrupt() in suspend()");
}
public void resume() {
suspended = false;
}
public void stop() {
stopped = true;
th.interrupt();
System.out.println("interrupt() in stop()");
}
public void start() {
th.start();
}
}
실행결과)
*
**
***
*
**
***
*
interrupt() in suspend()
* - interrupted
***
**
**
***
** - interrupted
interrupt() in suspend()
***
***
***
*
***
*
***
*
***
*
interrupt() in stop()
interrupt() in stop()
* - interrupted
* - stopped
** - stopped
***
***
interrupt() in stop()
*** - interrupted
*** - stopped
- yield(): yield()를 호출해서 남은 실행시간을 while문에서 낭비하지 않고, 다른 쓰레드에게 양보하게 된다.
- interrupt(): InterruptedException을 발생시켜서 sleep(), join(), wait()에 의해 일시정지 상태인 쓰레드를 실행대기상태로 만든다. interrupt()가 호출되었을 때, sleep(), join(), wait()에 의한 일시정지상태가 아니라면 아무런 일도 일어나지 않는다. 만일 stop()이 호출되었을 때 Thread.sleep(1000)에 의해 쓰레드가 일시정지상태에 머물러 있는 상황이라면 쓰레드가 정지될 때까지 최대 약 1초의 시간지연이 생길 것이다. 그래서 잠자고 있는 쓰레드를 깨울 interrupt()가 필요하다. 같은 상황에서 interrupt()가 호출되면, Thread.sleep(1000)에서 InterruptedException이 발생하고 즉시 일시정지상태에서 벗어나 catch 블럭 내의 문장이 실행된다.
이처럼 interrupt()를 사용하면 쓰레드의 응답성을 높일 수 있다.