본문 바로가기

프로그래밍/JAVA

[ JAVA ] 쓰레드의 실행제어

효율적인 멀티쓰레드 프로그램을 만들기 위해서는 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.

⭐️ 쓰레드의 스케줄링과 관련된 메서드

메서드 설 명
static void sleep(long milllis)
static void sleep(long millis, int nanos)
지정된 시간(천분의 일초 단위)동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태가 된다. 
void join()
void join(long millis)
void join(long millis, int nanos)
지정된 시간동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면
join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
void interrupt() sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다.
해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지상태를 벗어나게 된다.
void stop() 쓰레드를 즉시 종료시킨다.
void suspend() 쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행대기상태가 된다.
void resume() suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다.
static void yield() 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은
실행대기상태가 된다.
참고) resume(), stop(), suspend()는 쓰레드를 교착상태(dead-lock)로 만들기 쉽기 때문에 deprecated되었다.

⭐️ 쓰레드의 상태

상태 설 명
NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
RUNNABLE 실행 중 또는 실행 가능한 상태
BLOCKED 동기화블럭에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태)
WAITING,
TIMED_WAITING
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrunnable) 일시정지 상태.
TIMED_WAITING은 일시정지시간이 지정된 경우를 의미한다.
TERMINATED 쓰레드의 작업이 종료된 상태
참고) 쓰레드의 상태는 Thread의 getState()메서드를 호출해서 확인할 수 있다. JDK1.5부터 추가됨

⭐️ 쓰레드의  생성부터 소멸까지의 과정

① 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐(queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.
② 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.
③ 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.
④ 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다.
    I/O block은 입출력작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데, 이런 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기상태가 된다.
⑤ 지정된 일시정지시간이 다되거나(time-out), notify(), resume(), interrupt()가 호출되면 일시정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.
⑥ 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.

① sleep(long millis) - 일정시간동안 쓰레드를 멈추게 한다.

static void sleep(long millis)  // 밀리세컨드, 1000분의 일초
static void sleep(long millis, int nanos)​ // 나노세컨드, 10억분의 일초

예제)

public class ThreadEx {

	public static void main(String[] args) {
		ThreadEx_1 th1 = new ThreadEx_1();
		ThreadEx_2 th2 = new ThreadEx_2();
		th1.start();
		th2.start();
		try {
			th1.sleep(2000);
		} catch(InterruptedException e) {}
			System.out.print("main 종료");
		}

	}

class ThreadEx_1 extends Thread {
	public void run() {
		for(int i =0;i<300;i++) 
			System.out.print("-");
		System.out.print("th1 종료");
	}
}
class ThreadEx_2 extends Thread {
	public void run() {
		for(int i =0;i<300;i++) 
			System.out.print("|");
		System.out.print("th2 종료");
	}
}
▶ sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면 (InterruptedException발생), 잠에서 깨어나 실행대기 상태가 된다.
▶ sleep()를 호출할 때는 항상 try-catch문으로 예외를 처리해줘야 한다.
▶ 예제의 결과를 보면 th1이 2초 동안 작업을 멈추고 일시정지 상태에 있도록 하였으니까 쓰레드 th1이 가장 늦게 종료되어야 하지만 결과에서는 제일 먼저 종료된다.
이유 : sleep()이 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에 'th1.sleep(2000)'과 같이 호출하였어도 실제로 영향을 받는 것은 main메서드를 실행하는 main쓰레드이다.
그래서 sleep()은 static으로 선언되어 있으며 참조변수를 이용해서 호출하기 보다는 'Thread.sleep(2000);'과 같이 사용해야 한다.

② interrupt()와 interrupted() - 쓰레드의 작업을 취소한다.

void interrupt()              // 쓰레드의 interrupted상태를 false에서 true로 변경
boolean isInterrupted()       // 쓰레드의 interrupted상태를 반환
static boolean interrupted()  // 현재 쓰레드의 interrupted상태를 반환 후, false로 변경
▶ interrupt() : 쓰레드에게 작업을 멈추라고 요청한다. 쓰레드를 강제로 종료시키지는 못하고, 쓰레드의 interrupted상태(인스턴스 변수)를 바꾼다.
▶ interrupted() : 쓰레드에 대해 interrupt()가 호출되었는지 알려준다. interrupt()가 호출되지 않았다면 false, 호출되었다면 true를 반환한다.
▶ isInterrupted() : 쓰레드의 interrupt()가 호출되었는지 알려준다. interrupted()와 달리 쓰레드의 interrupted상태를 false로 초기화하지 않는다.
▶ 쓰레드가 sleep(), wait(), join()에 의해 '일시정지 상태(WAITING)'에 있을 때, 해당 쓰레드에 대해 interrupt()를 호출하면, sleep(), wait(), join()에서 InterruptedException이 발생하고 쓰레드는 '실행대기 상태(RUNNABLE)'로 바뀐다.
즉, 멈춰있던 쓰레드를 깨워서 실행가능한 상태로 만드는 것이다.

③ suspend(), resume(), stop()

