13.8쓰레드의 실행제어

jungnoeun·2022년 6월 21일
1

java

목록 보기
16/22

쓰레드의 실행제어

쓰레드 프로그래밍이 어려운 이유는 동기화(synchronization)와 스케줄링(scheduling)때문이다. 앞서 우선순위를 통해 쓰레드간의 스케줄링하는 방법을 배우기는 했지만, 이것만으로는 한참 부족하다. 효율적인 멀티쓰레드 프로그램을 만들기 위해서는 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.

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

  • static void sleep(long millis) ,
    static void sleep(long millis, int nanos)
    : 지정된 시간 (천분의 일초 단위)동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태가 된다.

  • void join, void join(long millis) ,
    void join(long millis, int nanos)
    : 지정된 시간동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.

  • void interrupt()
    : sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다. 해당 쓰레드에서는 interruptedExeption이 발생함으로써 일시정지상태를 벗어나게 된다.

  • 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
    : 쓰레드의 작업이 종료된 상태

다음 그림은 쓰레드의 생성부터 소멸까지의 모든 과정을 그린것으로, 위에서 소개한 메서드들에 의해서 쓰레드의 상태가 어떻게 변화되는지 보여준다.





쓰레드 제어 예시

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

sleep은 지정된 시간동안 쓰레드를 멈추게 한다.

sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면, InterruptedException이 발생되어 실행대기상태가 된다.

sleep() 메서드는 th1.sleep(2000)과 같이 호출해도 실제로 영향을 받는 것은 현재 실행중인 쓰레드이다. 그래서 sleep()은 static으로 선언되어 있으며 참조변수를 이용해서 호출하기 보다는Thread.sleep(2000)과 같이 해야 한다.


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

interrupt()는 쓰레드에게 작업을 멈추라고 요청한다. 단지 멈추라고 요청만 하는 것일 뿐 쓰레드를 강제로 종료시키지는 못한다.
interrupt()는 그저 쓰레드의 interrupted 상태(인스턴스 변수)를 바꾸는 것일 뿐이다.

그리고 interrupted()는 쓰레드에 대해 interrupt()가 호출되었는지 알려준다.

interrupt()가 호출되지 않았다면 false를, 호출되었다면 true를 반환한다.

boolean isInterrupted()메서드를 통해 현재 쓰레드의 interrupted상태를 반환받을 수 있다.

쓰레드가 sleep(), wait(), join()에 의해 일시정지 상태(WAITING)에 있을 때, 해당 쓰레드에 대해 interrupt()를 호출하면 sleep(), wait(), join() 에서 Interrupted Exception이 발생하고 쓰레드는 실행대기 상태(RUNNABLE)로 바뀐다. 즉, 멈춰있던 쓰레드를 깨워서 실행가능한 상태로 만드는 것이다.

import javax.swing.JOptionPane;

class ThreadEx {
	public static void main(String[] args) throws Exception {
		ThreadEx_1 th1 = new ThreadEx_1();
		th1.start();

		String input = JOptionPane.showInputDialog("input data");
		th1.interrupt();
		System.out.println(" isInterrupted ");

	}
}

class ThreadEx_1 extends Thread {
	public void run() {
		int i = 10;
		
		while(i != 0 && !isInterrupted()) {
			System.out.println(i--);
			try {
				Thread.sleep(1000);
			} catch(InterruptedException e) {}
		}

		System.out.println("count end");

	}
} 

위와 같은 코드에서 th1에 interrupt를 발생시켜도 카운트 다운은 멈추지 않는다.

왜냐하면 Thread.sleep(1000)에서 InterruptedException이 발생되어 쓰레드의 interrupted 상태는 false로 자동 초기화되기 때문이다.

아래와 같이 코드를 수정하면 정상적으로 동작할 것이다.

try {
		Thread.sleep(1000);
	} catch(InterruptedException e) {
		interrupt();
	}

suspend(), resume(), stop()

suspend()sleep()처럼 쓰레드를 멈추게 한다.

suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행대기 상태가 되며, stop()은 호출되는 즉시 쓰레드가 종료된다.

이 메서드들은 쓰레드의 실행을 제어하는 가장 손쉬운 방법이지만 교착상태를 일으키기 쉽게 작성되어있으므로 사용이 권장되지 않는다.


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

yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보한다.

예를 들어 스케쥴러에 의해 1초의 실행시간을 할당받은 쓰레드가 0.5초의 시간동안 작업한 상태에서 yield()가 호출되면, 나머지 0.5초는 포기하고 다시 실행대기상태가 된다.

yield()interrupt()를 적절히 사용하면, 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능하게 할 수 있다.


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

쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 join()을 사용한다.

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

시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다. 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야할 필요가 있을 때 join()을 사용한다.

try{
	th1.join(); // 현재 실행중인 쓰레드가 쓰레드 th1의 작업이 끝날때까지 기다린다.
} catch(InterruptedException e) {}

join()sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으며, join()이 호출되는 부분은 try - catch문으로 감싸야 한다. join()은 여러모로 sleep()과 유사한 점이 많은데, sleep()과 다른 점은 join()은 현재 쓰레드가 아닌 특정 쓰레드에 동작하므로 static메서드가 아니라는 것이다.

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

        int requiredMemory = 0;

        for(int i = 0 ; i < 20 ; i++) {
            requiredMemory = (int) (Math.random() * 10) * 20;

            if(gc.freeMemory() < requiredMemory
                || gc.freeMemory() < gc.totalMemory() * 0.4) {
                    gc.interrupt(); // gc Thread의 Thread.sleep()에 interrupt를 건다.

                    try {
                        gc.join(100); // gc Thread가 동작할 시간을 제공해준다.
                    } catch (Exception e) {
                        //TODO: handle exception
                    }
            }

            gc.usedMemory += requiredMemory;
            System.out.println("usedMemory : " + gc.usedMemory);
        }
        
    }

}

class ThreadEx extends Thread {
    final static int MAX_MEMORY = 1000;

    int usedMemory = 0;

    public void run() {
        while (true) {
            try {
                Thread.sleep(10 * 1000);
            } catch(InterruptedException e) { // interrupt 발생 시
                System.out.println("Awaken by interrupt()");
            }

            gc();
        }
    }

    public void gc() {
        usedMemory -= 300;
        if(usedMemory < 0 ) 
            usedMemory = 0;    
    }
    public int totalMemory() {
        return MAX_MEMORY;
    }
    public int freeMemory() {
        return MAX_MEMORY - usedMemory;
    }

}

가비지콜랙터의 동작을 구현해본 코드이다.

데몬쓰레드를 생성하여 주기적으로 동작하게 하고 특정한 조건일 때 interrupt()를 호출해서 즉시 실행시킬 수 있고 join()을 함께 사용하여 해당 데몬쓰레드가 동작할 시간을 제공해준다.

profile
개발자

0개의 댓글