JAVA

thread 2 (스레드 제어~스레드 풀)

황민준 2022. 9. 24. 20:28

Thread Control

wait() : 스레드 스스로 정지 상태로 만든다.

notify() : 일시 정지 중인 스레드 중 하나를 깨운다.

notifyAll() : 정지된 모든 스레드를 깨운다.

-> 이 때 wait()을 먼저 하게 되면 깨워줄 스레드가 없게 되므로 다른 스레드를 notify()로 깨운 상태에서 wait()을 해야한다.

-> wait(), notify(), notifyAll() syncronized 안에서 호출해야 한다.

 

package chap10.ex09.wait;

public class WorkObj {

	// 두 개의 스레드가 번갈아가며 사용해야 한다.
	public synchronized void work() {
		System.out.println(Thread.currentThread().getName() + " 가 실행");

		notify(); // 다른 스레드를 깨우고

		try {
			wait(); // 스스로 대기함
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}
}
package chap10.ex09.wait;

public class WorkThread extends Thread {

	private WorkObj obj;

	public WorkThread(WorkObj obj) {
		this.obj = obj;
	}

	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			obj.work();
		}
	}
}
package chap10.ex09.wait;

public class Main {

	public static void main(String[] args) {		
		
		// 하나의 객체를 만들어서
		WorkObj obj = new WorkObj();
		
		// 두 개의 스레드가 사용하는 상황
		Thread thA = new WorkThread(obj);
		Thread thB = new WorkThread(obj);
		
		thA.start();
		thB.start();
		
	}

}

 

스레드를 종료하는 방법

기본적으로 스레드는 run()의 내용이 모두 실행되면 종료한다.

무한 반복문이 있을 경우 강제로 종료를 해야한다.

stop()을 사용하여 강제 종료할 수 있지만 사용하지 않는다

-> stop()은 종료할 것이라는 아무 경고 없이 바로 종료시키기에 후속 작업이 불가능하다.

이 때 stop flag를 사용하거나 interrupt()를 사용해야 한다.

stop flag는 반복문을 탈출할 수 있는 조건을 줌으로써 종료시킬 수 있는 방법이다.

interrupt() 방식은 스레드에 강제로 interrupt 예외를 발생시켜서 try-catch문을 통해 종료시킬 수 있는 방식이다.

-> 이 때 인터럽트 예외는 반드시 sleep이 있어야만 발생된다.

-> 다른 방법으로는 스레드.interrupted()를 통하여 현재 스레드가 인터럽트 됐는지 확인하여 break 하는 방법도 있다.

 

package chap10.ex10.stop;

public class StopFlag extends Thread {

	private boolean stop = false;
	
	public void setStop(boolean stop) {
		this.stop = stop;
	}

	@Override
	public void run() {
		
		while(stop==false) {
			System.out.println("StopFlag thread 실행 중");
		}
		System.out.println("작업 종료 후 뒷처리 실행");
		System.out.println("뒷처리 완료 후 종료");
	}
}
package chap10.ex10.stop;

public class Inter extends Thread {

	@Override
	public void run() {
/* interrupt() 를 이용한 방식 1
 * 
		try {
			while (true) {
				System.out.println("Inter thread 실행 중");
				// 인터럽트 예외는 반드시 sleep이 있어야만 발생된다.
				Thread.sleep(1);
			}
		} catch (InterruptedException e) {
			System.out.println("작업 종료 후 뒷처리");
			System.out.println("뒷처리 후 종료");
			// e.printStackTrace();
		}
*/
		// interrupt() 를 이용한 방식 2
		while (true) {
			System.out.println("Inter thread 실행 중");
			if(Thread.interrupted()) {
				break;
			}
		}
		System.out.println("작업 종료 후 뒷처리");
		System.out.println("뒷처리 후 종료");
	}
}
package chap10.ex10.stop;

public class Main {

	public static void main(String[] args) throws InterruptedException {
		/*
		 * StopFlag flag = new StopFlag(); 
		 * flag.start(); 
		 * Thread.sleep(1000);
		 * //flag.stop(); <- 권고하지 않는 방식 flag.setStop(true);
		 */

		Inter inter = new Inter();
		inter.start();
		Thread.sleep(1000);
		// 강제 interrupted 예외를 발생시키는 메소드
		inter.interrupt();

	}

}

 

Daemon Thread

데몬 스레드는 워크 스레드와 마찬가지로 메인 스레드에서 생성되는 스레드이다.

워크 스레드는 메인 스레드가 종료해도 할 일이 남았으면 계속 실행하지만 데몬 스레드는 할 일이 남아있어도 메인 스레드가 종료되면 같이 종료된다.

스레드.setDaemon()을 통하여 데몬 스레드로 만들 수 있다.

 

package chap10.ex11.demon;

public class WorkThread extends Thread {

