ThreadPool의 개념 및 생성 방법

정인호·2025년 5월 22일

스레드

목록 보기
1/2

스레드 풀 생성(ThreadPoolExecutor)

스레드 풀을 자바에서 구현한 구현체가 ThreadPoolExecutor이다.

newCachedThreadPool()

  • 이 메소드로 생성된 스레드풀의 초기 수와 코어 수는 0개이고, 작업 개수가 많아지면 새 스레드를 생성시켜 작업을 처리함.
  • 60초 동안 스레드가 아무 작업을 하지 않으면 스레드를 풀에서 제거함.

newFixedThreadPool(int nThreads)

  • 최대 스레드 수를 정할 수 있는데, 놀고 있는 스레드가 있어도 최대로 정한 스레드 수를 유지함.

코어수는 스레드 제거시 최소 몇개의 스레드를 남겨 놓을지를 의미, 코어 수가 3이면 최소 3개는 남겨놓음.

메소드명(매개변수)초기 수코어 수최대 수
newCachedThreadPool()00Integer.MAX_VALUE
newFixedThreadPool(int nThreads)0생성된 수nThreads
ExecutorService threadPool = new ThreadPoolExecutor(
	3,                                  // 코어 스레드 개수
	100,                                // 최대 스레드 개수
	120L,                               // 놀고 있는 시간
	TimeUnit.SECONDS,                   // 놀고 있는 시간 단위
	new SynchronousQueue<Runnable>()    // 작업 큐
);

new SynchronousQueue() → 이 동기 기능으로 인해 작업 큐에 한번에 한 스레드만 작업 처리 요청을 보내게됨. 동시에 보내면 문제 됨.

스레드 풀 종료

스레드풀의 스레드는 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있음.

스레드풀의 모든 스레드를 종료하려면 ExecutorService의 다음 두 메소드 중 하나를 실행 해야함.

리턴 타입메소드명(매개변수)설명
voidshutdown()현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드풀을 종료시킨다.
ListshutdonwNow()현재 작업 처리 중인 스레드를 interrupt해서 (작업이 완료되지 않았더라도) 작업을 중지시키고 스레드풀을 종료시킨다. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록(list)이다.

shutdownNow()는 실행중이던 작업이 완료되지 않았어도 중지시키기에 손실이 발생할 수 있음.

shutdown()이 안전함.

Runnable은 thread가 처리해야 할 작업 내용을 가지고 있는 객체

하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현 함.

둘의 차이는 작업 처리 완료 후 리턴값이 있냐 없냐와 예외를 던지냐 안던지냐의 차이.

