쓰레드의 실행제어

정순동·2024년 1월 4일
0

자바기초

목록 보기
72/89

쓰레드의 실행제어

쓰레드의 실행을 제어할 수 있는 메서드가 제공된다.
이 들을 활용해서 보다 효율적인 프로그램을 작성할 수 있다.

쓰레드 프로그래밍이 어려운 이유는 동기화와 스케줄링을 잘 관리해야 하기 때문이다. 우선순위를 통해 쓰레드간의 스케줄링을 희망할 수는 있지만 이것만으로 멀티쓰레드 프로그램을 만들기에는 매우 부족하기에 아래 메서드들을 사용하여 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원가 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.

resume(), stop(), suspend()는 쓰레드를 교착상태(daed-lock)로 만들기 쉽기 때문에 deprecated되었다.

여기서 static메서드는 자기자신 쓰레드에게만 사용가능하다. 다른 쓰레드를 재우거나 깨울 수 없다는 뜻이다.

sleep()

sleep()메서드는 지정된 시간동안 쓰레드를 멈추게 한다.

	static void sleep(long millis);
    static void sleep(long millis, int nanos);

밀리세컨드(1000분의 1초)와 나노세컨드(10억분의 1초)의 시간단위로 세밀하게 지정할 수 있지만, 어느 정도의 오차가 발생할 수 있다.

	try {
    	Thread.sleep(1, 500000); // 쓰레드를 0.0015초 동안 멈추게 한다.
    } catch(InterruptException e) {}

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

그래서 sleep()메서드는 try-catch문과 함께 작성해야 하는데 귀찮은 사람은 아래와 같이 메서드를 만들어서 사용한다.

	void delay(long millis) {
    	try {
        	Thread.sleep(millis);
        } catch (InterruptExceptioin e) {}
    }

예제

public class SleepExample {
    public static void main(String[] args) {
        Thread th1 = new Thread(new ThreadEx8_1());
        Thread th2 = new Thread(new ThreadEx8_2());
        th1.start(); th2.start();

        try {
            th1.sleep(2000);
        } catch (InterruptedException e) {}

        System.out.print("<<main 종료>>");
    }
}

class ThreadEx8_1 extends Thread {
    public void run() {
        for(int i = 0; i < 300; i++) {
            System.out.print("-");
        }
        System.out.print(">>th1종료<<");
    }
}

class ThreadEx8_2 extends Thread {
    public void run() {
        for(int i = 0; i < 300; i++) {
            System.out.print("│");
        }
        System.out.print(">>th2종료<<");
    }
}

위 코드를 실행하면 어떤 쓰레드가 가장 늦게 종료될까? th1.sleep(2000);으로 인해 th1이 제일 늦게 종료되지 않을까? 라는 생각을 처음에는 할 것이다. 하지만 static void sleep()은 th1, th2이렇게 아무리 지정해 봐야 이 메서드를 호출한 쓰레드에서 동작하기 때문에 th1이 2초 쉬는게 아니라 main쓰레드가 2초 쉬게된다. 따라서 sleep()을 사용할 때에는 아래처럼 작성하자.

	th1.sleep(2000); // x 어차피 지정안됨, 헷갈리기만 할 뿐
    Thread.sleep(2000); // o 어차피 지정안되니 클래스로 static메서드를 불러오자.

interrupt()

진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야할 때가 있다. 예로 큰 파일을 다운로드 할 때 중간에 다운로드를 포기하고 취소할 수 있어야 한다. interrupt()는 쓰레드에게 작업을 멈추라고 요청한다. 단지 멈추라고 요청만 할 뿐, 강제로 종료시키지는 못한다. interrupt()는 그저 쓰레드의 interrupted상태(인스턴스 변수)를 바꾸는 것일 뿐이다.

interrupted()는 쓰레드에 대해 interrupt()가 호출됐는지 알려준다. 호출됐다면 true를 반환한다.

	Thread th = new Thread();
    th.start();
    	...
    th.interrupt(); // 쓰레드 th에 interrupt()를 호출한다.
    	...
    class MyThread extends Thread {
    	public void run() {
        	while(!interrupted()) { // interrupt()를 호출 당하기 전까지 반복
            	...
            }
        }
    }

위 코드는 interrupt()가 호출되면, interrupted()의 결과가 true로 바뀌면서 while문을 벗어난다.

isInterrupted()도 쓰레드의 interrupt()가 호출됐는지 확인하는데 사용할 수 있지만, interrupted()와 달리, isInterrupted()는 쓰레드의 interrupted상태를 false로 초기화 하지 않는다.

	void interrupt(); // 쓰레드의 interrupted상태를 false에서 true로 변경
    boolean isInterrupted(); // 쓰레드의 interrupted상태를 반환
    static boolean interrupted(); // 현재 쓰레드의 interrupted상태를 반환 후, false로 변경

위의 예제들 처럼 '일시정지 상태(WAITING)'에 있는경우가 아니라 일반적인 상황에서도 사용할 수 있고, '일시정지 상태'를 깨우는데 사용할 수도 있다. 물론 깨워서 '실행대기 상태'로 바꾸면 Interrupted Exception이 한번 발생한다.

suspend(), resume(), stop()

suspend()는 sleep()처럼 쓰레드를 멈추게 한다. suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행대기 상태가 된다. stop()은 호출되는 즉시 쓰레드가 종료된다.

suspend(), resune(), stop()은 쓰레드 실행을 제어하는 손쉬운 방법이었으나 suspend()와 stop()이 deadlock을 일으키기 쉽게 작성돼어 있으 'deprecated'되었다.

