English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Java 스레드 풀 상세 설명 및 예제 코드

스레드 풀 기술 배경

객체를 생성하고 소멸하는 것은 대상 프로그래밍에서 매우 시간이 많이 걸리는 작업입니다. 이는 Java에서 더욱 그렇습니다. 가상 머신은 각 객체를 추적하여 객체가 소멸된 후에 쓰레기 수집을 수행하려고 시도합니다.

따라서 서비스 프로그램의 효율성을 높이는 한 가지 방법은 가능한 한 많은 객체를 생성하고 소멸하는 횟수를 줄이는 것입니다. 특히 많은 자원을 소모하는 객체의 생성과 소멸입니다. 어떻게既存 객체를 사용하여 서비스를 제공하는지는 해결해야 할 중요한 문제입니다. 이것이 바로 '풀화 자원' 기술이 등장하게 된 원인입니다.

예를 들어, Android에서 흔히 볼 수 있는 많은 일반 구성 요소는 일반적으로 '스레드' 개념이 빠지지 않습니다. 예를 들어, 다양한 이미지 로드 라이브러리, 네트워크 요청 라이브러리, Android의 메시지 전달 메커니즘에서 Message가 Message.obtain()를 사용할 때도 Message 풀의 객체를 사용합니다. 따라서 이 개념은 매우 중요합니다. 이 문서에서 소개할 스레드 풀 기술도 이 가상과 일치합니다.

스레드 풀의 장점:

1. 스레드 풀 내의 스레드를 재사용하여 객체 생성, 소멸로 인한 성능 비용을 줄입니다;

2. 스레드의 최대 동시 수를 효과적으로 제어하여 시스템 자원 이용률을 높이고, 과도한 자원 경쟁을 피하며, 블록을 피합니다;

3. 다중 스레드로 간단한 관리를 할 수 있으며, 스레드 사용을 간단하고 효율적이게 합니다.

스레드 풀 프레임워크 Executor

java에서의 스레드 풀은 Executor 프레임워크를 통해 구현됩니다. Executor 프레임워크는 Executor, Executors, ExecutorService, ThreadPoolExecutor, Callable 및 Future, FutureTask의 사용 등을 포함합니다.

Executor: 모든 스레드 풀의 인터페이스, 하나의 메서드만 있습니다.

public interface Executor {  
 void execute(Runnable command);  
}

ExecutorService: Executor의 행동을 추가하여 Executor 구현 클래스의 가장 직접적인 인터페이스입니다.

Executors: 스레드 풀을 생성하기 위한 일련의 팩토리 메서드를 제공합니다. 반환된 스레드 풀은 ExecutorService 인터페이스를 구현합니다.

ThreadPoolExecutor: 스레드 풀의 구체적인 구현 클래스, 일반적으로 사용되는 모든 스레드 풀은 이 클래스를 기반으로 구현됩니다. 생성자는 다음과 같습니다:

public ThreadPoolExecutor(int corePoolSize,
        int maximumPoolSize,
        long keepAliveTime,
        TimeUnit unit,
        BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}

corePoolSize: 스레드 풀의 기본 스레드 수입니다. 스레드 풀에서 실행되는 스레드 수는 항상 corePoolSize를 초과하지 않습니다. 기본적으로 keepAliveTime를 설정하여 일관되게 살아 있을 수 있습니다. allowCoreThreadTimeOut를 True로 설정하면, 기본 스레드 수는 0이며, 이 경우 keepAliveTime는 모든 스레드의 타임아웃 시간을 제어합니다.

maximumPoolSize: 스레드 풀이 허용하는 최대 스레드 수입니다;

keepAliveTime: 비활성 스레드가 종료되는 타임아웃 시간을 의미합니다;

unit: keepAliveTime의 단위를 나타내는 enum입니다;

workQueue: 작업을 저장하는 BlockingQueue<Runnable> 퀼을 나타냅니다.

BlockingQueue: 블록큐(BlockingQueue)는 java.util.concurrent 하에 주로 스레드 동기화를 제어하는 도구입니다. BlockQueue가 비어있으면, BlockingQueue에서 아이템을 꺼내는 작업은 대기 상태로 차단됩니다. 그리고 BlockingQueue에 아이템이 추가되면 깨어나 작업을 계속합니다. 마찬가지로, BlockingQueue가 가득 차면, 그 안에 아이템을 추가하려는 모든 작업도 대기 상태로 차단됩니다. 그리고 BlockingQueue에 공간이 생기면 깨어나 작업을 계속합니다. 블록큐는 일반적으로 생산자와 소비자의 상황에서 사용됩니다. 생산자는队列에 요소를 추가하는 스레드이고, 소비자는队列에서 요소를 꺼내는 스레드입니다. 블록큐는 생산자가 요소를 저장하는 컨테이너이고, 소비자는 컨테이너에서 요소를 꺼냅니다. 구현 클래스에는 LinkedBlockingQueue, ArrayBlockingQueued 등이 있습니다. 일반적으로 내부는 Lock과 Condition(명시적 락(Lock) 및 Condition의 학습과 사용)을 통해 블록과 깨우기를 구현합니다.

