[김영한의 실전 자바 - 고급 1편] 스레드 제어와 생명주기2

Turtle·2024년 9월 2일
0
post-thumbnail

🏷️인터럽트

특정 쓰레드가 sleep()을 통해 쉬고 있을 때 해당 쓰레드를 급하게 깨워 일을 다시 시킬 수도 있고 sleep()으로 쉬고 있는 쓰레드에 대해 작업을 종료할 수도 있다.

인터럽트를 사용하면 WAITING, TIMED_WAITING과 같은 대기 상태 쓰레드를 직접 깨워서 작동하는 RUNNABLE 상태로 만들 수 있다.

public class ThreadStopMainV2 {

	public static void main(String[] args) {
		MyTask myTask = new MyTask();
		Thread thread = new Thread(myTask, "work");
		thread.start();
		
		sleep(4000);	// 4초 정지
		log("작업 중단 지시...");
		thread.interrupt();
		log("work 쓰레드 인터럽트 상태 = " + thread.isInterrupted());
	}

	static class MyTask implements Runnable {
		
		@Override
		public void run() {
			try {
				while (true) {
					log("작업 중...");
					Thread.sleep(3000);
				}
			} catch (InterruptedException e) {
				log("work 쓰레드 인터럽트 상태 = " + Thread.currentThread().isInterrupted());
				log("interrupt message = " + e.getMessage());
				log("state = " + Thread.currentThread().getState());
			}
			log("자원 정리...");
			log("작업 종료...");
		}
	}
}

실행 결과

13:40:06.963 [     work] 작업 중...
13:40:09.978 [     work] 작업 중...
13:40:10.915 [     main] 작업 중단 지시...
13:40:10.924 [     main] work 쓰레드 인터럽트 상태 = true
13:40:10.924 [     work] work 쓰레드 인터럽트 상태 = false
13:40:10.925 [     work] interrupt message = sleep interrupted
13:40:10.925 [     work] state = RUNNABLE
13:40:10.926 [     work] 자원 정리...
13:40:10.926 [     work] 작업 종료...

main 쓰레드에서 work 쓰레드를 실행시키고 main 쓰레드는 TIMED_WAITING 상태가 되고 work 쓰레드는 RUNNABLE 상태가 된다.

지정된 4초 중 3초가 지나면 work 쓰레드는 작업을 하고 3초간 대기한다. 그 다음 남은 1초간 work 쓰레드에서 작업을 하던 도중 인터럽트 상태가 되면서 main 쓰레드가 RUNNABLE 상태가 되면서 work 쓰레드에서 sleep()이 호출되면 InterruptedException 예외가 발생하게 되고 try ~ catch문으로 예외를 잡고 난 후, 밖으로 빠져나가면서 종료가 된다.

쓰레드의 인터럽트 상태는 boolean 타입이며 정상인 경우 false가 된다.
쓰레드의 인터럽트 상태를 정상으로 되돌리지 않으면 이후에도 계속 인터럽트가 발생하게 된다.
인터럽트 목적을 달성했다면 인터럽트 상태를 다시 정상으로 돌려놔야 한다.

while (인터럽트 상태 확인) 무한 반복문 조건을 판별할 때, 인터럽트 상태가 true라면 인터럽트 상태를 false로 돌려두는 방법을 사용한다.

쓰레드의 인터럽트 상태를 단순 확인할 용도라면 isInterrupted()를 사용하면 된다. 하지만 직접 체크해서 사용할 때는 Thread.interrupted()를 사용해야 한다.

  • 쓰레드의 인터럽트 상태가 문제가 되는 상태(true)라면 쓰레드의 인터럽트 상태를 정상(false)으로 돌려놓는다.
  • 쓰레드의 인터럽트 상태가 정상(false)이라면 해당 쓰레드 인터럽트 상태를 변경하지 않는다.
public class ThreadStopMainV4 {

	public static void main(String[] args) {
		MyTask myTask = new MyTask();
		Thread thread = new Thread(myTask, "work");
		thread.start();
		
		sleep(100);	// 1초 정지
		log("작업 중단 지시...");
		thread.interrupt();	// main 쓰레드에서 work 쓰레드에 인터럽트를 건다.
		log("work 쓰레드 인터럽트 상태 = " + thread.isInterrupted());
	}

	static class MyTask implements Runnable {
		
		@Override
		public void run() {
			// main 쓰레드에서 interrupt() 걸어 -> 인터럽트 상태 : true
			// !Thread.interrupted() -> 인터럽트 상태 : false
			while (!Thread.interrupted()) {
				log("작업 중...");
			}

			// work 쓰레드는 인터럽트 상태
			try {
				log("자원 정리 시도...");
				Thread.sleep(1000);
				log("자원 정리 완료...");
			} catch (InterruptedException e) {
				log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
				log("work 쓰레드 인터럽트 상태 = " + Thread.currentThread().isInterrupted());
			}
			log("작업 종료...");
		}
	}
}

🏷️인터럽트 활용 예제

public class MyPrinterV1 {

	public static void main(String[] args) {

		Printer printer = new Printer();
		Thread thread = new Thread(printer, "printer");
		thread.start();

		Scanner scanner = new Scanner(System.in);
		while (true) {
			log("출력할 문서를 입력하세요(종료 : q): ");
			String input = scanner.nextLine();
			if (input.equals("q")) {
				printer.work = false;
			}

			printer.addJob(input);
		}
	}

	static class Printer implements Runnable {

