导言
在并发编程中,线程池是一个非常重要的概念。使用线程池可以有效地管理和控制线程的数量,避免过多的线程消耗系统资源。
线程池流程
参数介绍
必须了解
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
-
corePoolSize(核心线程数):
:指定线程池中保持存活的基本线程数。即使这些线程处于空闲状态,它们也不会被回收。当有任务提交时,核心线程数会按需自动创建。如果设置了allowCoreThreadTimeOut(允许核心线程超时),那么核心线程会超时并且在超时后被终止。 -
maximumPoolSize(最大线程数)
:表示线程池中允许存在的最大线程数。在核心线程数已满并且任务队列已满时,线程池会根据需要创建新的线程,直到达到最大线程数。 -
keepAliveTime(线程空闲时间)
:非核心线程的闲置超时时间,当线程池中线程数量超过corePoolSize的时候,多出来的线程在空闲状态下的存活时间,超过这个时间就会被终止。 -
unit(时间单位)
:用于指定线程空闲时间的时间单位,可以是纳秒、毫秒、秒等。 -
workQueue(任务队列)
:用于保存等待执行的任务的阻塞队列。当线程池中的线程已满时,新提交的任务会被放入到任务队列中进行等待,直到有线程空闲时再被取出执行。 -
threadFactory(线程工厂)
:用于创建线程的工厂类,可以自定义线程的命名、优先级等属性。 -
handler(拒绝策略)
:当线程池无法执行新的任务时,用于处理被拒绝的任务的策略。常见的拒绝策略包括抛出异常、丢弃任务、丢弃队列头部任务和由调用线程执行任务等。
线程池主要有以下几种队列
1.无界阻塞队列:LinkedBlockingQueue
基于链表的阻塞队列,同ArrayBlockingQueue类似,其也是一个有界队列,但默认情况下大小为Integer.MAX_VALUE。
LinkedBlockingQueue
是一个用链表实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认容量为 Integer.MAX_VALUE
,所以如果不指定,就等于是无界的。当使用无界队列时,线程池的设置最大线程数就失去了意义,因为当有新任务来的时候,如果线程池的实际线程数没有达到 corePoolSize
,那么新任务会新建一个线程执行任务,而不是放在队列中;如果达到 corePoolSize
,但是没有达到 maximumPoolSize
,那么新任务会新建一个线程执行任务,而不是放在队列中。
2.有界阻塞队列 :ArrayBlockingQueue
基于数组的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。
ArrayBlockingQueue
是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。队列头是在队列中存放时间最长的元素。新元素插入到队列的尾部,队列获取操作则是从队列头开始。
3.优先级阻塞队列 :PriorityBlockingQueue
具有优先级的无界阻塞队列。
PriorityBlockingQueue
是具有优先级的无界阻塞队列。它使用二进制堆实现,可以使用比较器或者元素的自然顺序排序。PriorityBlockingQueue
不允许使用 null
元素。无论何时插入元素,PriorityBlockingQueue
可以按优先级对元素进行排序,越优先的元素排在越前面。如果你试图对队列进行排序,或者对最小元素调用 remove(x)
或者 contains(x)
,那么使用的是按优先级遍历的元素,而不是队列的元素遍历。因为它是无界的,所以添加操作(add、offer、put 方法)
永远不阻塞,而在队列为空时,获取操作(remove、poll、take)
才阻塞。
4.直接提交队列 :SynchronousQueue
一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。
SynchronousQueue
是一个特殊的阻塞队列,它没有任何内部容量,甚至连一个队列的容量都没有。也就是说,该队列在任何时候,甚至在 put
操作后,都不能保证任何元素存在于队列中,因为这些元素可能已经被获取了。它是线程之间进行任务转移的一个载体。静态工厂方法Executors.newCachedThreadPool
使用了这个队列。
拒绝策略(handler)
Java创建线程池的RejectedExecutionHandler接口,提供了4种拒绝策略,当线程池无法接受新的任务时,这些策略可以对新提交的任务进行不同的处理方式。
-
AbortPolicy策略
:这是默认的策略。当提交一个任务被拒绝时,它将抛出一个RejectedExecutionException异常。 -
CallerRunsPolicy策略
:这种策略下,在调用者线程中运行被拒绝的任务,如果执行环境能够处理,则可以在这个策略下得以运行。 -
DiscardPolicy策略
:这种策略默默地丢弃被拒绝的任务,不会有任何抛出异常或其他说明。 -
DiscardOldestPolicy策略
:此策略将丢弃最老的请求,也就是即将被执行的任务,并尝试重新提交当前任务。
四种默认创建线程池的方法
当使用 Java 的 Executor
框架创建线程池时,有四种常见的方法可供选择。这些方法都定义在 java.util.concurrent.Executors
类中。
1.创建固定大小线程池:newFixedThreadPool(int nThreads)
创建一个固定大小的线程池,如果任务数量超过了线程池的大小,那么在队列中等待。这种类型的线程池对于处理大量长周期任务,资源消耗较少的情况下很有用。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
//defaultHandler:当线程池无法处理新提交的任务时,将抛出一个RejectedExecutionException异常。这是默认的拒绝策略,它会立即停止并抛弃新的任务请求。
优点
- 只有核心线程,线程数量固定,执行完立即回收,可以避免线程的过度创建和销毁的开销。
- 可以有效控制并发线程的数量,适用于需要限制并发级别的场景。
缺点
- 线程池的大小是固定的,不适用于那些需要动态调整线程池大小以根据负载情况进行伸缩的场景。
- 由于采用无界队列,可能会导致内存占用增加,特别是在任务提交速度高于线程池工作速度的情况下。
2.创建单线程池:newSingleThreadExecutor()
创建一个单线程的线程池,该线程池只存在一个线程,所以任务是按照先进先出顺序执行。如果这个唯一的线程在异常结束后,会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
优点
- 单线程线程池适用于顺序执行任务的场景,可以保证任务的执行顺序。
- 由于线程池中只有一个线程,避免了并发操作导致的线程安全问题。
缺点
- 线程池中只有一个线程,不适用于需要并发执行多个任务或需要快速处理大量任务的场景。
- 如果任务提交速度过快,可能会导致任务队列无限增长,进而导致内存占用增加。
3.创建缓存线程池:newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的大小超过了处理的需求,那么会自动的回收空闲的线程。然而,如果任务来的速度超过了线程池的处理速度,那么会自动的添加新的线程来处理任务。这种类型的线程池对于处理大量短周期任务的性能优势明显。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
优点
- 可以根据任务负载自动调整线程数,灵活地创建和回收线程。
- 对于短时间内需要处理大量任务的场景,这种线程池可以快速创建线程来处理任务,并在空闲时及时回收线程,节省资源。
缺点
- 如果任务处理速度大于线程创建和回收的速度,可能会导致线程数过多,进而导致资源消耗过高。
SynchronousQueue
是一个无缓冲队列,当有大量任务同时提交时,可能会导致部分任务无法立即执行而被拒绝。
4.创建定时任务线程池:newScheduledThreadPool(int corePoolSize)
一个可定期或者延时执行任务的线程池,初始化时需要指定线程数量,内部使用DelayQueue存储任务。此类型线程池适用于需要定时或者周期性执行任务的场景。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
优点
- 可以根据需要自动创建和回收线程,根据任务的数量和延迟时间灵活地处理任务。
- 适用于需要定时执行任务或者延迟执行任务的场景,例如定时任务调度、周期性任务等。
缺点
- 如果任务数量非常频繁且延迟时间较短,可能会导致线程数过多,进而导致资源消耗过高。
- DelayedWorkQueue 是一个基于优先级队列的阻塞队列,插入和移除操作的性能较低。
对比
手搓线程池
创建自定义线程池涉及到以下几步
- 创建线程池对象,并设定线程池的核心线程数、最大线程数、存活时间等参数。
- 创建工作队列,线程池中的任务都会被放入这个队列中。
- 创建线程工厂,设定创建线程的具体细节,比如线程的名称、优先级等。
- 创建拒绝策略,当工作队列满了,且线程池中的线程数已达最大值时,如何处理新进来的任务。
简单示例
import java.util.concurrent.*;
public class ThreadPoolUtils {
private static ThreadPoolExecutor threadPool;
// 创建线程工厂
private static ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "myThreadPool thread:" + atomicInteger.getAndIncrement());
}
};
static {
// 获取到服务器的cpu个数
int cpuNum = Runtime.getRuntime().availableProcessors();
// 核心线程数=CPU核心数+1,最大线程数=CPU核心数*2+1 这个是有科学依据的哦
threadPool = new ThreadPoolExecutor(
cpuNum+1,// 核心线程数
cpuNum*2+1,// 最大线程数
10,// 线程空闲时间
TimeUnit.SECONDS,// 时间单位
new ArrayBlockingQueue<>(200), // 工作队列
threadFactory , // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
}
/**
* 执行线程
* @param runnable
*/
public static void execute(Runnable runnable){
threadPool.execute(runnable);
}
/**
* 获取线程池的基本大小
* @return
*/
public static int getPoolSize(){
return threadPool.getPoolSize();
}
/**
* 关闭线程池
*/
public static void shutDown(){
threadPool.shutdown();
}
}
注意事项
-
当创建一个线程池时,需要考虑工作队列的大小和线程数的设定。如果设置不当,可能会导致资源浪费或任务执行延迟。
-
对于线程工厂,不仅可以自定义线程的创建,还可以在创建线程时添加一些额外的操作,如记录日志等。
-
在设定拒绝策略时,需要考虑应用的具体需求,选择最适合的策略。
-
在使用线程池时,务必确保所有的任务都能得到正确的执行。当线程池关闭时,一定要等待所有的任务都已经完成后再进行下一步操作。
-
需要合理控制线程池大小,避免因线程过多导致系统压力过大。
-
一般情况下,应优先使用JDK提供的线程池,如:newCachedThreadPool, newFixedThreadPool, newSingleThreadExecutor等。因为手动创建线程池需要考虑更多的因素,比较复杂。(前提是你要了解这些线程池的优劣,不要盲目使用!)