线程 풀의 작업 과정은 다음과 같습니다:

线程 풀이 최초로 생성될 때, 그 안에는 어떤线程도 없습니다. 큐는 매개변수로 전달됩니다. 그러나 큐에 작업이 있더라도,线程 풀은 즉시 실행하지 않습니다.

execute() 메서드를 호출하여 작업을 추가할 때,线程 풀은 다음과 같은 판단을 합니다:

실행 중인 스레드 수가 corePoolSize보다 적다면, 즉시 스레드를 생성하여 이 작업을 실행합니다;

실행 중인 스레드 수가 corePoolSize보다 크거나 같다면, 이 작업을 큐에 넣습니다;

이때 큐가 가득 차 있고 실행 중인 스레드 수가 maximumPoolSize보다 적다면, 여전히 비핵심 스레드를 생성하여 이 작업을 즉시 실행해야 합니다;

큐가 가득 차 있고, 실행 중인 스레드 수가 maximumPoolSize보다 크거나 같다면, 스레드 풀이 RejectExecutionException를 투げ던집니다.

일반적인线程이 작업을 완료하면, 그는 큐에서 다음 작업을 가져와 실행합니다.

일반적인线程이 일할 것이 없을 때, 특정 시간(keepAliveTime)이 지나면,线程 풀은 판단하고, 현재 실행 중인线程 수가 corePoolSize보다 크다면, 이线程은 중지됩니다. 따라서 모든 작업이 완료되면,线程 풀은 최종적으로 corePoolSize 크기로 축소됩니다.

线程 풀의 생성 및 사용

线程 풀 생성 및 사용 방법을 설명하는 도구 클래스 Executors의 정적 메서드를 사용하여 생성된线程 풀이며, 다음은 일반적인线程 풀 중 몇 가지입니다.

SingleThreadExecutor: 단일 배경线程 (그 버퍼 큐는 무한대입니다)

public static ExecutorService newSingleThreadExecutor() {  
 return new FinalizableDelegatedExecutorService (
  new ThreadPoolExecutor(1, 1,         
  0L, TimeUnit.MILLISECONDS,         
  new LinkedBlockingQueue<Runnable>())); 
}

단一线 성线程 풀을 생성합니다. 이 풀은 하나의 핵심线程만 작업을 수행하며, 모든 작업이 단一线 성 순차적으로 실행되는 것과 같습니다. 이 유일한线程이 예외로 끝나면 새로운线程이 그 대신에 들어옵니다. 이 풀은 모든 작업이 제출된 순서대로 실행되도록 보장합니다.

FixedThreadPool:기본적인 스레드만을 가진 스레드 풀로, 크기가 고정되어 있습니다 (그 버퍼 큐는 무한입니다).

public static ExecutorService newFixedThreadPool(int nThreads) {        
        return new ThreadPoolExecutor(nThreads, nThreads,                                      
            0L, TimeUnit.MILLISECONDS,                                        
            new LinkedBlockingQueue<Runnable>());    
}
고정 크기의 스레드 풀을 생성합니다. 작업을 제출할 때마다 스레드를 생성하여 작업을 처리합니다. 스레드 풀의 크기가 최대 크기에 도달하면 크기가 유지되며, 어떤 스레드가 실행 중에 예외가 발생하여 종료되면 스레드 풀이 새로운 스레드를 추가하여 보충합니다.

CachedThreadPool:무한 스레드 풀로, 자동 스레드 회수가 가능합니다.

public static ExecutorService newCachedThreadPool() {   
 return new ThreadPoolExecutor(0, Integer.MAX_VALUE,           
   60L, TimeUnit.SECONDS,          
   new SynchronousQueue<Runnable>());  
}