join()과 yield()

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

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

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

시간을 지정하지 않는다면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다. 작업 중 다른 쓰레드의 작업이 선행돼야 한는 경우 join()을 사용한다.

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

join()도 sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으며, 따라서 join()이 사용 되는 부분이 try-catch처리 돼야 한다. join()과 sleep()은 비슷하나, 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작함에 있어 static 메서드가 아니다.

join()은 자신의 작업 중간에 다른 쓰레드의 작업을 참여(join)시킨다는 의미로 이름 지어진 것이다.

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

yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)한다. 예를 들어 스케쥴러에 의해 1초를 받고 0.5를 사용한 후 yield()를 호출하면, 나머지 0.5초를 포기하고 실행대기상태가 된다.

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

yield()도 OS스케쥴러에게 그저 바라는 메서드일 뿐이므로 큰 기대는 하지 말자.

쓰레드의 동기화(synchronization)

멀티쓰레드 프로그램의 경우 다른 쓰레드의 작업에 영향을 미칠 수 있다.
진행 중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 '동기화'가 필요하다.

동기화하려면 간섭받지 않아야 하는 문장들을 '임계 영역'으로 설정

임계영역은 락(lock)을 얻은 단 하나의 쓰레드만 출입가능(객체 1개에 락 1개)

공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하난의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다. 이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 '쓰레드의 동기화(synchronization)'이라 한다.

쓰레드의 동기화 - 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는것.

JDK1.5이전에서는 synchronized블럭을 이용해 쓰레드의 동기화를 지원했지만, 이후 부터는 'java.util.concurrent.locks'와 'java.util.concurrent.atomic'패키지를 통해 다양한 방식으로 동기화를 구현할 수 있도록 한다.

synchronized키워드를 이용한 동기화

가장 간단한 동기화 방법으로는 synchronized라는 키워드를 사용해서 동기화 하는데, 이 키워드는 임계 영역을 설정하는데 사용된다. 아래와 같이 두 가지 방법이 존재한다.

	// 1. 메서드 전체를 임계 영역으로 지정
    public synchronized void calcSum() {
    	// ...
    }
    // 2. 특정한 영역을 임계 영역으로 지정
    synchronized(객체의 참조변수) {
    	// ...
    }

1번째 방법은 메서드에 synchronized를 붙임으로써 해당 메서드 전체가 임계 영역이 된다. 쓰레드는 synchronized메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻고 작업을 수행하다 메서드가 종료되면 lock을 반환한다.

2번째 방법은 메서드 내의 코드 일부를 블럭{}으로 감싸고 블럭 앞에 'synchronized (참조변수)'를 붙이는 것이다. 이떄 참조변수는 락(lock)을 걸고자 하는 객체를 참조하는 것이어야 한다. 이 블럭을 synchronized 블럭이라고 부르며, 이 블럭의 영역 안으로 들어가면서부터 쓰레드는 임계영역 객체의 lock을 얻고 이 블럭을 벗어나면 lock을 반환한다.

두 방법 모두 lock의 획득가 반납이 모두 자동적으로 이루어지므로 우리가 해야 할 일은 그저 임계 영역만 제대로 설정해 주면 된다.

임계 영역은 멀티 쓰레드 프로그램의 성능을 좌우하기에 가능하면 메서드 전체에 락을 거는것 보단 synchronized블럭으로 임계 영역을 최소화해서 보다 효율적인 프로그램이 되도록 지양해야한다.

따라서 동기화는 아무리 멀티쓰레드라도 한 메서드에 한 쓰레드만 접근할 수 있기 때문에, 동기화를 막 쓰면 프로그램의 성능이 하락한다.

wait()과 notify()

동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출하여 쓰레드가 락을 반납하고 그 자리에서 기다리게 한다. 그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있고, 기존에 작업하던 쓰레드가 다시 작업을 진행할 수 있는 상황이 되면 notify()를 호출하여 중단했던 작업을 쓰레드가 다시 락을 얻어 진행할 수 있게된다.

일단 wait()이 호출되면 호출한 쓰레드는 객체마다 있는 waiting pool에서 통지를 기다린다. 이 때 notify()를 호출하면 waiting pool에서 임의의 쓰레드 하나만 통지를 받게된다. notifyAll()을 호출하면 waiting pool에 있던 모든 쓰레드가 깨어나고 작업을 진행하려한다. 하지만 이 때도 락은 하나만 존재하기에 동시에 일어난 쓰레드 중 하나만 작업에 들어갈 것이다.

wait(), notify()는 특정 객체에 대한 것이므로 Object클래스에 정의돼있다.

또, waiting pool은 객체마다 하나씩 있는 공간이므로 notifyAll()이 호출 되더라도 다른 객체의 waiting pool에서는 아무런 변화가 없다.

	wait(), notify(), notifyAll()
    	- Object에 정의돼 있다.
        - 동기화 블록(synchronized블록) 내에서만 사용 가능하다.
        - 보다 효율적인 동기화를 가능하게 한다.

하지만 코드를 작성하다보면 쓰레드가 3개 이상일때 부터 특정 쓰레드를 깨우고싶은데 자꾸 다른 쓰레드가 notify()로 깨워지는걸 볼 수 있다. 이럴때는 Lock & Condition을 사용하자

https://cano721.tistory.com/166#Lock%EA%B3%BC_Condition%EC%9D%84_%EC%9D%B4%EC%9A%A9%ED%95%9C_%EB%8F%99%EA%B8%B0%ED%99%94
Lock & Condition이 포함된 다른 글.

0개의 댓글