		volatile boolean work = true;
		Queue<String> jobQueue = new ConcurrentLinkedQueue<>();	// 동시성

		@Override
		public void run() {
			while (work) {
				// 큐가 비어있다면?
				if (jobQueue.isEmpty()) {
					continue;
				}

				String poll = jobQueue.poll();
				log("출력 : " + poll + ", 대기 : " + jobQueue);
				sleep(3000);	// 3초 대기
				log("출력 완료");
			}
		}

		public void addJob(String input) {
			jobQueue.offer(input);
		}
	}
}

👉과정

  • main 쓰레드 : Scanner로 사용자의 입력을 받아 Printer 인스턴스에 addJob으로 담는다.
  • printer 쓰레드
    1) q를 입력하면 workfalse가 되면서 종료가 된다.
    2) 문자열을 입력하면 printer 쓰레드에서 큐가 비어있는지 확인한다.
    3) 큐가 비어있지 않다면 poll()을 통해서 가져온다.
    4) 가져온 문자열과 큐에 대기하는 것들을 함께 출력한다. 이후 3초간 대기한다.

👉volatile : 여러 쓰레드가 동시에 접근하는 변수의 경우 해당 키워드를 사용해야 안전하다.
👉ConcurrentLinkedQueue : 여러 쓰레드가 동시에 접근하는 경우 컬렉션 프레임워크가 제공하는 일반적인 자료구조를 사용하면 안전하지 않다. 여러 쓰레드가 동시에 접근하는 경우 동시성을 지원하는 동시성 컬렉션을 사용해야 한다.

public class MyPrinterV3 {

	public static void main(String[] args) {

		Printer printer = new Printer();
		Thread thread = new Thread(printer, "printer");
		thread.start();

		Scanner scanner = new Scanner(System.in);
		while (true) {
			log("출력할 문서를 입력하세요(종료 : q): ");
			String input = scanner.nextLine();
			if (input.equals("q")) {
				thread.interrupt();
				break;
			}

			printer.addJob(input);
		}
	}

	static class Printer implements Runnable {

		Queue<String> jobQueue = new ConcurrentLinkedQueue<>();	// 동시성

		@Override
		public void run() {
			while (!Thread.interrupted()) {
				// 큐가 비어있다면?
				if (jobQueue.isEmpty()) {
					continue;
				}

				try {
					String poll = jobQueue.poll();
					log("출력 : " + poll + ", 대기 : " + jobQueue);
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					log("인터럽트");
					break;
				}
				log("출력 완료");
			}
		}

		public void addJob(String input) {
			jobQueue.offer(input);
		}
	}
}

🏷️yield 양보하기

public class YieldMain {

	private static final int THREAD_COUNT = 1000;

	public static void main(String[] args) {
		for (int i = 0; i < THREAD_COUNT; i++) {
			Thread thread = new Thread(new MyRunnable());
			thread.start();
		}
	}

	static class MyRunnable implements Runnable {

		@Override
		public void run() {
			for (int i = 0; i < 10; i++) {
				System.out.println(Thread.currentThread().getName() + " - " + i);
			}
		}
	}
}

👉과정

  • 특정 쓰레드가 0 ~ 9까지 모두 출력한 후 다음 쓰레드가 계속 작업을 이어나간다.
  • 쓰레드의 실행 순서는 보장되지 않는다.
public class YieldMain {

	private static final int THREAD_COUNT = 1000;

	public static void main(String[] args) {
		for (int i = 0; i < THREAD_COUNT; i++) {
			Thread thread = new Thread(new MyRunnable());
			thread.start();
		}
	}

	static class MyRunnable implements Runnable {

		@Override
		public void run() {
			for (int i = 0; i < 10; i++) {
				System.out.println(Thread.currentThread().getName() + " - " + i);
				sleep(1000);
			}
		}
	}
}

👉과정

  • sleep(1000)을 사용하면 1초 동안 쓰레드가 TIMED_WAITING 상태가 된다.
  • TIMED_WAITING → RUNNABLE 상태가 된다.
  • 이 방식은 쓰레드의 상태 변화가 RUNNABLE → TIMED_WAITING → RUNNABLE 상태가 되면서 스케줄링 큐에 넣었다가 큐에서 빼는 과정을 반복한다.
  • 이 과정은 복잡한 과정이며 특정 시간만큼 스레드가 실행되지 않는다는 단점이 있다.
  • 양보할 쓰레드가 없는 상황에서 위와 같은 과정을 반복하면 굳이 양보할 상황이 아닌데 자기 자신 쓰레드가 실행되지 않고 기다려야 하는 문제가 발생한다.
public class YieldMain {

	private static final int THREAD_COUNT = 1000;

	public static void main(String[] args) {
		for (int i = 0; i < THREAD_COUNT; i++) {
			Thread thread = new Thread(new MyRunnable());
			thread.start();
		}
	}

	static class MyRunnable implements Runnable {

		@Override
		public void run() {
			for (int i = 0; i < 10; i++) {
				System.out.println(Thread.currentThread().getName() + " - " + i);
				Thread.yield();
			}
		}
	}
}

👉과정

  • 현재 실행 중인 쓰레드가 자발적으로 CPU를 양보하여 다른 쓰레드가 실행되도록 할 수 있다.
  • yield()의 경우 RUNNABLE 상태를 유지한 상태로 양보를 한다. 양보할 쓰레드가 없다면 본인이 실행되는 것이다.

0개의 댓글