스레드 풀의 크기가 작업 처리에 필요한 스레드 크기를 초과하면, 일부 비활성화된 스레드를 회수합니다 (60초 동안 작업을 수행하지 않는 스레드가 있을 때, 작업 수가 증가하면 이 스레드 풀이 스마트하게 새로운 스레드를 추가하여 작업을 처리할 수 있습니다. 이 스레드 풀이 스레드 풀 크기를 제한하지 않으며, 스레드 풀 크기는 운영 체제(또는 JVM)가 생성할 수 있는 최대 스레드 크기에 따라 완전히 의존합니다. SynchronousQueue는 버퍼가1의 블록 큐.

ScheduledThreadPool:기본적인 스레드 풀로, 크기는 무한입니다. 이 스레드 풀은 타이밍 및 주기적인 작업 실행 요구를 지원합니다.

public static ExecutorService newScheduledThreadPool(int corePoolSize) {   
 return new ScheduledThreadPool(corePoolSize, 
    Integer.MAX_VALUE,             
    DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,             
    new DelayedWorkQueue()); 
}

주기적으로 작업을 수행하는 스레드 풀을 생성합니다. 비활성 상태에서는 비핵심 스레드 풀이 DEFAULT_KEEPALIVEMILLIS 시간 내에 회수됩니다.

스레드 풀에서 가장 많이 사용되는 작업 제출 메서드는 두 가지가 있습니다:

execute:

ExecutorService.execute(Runnable runable);

submit:

FutureTask task = ExecutorService.submit(Runnable runnable);
FutureTask<T> task = ExecutorService.submit(Runnable runnable, T Result);

FutureTask<T> task = ExecutorService.submit(Callable<T> callable);

submit(Callable callable) 구현, submit(Runnable runnable)와 동일합니다.

public <T> Future<T> submit(Callable<T> task) {
 if (task == null) throw new NullPointerException();
 FutureTask<T> ftask = newTaskFor(task);
 execute(ftask);
 return ftask;
}

submit이 실행되면 결과를 반환하는 작업이 시작되며, FutureTask 객체를 반환합니다. 이렇게 get() 메서드를 통해 결과를 얻을 수 있습니다. submit은 최종적으로도 execute(Runnable runable)을 호출합니다. submit은 Callable 객체나 Runnable을 FutureTask 객체로 포장하는 것뿐입니다. FutureTask는 Runnable이기 때문에 execute에서 실행할 수 있습니다. Callable 객체와 Runnable이 어떻게 FutureTask 객체로 포장되는지는 Callable과 Future, FutureTask 사용법을 참조하세요.

스레드 풀 구현 원리

만약 스레드 풀 사용에 대해만 이야기한다면, 이 블로그는 큰 가치가 없습니다. 그저 Executor 관련 API를 익히는 과정일 뿐입니다. 스레드 풀 구현 과정에서는 Synchronized 키워드를 사용하지 않고, 대신 Volatile, Lock, 동기(차단) 큐, Atomic 관련 클래스, FutureTask 등을 사용합니다. 이는 후자의 성능이 더 좋기 때문입니다. 이해 과정을 통해 소스 코드 중 병합 제어의 사고를 잘 배울 수 있습니다.

먼저 언급했듯이 스레드 풀의 장점은 다음 세 가지로 요약할 수 있습니다:

스레드 재사용

최대 동시 수행 수 제어

스레드 관리

1. 스레드 재사용 과정

스레드 재사용 원리를 이해하려면 먼저 스레드 생명주기를 이해해야 합니다.

스레드의 생명주기는 생성(New), 대기(Runnable), 실행(Running), 블록(Blocked) 및 사망(Dead)으로 이루어져 있습니다.5상태.

Thread는 new를 통해 새로운 스레드를 생성합니다. 이 과정은 스레드 정보를 초기화하는 것으로, 스레드 이름, ID, 스레드가 속한 그룹 등을 의미합니다. 이는 단순한 객체로 생각할 수 있습니다. Thread의 start()를 호출하면 Java 가상 maszyna를 호출하며 메서드 호출 스택과 프로그램 계산기를 생성하고, hasBeenStarted를 true로 설정합니다. 이후 start 메서드를 호출하면 예외가 발생합니다.

이 상태에 있는线程은 아직 실행을 시작하지 않았습니다. 그저 이线程이 실행할 수 있다는 것을 의미합니다. 이线程이 언제 실행되는지는 JVM의 스케줄러에 따릅니다.线程이 CPU를 얻으면 run() 메서드가 호출됩니다. Thread의 run() 메서드를 직접 호출하지 마세요. 그런 다음 CPU의 스케줄링에 따라 대기 - 실행 - 블록 사이에서 전환하며, run() 메서드가 끝나거나 다른 방식으로 스레드가 중지되면 dead 상태로 이동합니다.

따라서线程 재사용의 원리는线程이 살아있는 상태를 유지해야 한다는 것입니다. (대기, 실행 또는 블록). 다음으로 ThreadPoolExecutor가 어떻게线程 재사용을 구현하는지 보겠습니다.

ThreadPoolExecutor의 주요 Worker 클래스에서线程 재사용을 제어합니다. Worker 클래스의 간단한 코드를 보면 이해하기 쉽습니다:

private final class Worker implements Runnable {
final Thread thread;
Runnable firstTask;
Worker(Runnable firstTask) {
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Runnable task = w.firstTask;
w.firstTask = null;
while (task != null || (task = getTask()) != null){
task.run();
}
}

Worker는 Runnable이며, 하나의 thread를 가지고 있으며, 이 thread는 생성할 스레드입니다. Worker 객체를 생성할 때, Thread 객체를 동시에 생성하고, Worker 자신을 TThread에 전달합니다. 이렇게 Thread의 start() 메서드가 호출될 때, 실행되는 것은 Worker의 run() 메서드입니다. 그리고 runWorker()로 이동하면, getTask()에서 Runnable 객체를 얻고, 순서대로 실행합니다. getTask()가Runnable 객체를 얻는 방법은 무엇인가요?

여전히 간소화된 코드입니다:

private Runnable getTask() {
 if(특정 상황) {
  return null;
 }
Runnable r = workQueue.take();
return r;
}

이 workQueue는 ThreadPoolExecutor를 초기화할 때 저장하는 BlockingQueue 큐입니다. 이 큐에 저장된 것들은 모두 실행될Runnable 작업입니다. BlockingQueue는 블록 큐이므로, BlockingQueue.take()가 비어 있으면, 새로운 객체가 추가될 때까지 블록된 스레드를 깨우는까지 대기합니다. 따라서 일반적으로 Thread의 run() 메서드는 종료되지 않고, workQueue에서Runnable 작업을 계속 수행하여 스레드 재사용 원리를 달성합니다.

2. 최대 병행 수 제어

Runnable이 언제 workQueue에 넣히는지, Worker가 언제 생성되고, Worker 내의 Thread가 언제 start()를 호출하여 새로운 스레드를 시작하고 Worker의 run() 메서드를 실행하는지 알고 싶습니다. 위의 분석에서 Worker 내의 runWorker()가 작업을 수행할 때는 하나씩, 일렬로 수행된다는 것을 알 수 있습니다. 그렇다면 병행은 어떻게 표현되는지 알고 싶습니다.

쉽게 생각하면 execute(Runnable runnable)에서 위의 몇 가지 작업을 수행하는 것입니다. execute 내에서 어떻게 수행하는지 보겠습니다.

execute:

간단화된 코드

public void execute(Runnable command) {
 if (command == null)
  throw new NullPointerException();
int c = ctl.get();
// 현재 스레드 수 < corePoolSize
if (workerCountOf(c) < corePoolSize) {
// 직접 새로운 스레드를 시작합니다.
if (addWorker(command, true))
return;
c = ctl.get();
}
// 활동 스레드 수 >= corePoolSize
// RUNNING 상태이고 큐가 비어 있지 않다면
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// RUNNING 상태가 되지 않았는지 다시 확인합니다
// . NONRUNNING 상태에서 작업을 workQueue에서 제거하고 거부합니다
if (!isRunning(recheck) && remove(command))
reject(command);// 스레드 풀이 지정한 전략을 사용하여 작업을 거부합니다
// 두 가지 상황:
// 1. NONRUNNING 상태에서 새로운 작업을 거부합니다
// 2. 큐가 가득 차 있고 새로운 스레드를 시작하는 데 실패함(workCount > maximumPoolSize)
} else if (!addWorker(command, false))
reject(command);
}

addWorker:

간단화된 코드

private boolean addWorker(Runnable firstTask, boolean core) {
int wc = workerCountOf(c);
if (wc >= (core63; corePoolSize : maximumPoolSize)) {
return false;
}
w = new Worker(firstTask);
final Thread t = w.thread;
t.start();
}

코드를 보고 위에 언급한 스레드 풀 작업 과정에서 작업 추가에 대해 보겠습니다:

* 실행 중인 스레드 수가 corePoolSize보다 적다면, 즉시 스레드를 생성하여 이 작업을 실행합니다;  
* 실행 중인 스레드 수가 corePoolSize보다 크거나 같다면, 이 작업을 큐에 넣습니다;
* 이때 큐가 가득 차 있고 실행 중인 스레드 수가 maximumPoolSize보다 적다면, 여전히 비핵심 스레드를 생성하여 이 작업을 즉시 실행해야 합니다;
* 큐가 가득 차 있고, 실행 중인 스레드 수가 maximumPoolSize보다 크거나 같다면, 스레드 풀이 RejectExecutionException를 투げ던집니다.

이것이 Android의 AsyncTask가 최대 작업 수를 초과할 때 RejectExecutionException를 투げ던지는 이유입니다. 자세한 내용은 최신 버전의 AsyncTask 소스 코드 해석 및 AsyncTask의 암흑면을 참조하세요.

addWorker를 통해 새로운 스레드가 성공적으로 생성되면, start()를 통해 새로운 스레드를 시작하고, firstTask를 이 Worker의 run()에서 수행할 첫 번째 작업으로 설정합니다.

각 Worker의 작업은 시리얼 처리되지만, 여러 Worker가 생성되면 공통된 workQueue를 공유하기 때문에 병행적으로 처리됩니다.

따라서 corePoolSize와 maximumPoolSize를 통해 최대 동시 수를 제어합니다. 대략적인 과정은 다음 그림으로 표현할 수 있습니다.

위 설명과 그림을 통해 이 과정을 잘 이해할 수 있습니다.

Android 개발을 하고 있으며, Handler 원리에 대한 이해가 깊다면, 이 그림이 익숙해 보일 수 있습니다. 일부 과정이 Handler, Looper, Message 사용 시와 유사합니다. Handler.send(Message)는 execute(Runnable)와 같으며, Looper가 유지하는 Message 큐는 BlockingQueue와 같지만, 이 큐를 동기화하여 유지해야 하며, Looper의 loop() 함수는 반복적으로 Message 큐에서 Message를 가져오고, Worker의 runWork()는 BlockingQueue에서 Runnable을 반복적으로 가져오는 것과 같은 원리입니다.

3. 스레드 관리

스레드 풀을 통해 스레드의 재사용을 잘 관리하고, 동시 수를 제어하고, 파괴 등의 과정을 관리할 수 있습니다. 스레드의 재사용과 동시 수 제어에 대해 이미 설명했지만, 스레드 관리 과정은 그 안에 포함되어 있으며, 이해하기 쉽습니다.

ThreadPoolExecutor에 ctl의 AtomicInteger 변수가 있습니다. 이 하나의 변수를 통해 두 가지 내용을 저장합니다:

모든 스레드의 수 각 스레드가 있는 상태 중 낮은29비트 저장 스레드 수, 높은3비트 저장 runState, 비트 연산을 통해 다른 값을 얻습니다.

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//线程의 상태를 얻습니다
private static int runStateOf(int c) {
return c & ~CAPACITY;
}
//Worker의 수를 얻습니다
private static int workerCountOf(int c) {
return c & CAPACITY;
}
// 스레드가 실행 중인지 확인
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}