▶ suspend() : sleep()처럼 쓰레드를 멈추게한다.
▶ resume() : suspend()에 의해 일시정지상태에 있는 쓰레드를 다시 실행대기 상태로 만든다.
▶ stop() : 쓰레드를 즉시 종료시킨다.
▶ suspend()와 stop()이 교착상태(deadlock)를 일으키기 쉽게 작성되어있으므로 사용이 권장되지 않는다.

④ yield() - 다른 쓰레드에게 양보한다.

▶ yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보한다.
▶ yield()와 interrupt()를 적절히 사용하면, 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능하게 할 수 있다.

예제)

public class ThreadEx {
	public static void main(String[] args) {
		ThreadEx_1 th1 = new ThreadEx_1("*");
   		ThreadEx_1 th2 = new ThreadEx_1("**");
  		ThreadEx_1 th3 = new ThreadEx_1("***");
		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 ThreadEx_1 implements Runnable {
	boolean suspended = false;
	boolean stopped = false;
    
	Thread th;
	
	ThreadEx_1(String name) {
		th = new Thread(this, name);
	}
    
	public void run() {
		String name = th.getName();
        
		while(!stopped) {
			if(!suspended) {
				System.out.println(name);
				try {
					// 만약 stop()가 호출되었을 때 th.interrupt();를 하지 않으면
					// stopped의 값이 true로 바뀌었어도 쓰레드가 정지될 때까지 
					// 최대 1초의 시간지연이 생길 것이다.
					Thread.sleep(1000); // interrupt()가 호출되면, 예외가 발생된다.
				} catch (InterruptedException e) {
					System.out.println(name + "- interrupted");                
				}
			} else {
				// yield()를 호출해서 남은 실행시간을 while문에서 낭비하지 않고 
				// 다른 쓰레드에게 양보하게 되므로 더 효율적이다.
				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;
		// sleep()에서 InterruptedException이 발생하여
		// 즉시 일시정지 상태에서 벗어나게 되므로 응답성이 좋아진다.
		th.interrupt(); 
		System.out.println(th.getName() + "- interrupt() by stop()");   
    }
	public void resume() {suspended =false;}
	public void start() {th.start();}
}
▶ stopped와 suspended라는 boolean타입의 두 변수를 인스턴스 변수로 선언하고, 이 변수의 값을 변경함으로써 쓰레드의 작업이 중지되었다가 재개되거나 종료되도록 할 수 있다.

⑤ join() - 다른 쓰레드의 작업을 기다린다.

void join()
void join(long millis)
void join(long millis, int nanos)

 // Example
 try {
 	th1.join(); // 현재 실행중인 쓰레드가 th1의 작업이 끝날 때까지 기다린다.
 } catch(InterruptedException e) {}
▶ 쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 사용한다.
▶ 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다.
▶ interrupt()에 의해 대기상태에서 벗어날 수 있으며, join()이 호출되는 부분을 try-catch문으로 감싸야 한다.
▶ join()은 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static메서드가 아니다.

예제)

public class ThreadEx {
	public static void main(String[] args) {
		ThreadEx_1 gc = new ThreadEx_1();
		gc.setDaemon(true);
		gc.start();

		int requiredMemory = 0;
		
		for(int i=0; i<20; i++) {
			requiredMemory = (int)(Math.random()*10) * 20;
            
			// 필요한 메모리가 사용할 수 있는 양보다 크거나 전체 메모리의 60%이상을
			// 사용했을 경우 gc를 깨운다.
			if(gc.freeMemory() < requiredMemory
				|| gc.freeMemory() < gc.totalMemory() * 0.4) {
				gc.interrupt(); // 잠자고 있는 쓰레드 gc를 깨운다.
				try {
				// 쓰레드 gc가 작업할 시간을 어느 정도 주고
				// main 쓰레드가 기다리도록 해서, 사용할 수 있는 메모리가 확보된 다음
				// 작업을 계속한다.
					gc.join(100);
				} catch(InterruptedException e) {}
			}
			gc.usedMemory += requiredMemory;
			System.out.println("usedMemory:"+gc.usedMemory);
		}
	}
}

class ThreadEx_1 extends Thread {
	final static int MAX_MEMORY = 1000;
	int usedMemory = 0;
    
	public void run() {
		while(true) {
			try {
				Thread.sleep(10*1000); // 10초를 기다린다.
			} catch (InterruptedException e) {
				System.out.println("Awaken by interrupt()");
			}
			gc(); // garbage collection을 수행한다.
			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;}
}
▶ sleep()을 이용해서 10초마다 한 번씩 가비지 컬렉션을 수행하는 쓰레드를 만든 다음, 쓰레드를 생성해서 데몬 쓰레드로 설정
▶ 반복문을 사용해서 메모리의 양을 계속 감소시키도록 했고, 매 반복마다 if문으로 메모리를 확인해서 남은 메모리가 전체메모리의 40%미만일 경우에 interrupt()를 호출해서, 즉시 가비지 컬렉터 쓰레드를 깨워서 gc()를 수행하도록 함
▶ 가비지 컬렉터 쓰레드를 깨울 때 join()을 이용해서 쓰레드 gc가 작업할 시간을 어느 정도 주고 main 쓰레드가 기다리도록 해서, 사용할 수 있는 메모리가 확보된 다음에 작업을 계속 한다.