	@Override
	public void run() {
		
		try {
			while(true) {
				Thread.sleep(1000);
				System.out.println("워크 스레드 작업!!!");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}
package chap10.ex11.demon;

public class MainThread {

	public static void main(String[] args) throws InterruptedException {

		// 워크 스레드 생성 후
		WorkThread work = new WorkThread();
		work.setDaemon(true); // work -> demon
		work.start(); // 동작
		
		Thread.sleep(3000); // 3초 대기 후
		
		System.out.println("메인 스레드 종료");
		
		// 워크 스레드 : 할 일이 남아 있으면 메인 스레드가 죽어도 계속 실행한다.
		// 데몬 스레드 : 할 일이 남아 있어도 메인 스레드가 죽으면 같이 죽는다.
	}

}

 

Thread Group

스레드는 Name을 통하여 관리하는데 양이 많아지면 Group을 통해 관리할 수 있다.

스레드 그룹에는 단체 명령어를 사용할 수 있다.

 

Thread Pool

스레드 풀이란 미리 스레드를 만들어 놓고 스레드를 사용한다면 꺼내주고 사용이 끝나면 다시 반납받는 형식이다.

스레드 풀을 쓰는 이유는 스레드를 계속 만들게 되면 병렬처리의 폭증으로 시스템 저하가 발생하는데 정해진 스레드가 작업 큐에 있는 작업들을 하나씩 처리하기 때문에 이를 해결할 수 있다.

Thread PoolExecutorService 객체에서 생성된다.

스레드 풀을 생성하는 방법으로는 newcachedThreadPoolnewFixedThreadPool 가 있다.

 

newcachedThreadPoolnewFixedThreadPool의 차이점

 

- newcachedThreadPool : 초기에는 0개의 스레드고 요청이 있으면 스레드를 만든다. 스레드 사용 후에 반납을 받으면 60초간 유지하고 60초 동안 그 스레드를 다시 사용하지 않으면 삭제한다.

 

- newFixedThreadPool : 처음부터 n개의 스레드를 만들고 그것으로 사용한다.

-> 이 때 내가 사용가능한 스레드의 수를 알고 싶으면

Runtime.getruntime.availableProcessors()를 사용한다.

 

package chap10.ex12.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {

	public static void main(String[] args) {

		// Thread Pool 생성 방법
		// 1개 이상의 스레드가 추가되었을 때 60초 동안 놀고 있으면 스레드를 제거한다.
		// 초기에는 스레드가 0개 -> 요청이 있으면 제작 -> 반납받은 스케이트가 있으면 대여
		// 반납받은 스케이트가 60초 이상 아무도 안 빌려 가면 버린다.
		ExecutorService pool1 = Executors.newCachedThreadPool();
		
		// 기본적으로 n개의 스레드를 유지한다.
		// n개 -> 내 CPU에서 가용할 수 있는 메인 스레드 수
		int n = Runtime.getRuntime().availableProcessors();
		System.out.println(n);
		ExecutorService pool2 = Executors.newFixedThreadPool(n);		
		
	}

}

 

Thread Pool의 사용 방법

스레드 풀에서 할 일을 만드는 방법으로는 Runnable 구현 클래스와 Callable 구현 클래스 방법이 있다.

Runnable 방식은 return값이 없고 Callable 방식은 return값이 있다.

그리고 Callable 방식에선 제네릭을 사용해야 하고 반환 타입도 제네릭 타입이다.

Thread Pool을 사용하려면 execute()submit()을 사용해야 한다.

execute()는 처리 결과를 반환받는 것이 없고 작업 처리 도중 예외가 생기면 스레드를 종료하고 풀에서 스레드를 제거한다.

submit()은 처리 결과를 Future 객체를 반환받고 작업 처리 도중 예외가 생기면 스레드를 종료하지 않고 재사용한다.

Runnable 방식은 execute()submit()을 둘 다 사용 가능하다.

Callable 방식은 submit()만 사용 가능하다. -> 리턴값이 무조건 있기 때문에

submit()을 사용하여 값을 꺼내 오려면 get()을 사용해야 한다.

 

Thread Pool 종료 방법

스레드 풀은 메인 스레드가 종료되어도 계속 실행 상태로 남아있기 때문에 종료를 해주어야 한다.

shutdown(), shutdownNow(), awaitTermination()이 있다.

 

shutdown() : 작업 중인 내용을 기다렸다가 마무리하고 스레드 풀 종료

shutdownNow() : 작업 중인 내용이 있어도 상관없이 인터럽트하여 종료

awaitTermination() : timeout 시간 부여에 따라 시간 안에 전부 처리했으면 true, 시간 내에 처리하지 못했으면 인터럽트하고 false

 

package chap10.ex13.exec;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Runner {

	public static void main(String[] args) throws InterruptedException {

		// 1. Thread Pool 생성
		int n = Runtime.getRuntime().availableProcessors();
		ExecutorService pool = Executors.newFixedThreadPool(n);
		
		// 2. Runnable 로 해야 할 일 생성 -> 반환값 없는 작업
		// 익명 객체 - 원래는 Runnable 인터페이스를 구현받은 클래스를 만들어야 함
		Runnable runnable = new Runnable() {
			
			@Override
			public void run() {
				System.out.println("Runnable 처리");
			}
		};
		
		// 3. 작업 실행
		// Runnable은 submit()과 execute() 둘 다 사용 가능하다.
		// 반환값도 없는데 Runnable은 왜 submit()도 사용 가능한 것인가?
		// submit()을 사용하면 Future 객체를 반환받는데 이렇게 되면 get()을 사용하여 blocking 할 수 있다.
		
		// submit() : 반환값이 있고 예외 발생 스레드를 재사용한다.
		// execute() : 반환값이 없고 예외 발생 스레드를 제거한다.
		pool.execute(runnable);
		
		// 4. 스레드 풀 종료
		pool.shutdown(); // 다 들어올 때까지 기다렸다가 닫는다.
		pool.shutdownNow(); // 안 들어온 애들은 죽이고 닫는다.
		
		// API에서도 권고하는 종료 방법
		// 시간이 다 되도록 종료되지 않은 스레드는 강제 인터럽트를 발생시킨다.
		// 일반 예외 처리를 해준다.
		boolean end = pool.awaitTermination(10, TimeUnit.SECONDS);
		
	}

}
package chap10.ex13.exec;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Calla {

	public static void main(String[] args) throws InterruptedException, ExecutionException {

		// 1. Thread Pool 생성
		int n = Runtime.getRuntime().availableProcessors();
		ExecutorService pool = Executors.newFixedThreadPool(n);
		
		// 2. Callable로 작업 생성 -> 반환값 있는 경우
		Callable<String> callable = new Callable<String>() {
			
			@Override
			public String call() throws Exception {
				System.out.println("callable 처리");
				return "완료";
			}
		};
		
		// 3. 작업 실행
		// callable은 무조건 submit()만 사용 가능하다.
		// 반환값이 있기 때문이다.
		Future<String> result = pool.submit(callable);
		System.out.println(result.get()); // 반환 값은 Future라는 객체에 담겨 오기 때문에 get() 으로 꺼내줘야 한다.
		// 왜 굳이 Future로 반환하며 get()까지 써서 꺼내야 하나?
		// Future 객체에 get()을 써줘야 join()의 역할을 하여 blocking 할 수 있기 때문에
		
		pool.shutdown();
		
	}

}

 

*Runnable 방식에서 굳이 submit()을 쓰는 이유는? 그리고 Future 객체를 굳이 get()으로 꺼내오는 이유는?

get()을 사용하면 join()과 같은 역할을 하여 blocking할 수 있다. (해당 스레드가 처리할 때까지 다른 스레드의 진행을 막는다)

이 때 Runnable 방식은 return이 없는데 Future 객체의 제네릭을 뭐라고 써야할까?

-> 어떤 타입인지 모를 때는 ? (와일드 카드)를 사용한다.

 

package chap10.ex14.block;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class NoResultBlock {

	public static void main(String[] args) throws InterruptedException, ExecutionException {

		int n = Runtime.getRuntime().availableProcessors();
		ExecutorService pool = Executors.newFixedThreadPool(n);

		Runnable job = new Runnable() {

			@Override
			public void run() {
				
				try {
					for (int i = 1; i <= 10; i++) {
						System.out.println("작업 처리" + i);
						Thread.sleep(500);
					}
				} catch (InterruptedException e) {
					e.printStackTrace();

				}
				
			}
		};
		
		Future<?> result = pool.submit(job); // 리턴값이 없으므로 제네릭에 와일드 카드를 사용한다.
		result.get();
		System.out.println("작업종료");
		
		pool.shutdownNow();
	}

}
package chap10.ex14.block;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ResultBlock {

	public static void main(String[] args) throws InterruptedException, ExecutionException {

		// 1. 스레드 풀 생성
		int n = Runtime.getRuntime().availableProcessors();
		ExecutorService pool = Executors.newFixedThreadPool(n);
		
		// 2. 해야할 일(1에서 100까지 더하기)
		Callable<Integer> call = new Callable<Integer>() {
			
			@Override
			public Integer call() throws Exception {
				int sum = 0;
				for(int i=1;i<=100;i++) {
					sum += i;
				}
				System.out.println(sum);
				return sum;
			}
		};
		
		// 3. 실행 요청
		Future<Integer>result = pool.submit(call);
		int sum = result.get(); // get()까지 사용해야 join()의 blocking 효과를 낼 수 있다.
		System.out.println("1~100 까지의 합은 ? " + sum);
		System.out.println("종료");
		
		// 4. 풀 종료
		pool.shutdownNow();
	}

}