new Runnable() {
	@Override
	public void run() {
		// 스레드가 처리할 작업 내용
	}
}
new Callable<T> {
	@Override
	public T call() throws Exception {
		// 스레드가 처리할 작업 내용
		return T;
	}
}
public class RunnableExecuteExample {
	public static void main(String[] args) {
		// 1000개의 메일(스레드) 생성
		String[][] mails = new String[1000][3];
		for (int i = 0; i < mails.length; i++) {
			mails[i][0] = "admin@my.com";
			mails[i][1] = "member"+i+"@my.com";
			mails[i][2] = "신상";
		}
	
		// ExecutorService 생성
		ExecutorService executorService = Executors.newFixedThreadPool(5); // 최대 스레드 수 5개로 제한
		
		// 이메일을 보내는 작업 생성 및 처리 요청
		for (int i = 0; i < 1000; i++) {
			final int idx = i;
			executorService.execute(new Runnable() {
				@Override
				public void run() {
					Thread thread = Thread.currentThread(); // 현재 실행하는 Thread에 대한 참조 (실행되고 있는 Thread중 어떤 Thread가 이 작업을 처리하고 있는지)
					String from = mails[idx][0];
					String to = mails[idx][1];
					String content = mails[idx][2];
					System.out.println("[" + thread.getName() + "]" + from + " ==> " + to " +: " + content);
				}
			});
		}
		
		// ExecutorService 종료
		executorService.shutdown();
		
	}
리턴 타입메소드명(매개변수)설명
voidexecute(Runnable command)- Runnable을 작업 큐에 저장
- 작업 처리 결과를 리턴하지 않음
Futuresubmit(Callable task)- Callable을 작업 큐에 저장
- 작업 처리 결과를 얻을 수 있도록 Future를 리턴
public class CallableSubmitExample {
	public static void main(String[] args) {
		// ExecutorService 생성
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		
		// 계산 작업 생성 및 처리 요청
		for (int i = 1; i <= 100; i++) {
			final int idx = i;
			Future<Integer> future = executorService.submit(new Callable<Integer>() {
				@Override
				public Integer call() throws Exception {
					int sum = 0;
					for (int i=1; i<=idx; i++) {
						sum += i;
					}
					Thread thread = Thread.currentThread();
					return sum;
				}
			});
			
			try {
				int result = future.get(); // call 메서드의 리턴값이 생기기전까지 대기함.
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		executorService.shutdown();
	}
}

스레드 풀을 사용하지 않고 요청마다 스레드를 생성하면, 스레드 생성에 소요되는 시간 때문에 요청처리가 더 오래 걸림.

처리 속도보다 더 빠르게 요청이 늘어나면

  • 스레드가 계속 생성 (스레드 수 증가) → 메모리가 점점 고갈됨.
  • 컨텍스트 스위칭이 더 자주 발생 (→ 컨텍스트 스위칭은 하나의 프로세스 또는 스레드에서 다른 프로세스로 전환하는 과정)
  • CPU 오버헤드 증가로 CPU time 낭비
  • 어느 순간 서버 전체가 응답 불가능 상태에 빠짐.

스레드 풀 사용시 주의할 점

작업 큐에 제한이 있는 지 확인.

작업 큐에 제한이 없다면, 만약 모든 스레드가 요청을 처리 중이어서 작업큐에 요청이 무한정 쌓이게 된다면 메모리를 고갈시키게 되고 전체 시스템에 문제를 발생시키게 됨.

이 경우에는 작업 큐에 제한을 두어 버려지는 요청이 생기더라도 시스템에 문제가 발생하지 않도록 해야함.

public class ThreadPoolWithLimitedQueue {
	public static void main(String[] args) {
		// 제한된 크기의 작업 큐 (최대 5개까지 대기 가능)
		BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(5);
		
		// 스레드 풀 생성 (코어 스레드 2개, 최대 4개, 작업 큐 taskQueue 사용)
		ThreadPoolExecutor executor = new ThreadPoolExecutor(
			2,                                      // 코어 스레드 개수
			4,                                      // 최대 스레드 개수
			60, TimeUnit.SECONDS,                   // 초과 스레드의 유휴 시간
			taskQueue,                              // 작업을 저장할 큐 (크기 제한)
			new ThreadPoolExecutor.AbortPolicy()    // 큐가 꽉 차면 예외 발생
		);
		
		// 10개의 작업을 제출
		for (int i = 1; i <= 10; i++) {
			final int taskId = i;
			try {
				executor.execute(() -> {
					System.out.println("작업 " + taskId + " 실행 (thread: " + thread.currentThread().getName() + ")");
					try {
						Thread.sleep(2000); // 작업 수행 시간
					} catch (InterruptedException e) {
						Thread.currentThread().interrupt();
					}
				});
			} catch (RejectedExecutionException e) {
				System.out.println("작업 " + taskId + "이(가) 큐가 꽉 차서 거부됨");
			}
		}
		
		executor.shutdown();
	}
}
작업 8 실행 (Thread: pool-3-thread-3)
작업 2 실행 (Thread: pool-3-thread-2)
작업 9 실행 (Thread: pool-3-thread-4)
작업 10() 큐가 꽉 차서 거부됨!
작업 1 실행 (Thread: pool-3-thread-1)
작업 3 실행 (Thread: pool-3-thread-4)
작업 5 실행 (Thread: pool-3-thread-2)
작업 4 실행 (Thread: pool-3-thread-3)
작업 6 실행 (Thread: pool-3-thread-1)
작업 7 실행 (Thread: pool-3-thread-4)
  1. 작업 1, 2 → 코어 스레드(2개)가 바로 실행
  2. 작업 3 ~ 7 → 큐(5개)가 가득 참
  3. 작업 8, 9 → 최대 스레드 확장 (2개 추가되어 총 4개 실행)
  4. 작업 10 → 큐도 꽉 차고, 최대 스레드(4개)도 사용 중이라 거부됨.

최대 스레드 수를 5로 늘리면 거부 메시지가 뜨지 않음.

profile
백엔드 개발자

0개의 댓글