쓰레드의 실행제어
1. 쓰레드의 실행제어
- 쓰레드 프로그래밍이 어려운 이유는 동기화(synchronization)와 스케줄링(scheduling) 때문이다.
- 멀티쓰레드를 구현하기 위해서는 보다 정교한 스케줄링을 통해 자원 낭비없이 프로그래밍
해야한다.
- 아래의 코드는 쓰레드 상태 관련 메서드들이다.
메서드 | 설명 |
static void sleep(long millis), sleep(long millis, int nanos) | 지정된 시간(천분의 일초 단위) 동안 쓰레드를 일시정지한다. 시간이 지나면 다시 실행대기 상태가 된다. |
void join(), join(long millis), join(long millis, int nanos) | 지정된 시간만큼 작업을 수행 후, 호출한 쓰레드로 다시 돌아온다. |
void interrupt() | sleep() 또는 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기 상태로 만든다. 해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지 상태를 벗어나게 한다. |
void stop() | 쓰레드를 즉시 종료시킨다. |
void suspend() | 쓰레드를 일시정지 시킨다. resume()을 호출하면 다시 실행대기 상태가 된다. |
void resume() | |
static void yield() | 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기 상태가 된다. 응답성을 높이기 위해서 만들어지게 되었다. |
( resume(), stop(), suspend()는 쓰레드를 교착상태(dead-lock)로 만들기 쉽기 때문에, deprecated가 되었다. )
( 쓰레드가 실제로 실행될 때 쓰레드 상태에 대한 과정은 749p를 참고한다. )
1-1. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(1)
: sleep에 대한 함수를 구현해보고 프로그래밍 순서를 파악하는 예제이다.
public class Exercise009 {
public static void main(String[] args) {
ThreadEx7 t1 = new ThreadEx7();
ThreadEx8 t2 = new ThreadEx8();
t1.start();
t2.start();
try {
t1.sleep(2000);
//Thread.sleep(1000);
}catch (Exception e) {
// TODO: handle exception
}
System.out.println("<<main 종료>>");
}
}
class ThreadEx7 extends Thread{
public void run() {
for(int i=0; i<300; i++) {
System.out.print("-");
}
System.out.println("<<t1 종료>>");
}
}
class ThreadEx8 extends Thread{
public void run() {
for(int i=0; i<300; i++) {
System.out.print("|");
}
System.out.println("<<t2 종료>>");
}
}
( 위 코드만 보면 출력이 "<<t2 종료>>"가 먼저될 거 같지만 "<<t1 종료>>"가 먼저될 수 있다. )
( 저렇게 순서가 다른 이유는 "t1.sleep(2000);" 부분 때문이다. )
( t1.sleep(2000)을 하면 해당 쓰레드가 sleep에 빠져들 것 같지만 사실 실제로 영향을 받는 것은
main 메서드를 실행하는 main 쓰레드이다. )
( 그래서 참조변수로 호출하기 보다는 "Thread.sleep()"으로 선언하는 것이 좋다. )
( (참고) "<<main 종료>>"는 제일 마지막에 출력된다. )
1-2. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(2)
: interrupt()와 interrupted(), isInterrupted()에 대한 함수를 알아보고 구현해보는 예제이다.
( interrupt(): 쓰레드에게 작업을 멈추라고 요청한다. 단, 강제로 종료시키지는 못한다. )
( interrupted(): 쓰레드의 상태가 interrupted인지 true, false로 받아온다. )
( isInterrupted(): interrupted()와 같은 내용이지만 만약 true라면 상태를 false로 초기화시킨다. )
import javax.swing.JOptionPane;
public class Exercise010 {
public static void main(String[] args) throws Exception {
ThreadEx9 t1 = new ThreadEx9();
t1.start();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " + input + "입니다.");
t1.interrupt();//첫 번째 실행
System.out.println("isInterrupted(): " + t1.isInterrupted());
}
}
class ThreadEx9 extends Thread{
public void run() {
int i=10;
while(i!=0 && !isInterrupted()) {
System.out.println(i--);
try {
Thread.sleep(500);
}catch (InterruptedException e) {
interrupt();//두번쨰 실행
}
}
System.out.println("종료");
}
}
( 10에서 0까지 while문을 돌리는데 만약 main에서 interrupt가 발생하게 되면, InterruptedException을
발생시켜 쓰레드를 중단시키는 작업을 한다. )
( 순서는 interrupt()를 발생시켜서 catch문을 돌아가게 한 다음, 그 안에서 interrupt() 발생시켜
while문 종료를 수행한다. )
1-3. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(3)
: suspend(), resume(), stop()을구현해보는 예제이다.
import javax.swing.JOptionPane;
public class Exercise011 {
public static void main(String[] args) {
RunImplEx11 r = new RunImplEx11();
Thread t1 = new Thread(r, "*");
Thread t2 = new Thread(r, "**");
Thread t3 = new Thread(r, "***");
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(2000);
t1.suspend();
Thread.sleep(2000);
t2.suspend();
Thread.sleep(3000);
t1.resume();
Thread.sleep(3000);
t1.stop();
t2.stop();
Thread.sleep(2000);
t3.stop();
}catch (InterruptedException e) {
// TODO: handle exception
}
}
}
class RunImplEx11 implements Runnable{
int i=1;
public void run() {
while(true) {
System.out.println(Thread.currentThread().getName() + " " + i++);
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
// TODO: handle exception
}
}
}
}
( suspend()와 stop()은 교착상태를 유발하기 때문에, deprecated된 상태인데, 일단 이해하기 위해
예제를 수행했다. )
( 위의 숫자를 보여준 것은 new Thread()를 할 때 같은 참조변수를 사용해서 Runnable을 초기화
시켰기 때문에 저렇게 하면 상태에 대한 내용이 공유될 수 있다는 위험을 알려준다. )
( 그래서 아래의 코드처럼 suspend()와 stop()에 대해 코드를 새로 정의하고 각각의 Runnable을
만들어 준다. )
public class Exercise012 {
public static void main(String[] args) {
RunImplEx12 r1 = new RunImplEx12();
RunImplEx12 r2 = new RunImplEx12();
RunImplEx12 r3 = new RunImplEx12();
Thread t1 = new Thread(r1, "*");
Thread t2 = new Thread(r2, "**");
Thread t3 = new Thread(r3, "***");
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(2000);
r1.suspend();
Thread.sleep(2000);
r2.suspend();
Thread.sleep(3000);
r1.resume();
Thread.sleep(3000);
r1.stop();
r2.stop();
Thread.sleep(2000);
r3.stop();
}catch (InterruptedException e) {
// TODO: handle exception
}
}
}
class RunImplEx12 implements Runnable{
boolean suspended = false;
boolean stopped = false;
int i=1;
public void run() {
while(!stopped) {
if(!suspended) {
System.out.println(Thread.currentThread().getName() + " " + i++);
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
// TODO: handle exception
}
}
}
}
public void suspend() { suspended = true; }
public void stop() { stopped = true; }
public void resume() { suspended = false; }
}
( 위와 다르게 공유가 안 되어있기 때문에 출력문의 숫자가 각각 따로 나온다. )
( Runnable에 suspend()와 resume()과 stop()을 따로 구현했다. )
( 아래의 코드는 더 객체지향적으로 만든 코드이다. )
public class Exercise013 {
public static void main(String[] args) {
ThreadEx13 t1 = new ThreadEx13("*");
ThreadEx13 t2 = new ThreadEx13("**");
ThreadEx13 t3 = new ThreadEx13("***");
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(2000);
t1.suspend();
Thread.sleep(2000);
t2.suspend();
Thread.sleep(3000);
t1.resume();
Thread.sleep(3000);
t1.stop();
t2.stop();
Thread.sleep(2000);
t3.stop();
}catch (InterruptedException e) {
}
}
}
class ThreadEx13 implements Runnable{
boolean suspended = false;
boolean stopped = false;
Thread th;
public ThreadEx13(String name) {
th = new Thread(this,name);
//Thread(Runnable r, String name)
}
@Override
public void run() {
while(!stopped) {
if(!suspended) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
}catch (InterruptedException e) {}
}
}
System.out.println(Thread.currentThread().getName() + " ~ stopped");
}
public void suspend() { suspended = true; }
public void stop() { stopped = true; }
public void resume() { suspended = false; }
public void start() { th.start(); }
}
1-4. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(4)
: yield()에 대해 알아보고 구현해보는 예제이다.
( yield(): 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보한다. )
( yield()는 interrupt()와 적절히 사용한다면, 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능
하게 할 수 있다. )
public class Exercise014 {
public static void main(String[] args) {
ThreadEx14 t1 = new ThreadEx14("*");
ThreadEx14 t2 = new ThreadEx14("**");
ThreadEx14 t3 = new ThreadEx14("***");
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(2000);
t1.suspend();
Thread.sleep(2000);
t2.suspend();
Thread.sleep(3000);
t1.resume();
Thread.sleep(3000);
t1.stop();
t2.stop();
Thread.sleep(2000);
t3.stop();
}catch (InterruptedException e) {
// TODO: handle exception
}
}
}
class ThreadEx14 implements Runnable{
boolean suspended = false;
boolean stopped = false;
Thread th;
public ThreadEx14(String name) {
th = new Thread(this,name);
//Thread(Runnable r, String name)
}
@Override
public void run() {
String name = th.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();
System.out.println(th.getName() + " - interrupt() by suspend()");
}
public void stop() {
stopped = true;
th.interrupt();
System.out.println(th.getName() + " - interrupt() by stop()");
}
public void resume() { suspended = false; }
public void start() { th.start(); }
}
( 50~52번째 줄에 suspend가 true이면 else를 넣어서 Thread.yield()를 발생시켰다. 그러므로,
while문을 돌지 않고 효율적으로 사용하게 된다. )
( 또한 suspend()와 stop()에 th.interrupt()를 넣었는데, 그 이유는 Thread.sleep(1000)에 상태에 있을때
바로 나올 수 있게 하기 위해서이다. 그러므로, 응답성이 좋아진다. )
1-5. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(5)
: join()에 대해 알아보고 구현해본다.
( join(): 지정된 시간만큼 작업을 수행 후, 호출한 쓰레드로 다시 돌아온다. )
( 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 join()을 사용한다. )
( 만약 지정된 시간이 없다면, 다른 쓰레드가 작업이 완료될 때까지 기다린다. )
( join() 또한 sleep()처럼 try-catch문으로 감싸야 하며, sleep()과 다른 점은 join()은 현재 쓰레드가
아닌 특정 쓰레드에 대해 동작해야 하므로 static 메서드가 아니다. )
public class Exercise015 {
static long startTime;
public static void main(String[] args) {
ThreadEx15_1 th1 = new ThreadEx15_1();
ThreadEx15_2 th2 = new ThreadEx15_2();
th1.start();
th2.start();
startTime = System.currentTimeMillis();
try {
th1.join(); //maain 쓰레드가 th1의 작업이 끝날 때까지 기다린다.
th2.join(); //main 쓰레드가 th2의 작업이 끝날 때까지 기다린다.
}catch (InterruptedException e) {
}
System.out.println("소요시간: " + (System.currentTimeMillis() - Exercise015.startTime));
}
}
class ThreadEx15_1 extends Thread {
public void run() {
for(int i=0; i<300; i++) {
System.out.print(new String("-"));
}
}
}
class ThreadEx15_2 extends Thread {
public void run() {
for(int i=0; i<300; i++) {
System.out.print(new String("|"));
}
}
}
( join()을 사용하지 않았다면, main 쓰레드는 먼저 종료되었을 것이다. )
1-6. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(6)
: join()을 활용하여 가비지 컬렉터를 구현한다.
public class Exercise016 {
public static void main(String[] args) {
ThreadEx16 gc = new ThreadEx16();
gc.setDaemon(true);
gc.start();
int requireMemory = 0;
for(int i=0; i<20; i++) {
requireMemory = (int) (Math.random() * 10) *20;
//필요한 메모리가 사용할 수 있는 양보다 크거나 전체 메모리의 60%인 경우
// gc를 꺠운다.
if(gc.freeMemory() < requireMemory || gc.freeMemory() < gc.totalMemory()*0.4) {
gc.interrupt();
}
gc.usedMemory += requireMemory;
System.out.println("usedMemory: " + gc.usedMemory);
}
}
}
class ThreadEx16 extends Thread {
final static int MAX_MEMORY = 1000;
int usedMemory;
public void run() {
while(true) {
try {
Thread.sleep(10000);
}catch (InterruptedException e) {
System.out.println("Awaken by interrupt().");
}
gc();
System.out.println("Garbage Collected. Free Memory: " + freeMemory());
}
}
public void gc() {
usedMemory -= 300;
if(usedMemory<0) usedMemory = 0;
}
public int totalMemory() { return MAX_MEMORY; }
public int freeMemory() { return MAX_MEMORY - usedMemory; }
}
( 가비지 컬렉터를 수행하는 쓰레드를 데몬으로 만들어서 10초동안 대기하다가 gc() 함수를 수행
하도록 만들었다. )
( gc() 함수는 가비지 컬렉션을 수행하는 것이다. )
( 그러나 출력문을 보면 1000이 넘었음에도 계속 수행되었다. 그 이유는 쓰레드 gc가 interrupt()에
의해서 깨어났음에도 불구하고 gc()가 수행되기 이전에 main 쓰레드의 작업이 수행되어 메모리를
사용하기 때문이다. )
( 그래서 gc()가 돌아갈 수 있게 join()을 이용해서 시간을 줘야한다. 즉, 메모리를 확보한 다음 main
쓰레드를 돌린다는 의미이다. )
( 아래와 같이 try-catch문을 넣어서 다시한번 돌려보자. )
( 1000을 넘기지 못하는 것을 볼 수 있다. )