ex). 손님 100명이 있다. 한번에 매장에 10명만 들어갈 수 있다면 손님들에게 쓰레드 풀을 할당하고, 자동으로 알아서 들어갔다 나왔다 설계할 수 있다.
스레드 풀(Thread Pool)은 많은 수의 스레드를 생성 및 소멸시키는 것에 따른 오버헤드를 줄이기 위해 사용되는 기술입니다. 스레드 풀을 이용하면 작업 처리에 사용되는 스레드의 수를 제한하고, 미리 생성된 스레드를 재사용함으로써 시스템의 자원을 효율적으로 관리할 수 있습니다.
Java thread pool은 여러 개의 thread를 만들어 두고 만들어 여러 번 재사용되는 작업자 thread 그룹을 나타낸다.
Thread pool은 고정 크기로 생성된 thread pool에 Runnable object를 주고 실행을 요청하면, thread pool에서 thread를 가져와 실행시킨다.
만약 thread pool에 남아 있는 thread가 없다면, 유휴 thread가 생길 때까지 해당 작업은 실행되지 못하고 대기상태에 있게 된다.
실행 중이던 thread는 작업이 완료되면 다시 thread pool로 돌아오게 되고, 대기 중인 작업이 있는 경우 다시 실행하게 된다.
자바에서는 java.util.concurrent
패키지를 통해 스레드 풀을 쉽게 사용할 수 있습니다. 가장 널리 사용되는 스레드 풀 구현체로는 Executors 클래스
가 있습니다. Executors 클래스
는 다양한 종류의 스레드 풀을 생성하는 정적 메소드
를 제공합니다.
예를 들어, 고정된 수의 스레드를 갖는 스레드 풀을 생성하려면 Executors.newFixedThreadPool(int nThreads)
메소드를 사용할 수 있습니다. 이 메소드는 지정된 수의 스레드를 갖는 스레드 풀을 생성하며, 모든 스레드가 바쁘게 작업을 처리하고 있을 때 추가 작업은 큐에 대기하게 됩니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Worker implements Runnable {
String name;
public Worker(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void run() {
System.out.println(getName() + " started ");
try {
Thread.sleep(1999);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + " finished");
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(new Worker("Worker" + i));
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("모든 작업이 완료되었습니다.");
}
}
실행결과
Worker0 started
Worker1 started
Worker3 started
Worker2 started
Worker4 started
Worker1 finished
Worker0 finished
Worker4 finished
Worker7 started
Worker2 finished
Worker8 started
Worker6 started
Worker5 started
Worker3 finished
Worker9 started
Worker7 finished
Worker6 finished
Worker9 finished
Worker8 finished
Worker5 finished
모든 작업이 완료되었습니다.
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class DownloadWorker implements Runnable {
private String url;
static String[] urls = {
"https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.go2pyBWXRZ6IXN3whxDFtg",
"https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.YQloTWfJRz24Xhq2aVSGgw",
"https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.DwdVMtMaTmOFS_mQebo56w",
"https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.e2pbYnmHT_mRPWZZ3Z511Q",
"https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.p0sB3Ke2Tt64uXFPa1sU5A",
"https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.gYevNY21TIOphjrtV1Bznw"
};
public DownloadWorker(String url) {
this.url = url;
}
@SuppressWarnings("deprecation")
@Override
public void run() {
String fileName = url.substring(url.lastIndexOf("/") + 1);
try (InputStream inputStream = new URL(url).openStream();
FileOutputStream outputStream = new FileOutputStream(fileName)) {
byte[] buffer = new byte[2048];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
inputStream.close();
outputStream.close();
System.out.println(fileName + " 다운로드 완료");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (String url : urls) {
Runnable worker = new DownloadWorker(url);
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
}
}
실행 결과
ocfkrcbb5vui.DwdVMtMaTmOFS_mQebo56w 다운로드 완료
ocfkrcbb5vui.YQloTWfJRz24Xhq2aVSGgw 다운로드 완료
ocfkrcbb5vui.go2pyBWXRZ6IXN3whxDFtg 다운로드 완료
ocfkrcbb5vui.gYevNY21TIOphjrtV1Bznw 다운로드 완료
ocfkrcbb5vui.p0sB3Ke2Tt64uXFPa1sU5A 다운로드 완료
ocfkrcbb5vui.e2pbYnmHT_mRPWZZ3Z511Q 다운로드 완료
스레드 풀의 주요 구성 요소:
1. 스레드 풀 매니저(Thread Pool Manager): 작업 큐에서 대기 중인 작업을 스레드에 할당하고, 스레드 풀의 상태를 관리합니다.
2. 작업 큐(Work Queue): 처리해야 할 작업들을 보관하는 대기열입니다. 스레드 풀에 있는 스레드들은 이 큐에서 작업을 가져와 처리합니다.
3. 워커 스레드(Worker Threads): 실제 작업을 수행하는 스레드들입니다. 미리 생성되어 작업 큐에서 작업을 가져와 처리합니다.
Thread 생성과 삭제에 따른 시간과 resource를 절약할 수 있다.
Deadlock
Thread Leakage
Resource Thrashing
Java에서의 thread는 user thread와 daemon thread로 나뉜다.
User thread는 특별히 daemon thread로 설정하지 않은 thread로서 일반적으로 생성해서 사용되는 thread이다.
JVM은 모든 user thread가 종료될 때까지 프로그램을 실행한다.
Daemon thread는 백그라운드에서 동작하는 thread로, user thread가 종료될 때 자동으로 종료된다.
주로 main thread나 다른 user thread의 보조 역할을 수행하거나, 특정 작업을 주기적으로 처리하는 thread 등에 사용된다.
JVM은 모든 user thread가 종료되면 daemon thread를 강제로 종료합니다.
모든 thread의 생성과 종료를 직접 관리하지 않을 수도 있다.
Deamon thread의 우선 순위는 낮으나 조정 가능하다.
public class ClockApplication {
public static void main(String[] args) {
// 사용자 스레드 생성 및 시작: 시간 표시 기능
Thread userThread = new Thread(new Runnable() {
public void run() {
while (true) {
System.out.println("현재 시간: " + System.currentTimeMillis());
try {
Thread.sleep(1000); // 1초마다 시간 표시
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
userThread.start(); // 사용자 스레드 시작
// 데몬 스레드 생성 및 시작: 로그 기록 기능
Thread daemonThread = new Thread(new Runnable() {
public void run() {
while (true) {
// 현재 시간을 로그 파일에 기록하는 코드 (가상 코드)
logTime(System.currentTimeMillis());
try {
Thread.sleep(5000); // 5초마다 로그 기록
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void logTime(long time) {
// 로그 파일에 시간을 기록하는 로직 (가상 코드)
System.out.println("로그 기록: " + time);
}
});
daemonThread.setDaemon(true); // 이 스레드를 데몬 스레드로 설정
daemonThread.start(); // 데몬 스레드 시작
}
}
이 예시에서 daemonThread
는 setDaemon(true)
메소드 호출을 통해 데몬 스레드
로 설정되었습니다. 이는 해당 스레드가 주 애플리케이션의 생명주기에 영향을 주지 않음을 의미합니다. 사용자가 애플리케이션을 종료하면, userThread
는 종료되며, 이후 JVM
은 daemonThread
가 아직 실행 중이라도 애플리케이션을 종료할 수 있습니다.