스레드 풀 사용법
1. 스레드 풀이란 무엇인가
풀: 컨테이너
스레드 풀: 스레드를 저장하는 컨테이너
여기서 누군가 질문할 수 있습니다. 우리가 스레드를 생성해서 작업을 실행하고 작업이 끝나면 스스로 사라지지 않아도 될까요? 왜 컨테이너에 저장해야 할까요?
기억하세요: 스레드 풀은 스레드 재사용 기술입니다. 재사용: 반복 사용
스레드 풀 기술은 스레드를 반복적으로 사용할 수 있는 기술이라고 생각할 수 있습니다.
왜 이 스레드를 반복 사용해야 할까요?
스레드를 생성할 때 실제로 자원 소모가 상당히 큽니다. 스레드를 무제한으로 생성하면 CPU에 영향을 미칩니다.
스레드 풀은 어떤 이론일까요?
우리가 스레드가 필요할 때 미리 스레드를 생성합니다. 작업이 완료되면 컨테이너에 저장합니다. 만약 스레드로 실행해야 할 작업이 있다면 컨테이너에서 미리 생성된 스레드를 꺼내 작업을 실행하면 됩니다. 이렇게 하면 스레드 생성 단계를 생략하여 자원 소모를 줄일 수 있습니다.
2. 스레드 풀의 기본 사용법
2.1. newCachedThreadPool로 스레드 풀 생성하기
이 유형의 스레드 풀은 스레드의 핵심 매개변수를 가지고 있지 않습니다.
작업이 완료되지 않은 상태에서 새 작업이 들어올 때마다 새 스레드를 생성합니다. 이론적으로 스레드 수에는 상한이 없습니다.
실제로 이 방식으로 스레드 풀을 생성할 때도 스레드 수에는 상한이 있습니다.
왜냐하면 스레드 풀에서는 스레드를 나타내기 위해 총 4바이트를 사용하는데, 그중 앞의 3비트는 스레드 풀의 상태를 나타내고, 뒤의 29비트는 스레드의 개수를 나타냅니다. 따라서 이 방식으로 스레드를 생성할 때 최대 스레드 수는 2의 29제곱 - 1개입니다.
이전 작업이 완료되면 스레드를 재사용할 수도 있습니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class ThreadPoolDemo {
public static void main(String[] args) {
testThreadPoolCreation();
}
/**
* 스레드 풀 생성 방법
*/
private static void testThreadPoolCreation() {
// 스레드 풀 객체 생성
ExecutorService threadPool = Executors.newCachedThreadPool(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
});
// 스레드 풀에 작업 추가
for (int i = 1; i <= 10; i++) {
threadPool.submit(new TaskWorker(i));
}
}
/**
* 실행할 작업 정의
*/
static class TaskWorker implements Runnable {
private int taskNumber;
public TaskWorker(int taskNumber) {
this.taskNumber = taskNumber;
}
@Override
public void run() {
// 스레드 이름 가져오기
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "----작업 번호 " + this.taskNumber + " 실행 중");
}
}
}
2.2. newFixedThreadPool로 스레드 풀 생성하기
지정된 수의 스레드를 생성하는 스레드 풀입니다.
예를 들어, 스레드 수를 3으로 지정하고 10개의 작업을 제공하면, 이는 전형적인 스레드 재사용을 통해 효율성을 높이는 문제입니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class FixedThreadPoolDemo {
public static void main(String[] args) {
createFixedThreadPool();
}
/**
* 고정 크기 스레드 풀 생성
*/
private static void createFixedThreadPool() {
ExecutorService threadPool = Executors.newFixedThreadPool(3, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
});
// 스레드 풀에 작업 추가
for (int i = 1; i <= 10; i++) {
threadPool.submit(new JobTask(i));
}
}
/**
* 작업 클래스 정의
*/
static class JobTask implements Runnable {
private int taskId;
public JobTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "----작업 ID " + this.taskId + " 실행 중");
}
}
}
2.3. newSingleThreadExecutor로 스레드 풀 생성하기
하나의 스레드만 가지는 스레드 풀을 생성합니다.
작업 수가 적을 때 자원 사용량이 상대적으로 큽니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class SingleThreadExecutorDemo {
public static void main(String[] args) {
createSingleThreadExecutor();
}
/**
* 단일 스레드 실행자 생성
*/
private static void createSingleThreadExecutor() {
// 스레드 풀 생성
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
});
// 작업 제출
for (int i = 1; i <= 10; i++) {
executor.submit(new WorkItem(i));
}
}
/**
* 작업 항목 클래스
*/
static class WorkItem implements Runnable {
private int itemNumber;
public WorkItem(int itemNumber) {
this.itemNumber = itemNumber;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "----작품 번호 " + this.itemNumber + " 처리 중");
}
}
}
2.4. 원본 ThreadPoolExecutor로 스레드 생성하기
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolDemo {
public static void main(String[] args) {
/**
* 첫 번째 매개변수: 핵심 스레드 수
* 두 번째 매개변수: 최대 스레드 수
* 세 번째 매개변수: 유휴 시간
* 네 번째 매개변수: 시간 단위
* 다섯 번째 매개변수: 큐
* 여섯 번째 매개변수: 스레드 팩토리
* 일곱 번째 매개변수: 거부 정책
*/
ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
5,
5,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1));
// 스레드 풀에 작업 추가
for (int i = 1; i <= 6; i++) {
customThreadPool.execute(new ProcessingTask(i));
}
}
/**
* 처리 작업 클래스
*/
static class ProcessingTask implements Runnable {
private int taskNum;
public ProcessingTask(int taskNum) {
this.taskNum = taskNum;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "----작업 번호 " + this.taskNum + " 처리 중");
}
}
}
2.5. 스레드 풀 종료 문제
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class ThreadPoolShutdownDemo {
public static void main(String[] args) {
testShutdown();
}
/**
* 스레드 풀 종료 테스트
*/
private static void testShutdown() {
// 스레드 풀 객체 생성
ExecutorService service = Executors.newSingleThreadExecutor();
// 스레드 풀에 작업 추가
for (int i = 1; i <= 10; i++) {
service.submit(new UnitOfWork(i));
}
// 새로운 작업을 더 이상 받지 않고, 이미 제출된 작업은 정상적으로 실행 완료
// service.shutdown();
// service.submit(new UnitOfWork(99));
// shutdownNow: 이미 제출된 실행 중인 작업은 계속 실행하고, 실행되지 않은 작업은 즉시 중단
List<Runnable> pendingTasks = service.shutdownNow();
System.out.println("실행되지 않은 작업 수: " + pendingTasks.size());
}
/**
* 작업 단위 클래스
*/
static class UnitOfWork implements Runnable {
private int number;
public UnitOfWork(int number) {
this.number = number;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "----작업 번호 " + this.number + " 실행 중");
}
@Override
public String toString() {
return "UnitOfWork{" +
"number=" + number +
'}';
}
}
}
2.6. 거부 정책 및 스레드 팩토리 사용
일반적인 스레드 풀 거부 정책에는 다음과 같은 것들이 있습니다:
1. AbortPolicy(기본 정책): RejectedExecutionException 예외를 발생시켜 호출자에게 작업이 거부되었음을 알립니다.
2. CallerRunsPolicy: 호출 스레드(작업을 제출한 스레드)가 이 작업을 실행합니다. 이렇게 하면 새 작업 제출 속도를 늦출 수 있지만 작업이 버려지지 않을 수 있습니다.
3. DiscardPolicy: 새 작업을 조용히 버리며 아무 처리도 하지 않습니다.
4. DiscardOldestPolicy: 큐에서 가장 오래된 작업을 버리고 새 작업을 다시 실행하려고 시도합니다.
import java.util.concurrent.*;
public class ThreadPoolPolicyDemo {
public static void main(String[] args) {
testThreadFactory();
testRejectionPolicy();
}
/**
* 스레드 팩토리 테스트
*/
private static void testThreadFactory() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10,
10,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(8192),
new ThreadFactory() {
public Thread newThread(Runnable r) {
return new Thread(r);
}
},
new ThreadPoolExecutor.AbortPolicy()
);
executor.execute(new Runnable() {
public void run() {
System.out.println("스레드 팩토리 테스트 중...");
}
});
}
/**
* 거부 정책 테스트
*/
private static void testRejectionPolicy() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1,
1,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(1),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 큐가 가득 찬 상태에서 작업 추가 시 거부 정책 테스트
for (int i = 1; i <= 5; i++) {
executor.submit(new PolicyTestTask(i));
}
}
/**
* 정책 테스트 작업 클래스
*/
static class PolicyTestTask implements Runnable {
private int taskNum;
public PolicyTestTask(int taskNum) {
this.taskNum = taskNum;
}
@Override
public void run() {
try {
Thread.sleep(1000); // 작업 처리 시간 시뮬레이션
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("작업 " + taskNum + " 완료");
}
}
}
3. 스케줄 작업을 위한 스레드 풀 생성
스케줄 작업이란 무엇인가?
반복 작업, 정시 작업, 지연 작업을 통칭하여 스케줄 작업이라고 합니다.
3.1. newScheduledThreadPool로 스레드 풀 생성
3.1.1. 지연 작업(schedule)
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledDelayTaskDemo {
public static void main(String[] args) {
// 스레드 풀 생성
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
// 지연 작업 실행
scheduler.schedule(new DelayedTask(), 20, TimeUnit.SECONDS);
}
/**
* 지연 작업 정의
*/
static class DelayedTask implements Runnable {
@Override
public void run() {
System.out.println("지연 작업 실행됨!!!!");
}
}
}
3.1.2. 반복 작업 실행(scheduleAtFixedRate)
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledRepeatingTaskDemo {
public static void main(String[] args) {
// 스레드 풀 생성
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
// 반복 작업 실행
scheduler.scheduleAtFixedRate(new RepeatingTask(), 1, 3, TimeUnit.SECONDS);
}
/**
* 반복 작업 정의
*/
static class RepeatingTask implements Runnable {
@Override
public void run() {
System.out.println("반복 작업 실행됨!!!!");
}
}
}
3.1.3. 지연 및 반복 작업 기능(scheduleWithFixedDelay)
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledDelayedRepeatingTaskDemo {
public static void main(String[] args) {
// 스레드 풀 생성
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
// 지연 및 반복 작업 실행
scheduler.scheduleWithFixedDelay(new DelayedRepeatingTask(), 1, 3, TimeUnit.SECONDS);
}
/**
* 지연 반복 작업 정의
*/
static class DelayedRepeatingTask implements Runnable {
@Override
public void run() {
System.out.println("지연 반복 작업 실행됨!!!!");
}
}
}
4. 비동기 계산 결과
어떤 작업이 비동기 계산 결과를 가질 수 있나요?
Callable만 가능합니다.
import java.util.concurrent.*;
public class AsyncResultDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 단일 스레드 스레드 풀 생성
ExecutorService executor = Executors.newSingleThreadExecutor();
// 반환 결과를 받을 수 있음
Future<Integer> result = executor.submit(new CalculationTask(1, 2));
// 데이터 가져오기
// result.isDone() 실행 완료 여부 확인
// result.isCancelled() 실행 취소 여부 확인
// 아래 get 메소드는 프로그램을 차단하며 비동기 결과가 반환될 때까지 대기
Integer value = result.get();
System.out.println("계산 완료: " + value);
}
static class CalculationTask implements Callable<Integer> {
private Integer num1;
private Integer num2;
public CalculationTask(Integer num1, Integer num2) {
this.num1 = num1;
this.num2 = num2;
}
@Override
public Integer call() throws Exception {
return (num1 + num2) * 10;
}
}
}