JAVA

[Thread] join(), yield()

ewok 2023. 2. 27. 14:39

join()

지정된 시간 동안 특정 스레드가 작업하는 것을 기다린다.

작업 중에 다른 스레드의 작업이 먼저 수행되어야 할 필요가 있을 때 사용한다.

 

void join()				// 작업이 모두 끝날 때까지
void join(long millis)			// 천분의 일초 동안
void join(long millis, int nanos)	// 천분의 일초 + 나노초 동안

interrupt()에 의해 대기상태에서 벗어날 수 있으며, 예외처리를 해야 한다. (InterruptedException이 발생하면 작업 재개)

 

 class ThreadEx19 {
	static long startTime = 0;

	public static void main(String args[]) {
		ThreadEx19_1 th1 = new ThreadEx19_1();
		ThreadEx19_2 th2 = new ThreadEx19_2();

		th1.start();
		th2.start();
		startTime = System.currentTimeMillis();

		try {
			th1.join();	// main쓰레드가 th1의 작업이 끝날 때까지 기다린다.
			th2.join();	// main쓰레드가 th2의 작업이 끝날 때까지 기다린다.
		} catch(InterruptedException e) {}

		System.out.print("소요시간:" + (System.currentTimeMillis() - ThreadEx19.startTime));
	} // main
}

class ThreadEx19_1 extends Thread {
	public void run() {
		for(int i=0; i < 300; i++) {
			System.out.print(new String("-"));
		}
	} // run()
}

class ThreadEx19_2 extends Thread {
	public void run() {
		for(int i=0; i < 300; i++) {
			System.out.print(new String("|"));
		}
	} // run()
}

 

아래 코드는 가비지 컬렉터를 간단히 흉내 내어 구현한 것이다.

class ThreadEx20 {
    public static void main(String args[]) {
        ThreadEx20_1 gc = new ThreadEx20_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을 깨운다.
            }

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

class ThreadEx20_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;
    }
}

실행해보면 MAX_MEMORY가 1000 임에도 usedMemory가 1000이 넘는 경우를 볼 수 있다. 이는 gc가 interrupt()에 의해 깨어났음에도 불구하고 gc()가 수행되기 이전에 main 스레드의 작업이 수행되어 메모리를 사용하기 때문이다.

 

따라서 gc를 깨우는 것뿐만 아니라 join()을 이용해서 스레드 gc가 작업할 시간을 어느 정도 주도록 main 스레드를 기다리게 할 필요가 있다.

if(gc.freeMemory() < requiredMemory || gc.freeMemory() < gc.totalMemory() * 0.4) {
    gc.interrupt();	// 잠자고 있는 쓰레드 t1을 깨운다.
    try {
        gc.join(100);
    } catch (InterruptedException e) {}
}

 

yield()

남은 시간을 다음 스레드에게 양보하고, 자신(현재 스레드)은 실행대기한다.

yield()와 interrupt()를 적절히 사용하면, 응답성과 효율을 높일 수 있다.

yield()를 사용하지 않을 경우 while 반복문이 무한 반복하며 busy-waiting 하게 된다. 이 경우 yield()를 사용하여 다른 스레드에게 순서를 양보하는 것이 더 효율적이다.

 

yield()는 반드시 동작한다는 보장이 없다. OS 스케줄러가 제어하기 때문이다.