이곳에서 shutdown과 shutdownNow()을 통해 스레드 풀의 종료 과정을 분석합니다. 스레드 풀은 작업 추가와 실행을 제어하기 위해 다섯 가지 상태를 가지고 있습니다. 주로 다음 세 가지를 소개합니다:

RUNNING 상태: 스레드 풀이 정상적으로 작동하며, 새로운 작업을 받고 큐에 있는 작업을 처리할 수 있습니다;

SHUTDOWN 상태: 새로운 작업을 받지 않지만, 큐에 있는 작업을 실행합니다;

STOP 상태: 새로운 작업을 받지 않으며, 큐에 있는 작업을 처리하지 않습니다. shutdown 메서드는 runState를 SHUTDOWN으로 설정합니다. 모든 available 스레드를 종료하며, 일어나고 있는 스레드는 영향을 받지 않으며, 큐에 있는 작업은 실행됩니다.

shutdownNow 메서드는 runState를 STOP으로 설정합니다. shutdown 메서드와의 차이점은, 이 메서드는 모든 스레드를 종료하기 때문에 큐에 있는 작업도 실행되지 않습니다.

정리
ThreadPoolExecutor 소스 코드 분석을 통해 스레드 풀의 생성, 작업 추가, 실행 등 과정을 전반적으로 이해했으며, 이 과정을 이해하면 스레드 풀 사용이 더 쉬워질 것입니다.

그리고 이를 통해 배운 동기화 제어 및 생산자-소비자 모델 작업 처리는 미래에 다른 문제를 이해하거나 해결하는 데 큰 도움이 될 것입니다. 예를 들어, Android의 Handler 메커니즘에서Looper의 Messager 큐를 BlookQueue로 처리하는 것도 가능합니다. 이것들은 소스 코드를 읽는 이득입니다.

이상은 Java 스레드 풀의 자료 정리입니다. 앞으로도 관련 자료를 계속 보충하겠습니다. 여러분의 사이트 지원에 감사합니다!

너도 좋아할 만한 것