Java 线程池快速入门
一、线程池是哪个类?
java.util.concurrent.ThreadPoolExecutor
这个类。在搞清楚这个类怎么用之前,我们先要过一遍基本概念。
二、线程池基本概念
- 线程池由两个部分组成:池和任务队列。
- 池中有很多线程用于执行任务,而任务队列则是用于缓存将要执行的任务。
- 池中的线程都是并发执行,因此池越大,可以同时处理的任务就越多。
- 当池中的某个线程没有正在执行的任务时,可看作是空闲的。
- 提交的任务会首先交由空闲的线程执行。如果没有空闲线程,任务会先进入任务队列,然后池中的线程会从队列中取任务执行,执行完了取下一个任务,如此反复。
- 空闲的线程在一定条件下可以被销毁,以节省计算机资源。
- 当任务队列已满时,再有新的任务进来,就会要有相应的策略来处理。我们在创建线程池时可以选择需要的策略,后面将进行详述。
初学者可能会觉得这套机制有些复杂,所以最好多看几遍以加深理解。
当创建一个 ThreadPoolExecutor
对象时,其中的池和任务队列都会被创建起来。创建的时候,池和任务队列都会有一些配置项:
类型 | 配置项 |
---|---|
int | corePoolSize |
int | maximumPoolSize |
long | keepAliveTime |
TimeUnit | unit |
BlockingQueue<Runnable> | workQueue |
ThreadFactory | threadFactory |
RejectedExecutionHandler | handler |
这些配置项可以在 ThreadPoolExecutor
的构造方法中找到,如下图所示:
这些参数分为池的配置和队列的配置两方面,下面分别介绍下。
(一)池的配置项
-
maximumPoolSize
指的是池的最大可用线程数。如果池中的线程数达到这个最大值,就不会再增加新的线程了。 -
corePoolSize
可以理解为线程池“想要持有的线程数量”,什么意思呢?- 当实际线程数少于这个值时,凡是有新的任务到来,池都会创建新的线程来执行,不论当前是否存在空闲的线程。
- 当实际线程数多于这个值时,如果一个线程空闲太久,池就会销毁这个线程,直到只剩下这么多线程为止。
-
keepAliveTime
和unit
就是用来指定当一个线程空闲多长时间,池需要检查该不该销毁它。这个配置仅当实际线程数多于corePoolSize
时有效,当实际线程数少于或等于corePoolSize
时,不论线程空闲多长时间都不会被销毁。 -
threadFactory
用来指定一个创建线程的工厂对象。ThreadPoolExecutor
有一个默认的线程工厂,同时允许我们通过这个参数来自己定义如何创建线程。
最佳实践:
首先最大线程数应该是多少?这个取决于任务是 CPU 密集型还是 I/O 密集型。前者主要消耗 CPU,后者主要读写网络或磁盘或其他的流。如果任务是 CPU 密集型,那么线程池的 maximumPoolSize
超过主机 CPU 核数是没有意义的,一般设置为跟核数一样即可。如果任务是 I/O 密集型的,那么线程池可以设置的非常大,一个典型例子是 Tomcat 的线程池配置,设置为几百上千的都有。
其次是是否有必要自定义 threadFactory
参数?答案是有必要,我们需要给每一个线程起名字,这样在运维的时候我们就能直观的看到一个线程是做什么的。下面是一个简单的例子:
/** * 用于创建带名字的线程的线程池 */ public class NamedThreadFactory implements ThreadFactory { private String name; private AtomicInteger counter = new AtomicInteger(); public NamedThreadFactory(String name) { this.name = name; } @Override public Thread newThread(Runnable r) { return new Thread(r, name + "-" + counter.incrementAndGet()); } }
(二)队列的配置项
任务队列是一个 BlockingQueue
对象(我们通常选用 LinkedBlockingQueue
,它的结构使得操作队列头尾的效率最高)。LinkedBlockingQueue
只有一个配置参数,就是队列长度。默认的队列长度为 Integer.MAX_VALUE
。
最后一个配置就是 ThreadPoolExecutor
的 handler
参数,类型是 RejectedExecutionHandler
,其含义是当队列满了(此时池中也必然没有空闲的线程)的时候,对于新的任务该如何处理。Java 默认提供下面几个 RejectedExecutionHandler
的子类:
-
CallerRunsPolicy
:让提交任务的线程自己去执行这个任务,也就意味着这个线程会因此而阻塞。 -
AbortPolicy
:拒绝执行这个任务,并且在提交任务的线程中抛出RejectedExecutionException
异常。 -
DiscardPolicy
:同样拒绝执行这个任务,但是不抛出异常。 -
DiscardOldestPolicy
:从任务队列中把最早加入的任务丢弃,然后把当前任务加进任务队列。
最佳实践:
首先不可使用默认的队列长度。如果这么做的话,在队列满掉之前,主机的内存就已经被撑爆,进程就挂掉了。队列长度直接影响了内存使用,因此要合理安排。
其次该选用哪个 RejectedExecutionHandler
,一般情况下都会选 CallerRunsPolicy
,因为只有它才不会把任务给丢了呀。
三、线程池的使用
(一)创建线程池
有了前面的说明,你应该知道这些参数怎么用了。下面是一个例子:
ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, 10, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(10), new NamedThreadFactory("DEMO"), // 这个类参考上面的代码示例 new CallerRunsPolicy() );
(二)执行任务
只要调用线程池的 execute(Runnable)
方法就能将任务提交给线程池执行。
注意任务内容当中一定要捕获所有的异常,线程池本身可不会对异常做任何处理,包括打印错误日志,这些必须你自己来做。
下面是一个例子:
Runnable task = () -> System.out.println( "Hello from thread " + Thread.currentThread().getName() ); // 向线程池提交 10 个任务 for (int i = 0; i < 10; i++) { executor.execute(task); }
(三)关闭线程池
当应用结束的时候,有两个要素要考虑:
- 线程池队列当中的任务要如何处理;
- 正在执行的那些任务要如何处理。
首先我们在设计上应该保证,队列中未被处理的任务是可以随时丢弃的。下次启动线程池后,这些未处理的任务可以重新再提交给线程池,业务不受影响。
其次对于正在执行的任务,我们应该保证,一旦在执行任务过程中,进程被结束,那么有两种处理策略:
- 所有已经作出的状态变更都会回滚,下次执行这个任务又可以从头开始。例如数据库事务;
- 这个任务可以从中间状态继续执行,比如下载文件中的断点续传。
但不管怎么样,我们希望当进程结束时,应该尽可能等待正在处理的任务执行完成,以减少出错的可能性。ThreadPoolExecutor
提供这样一种方式。下面是一个例子:
// 告诉线程池不再接受新的任务,也不再处理队列中的任务 executor.shutdownNow(); // 等待线程池中正在执行的任务都处理完毕,最多1小时 executor.awaitTermination(1, TimeUnit.HOURS);
以上就是对线程池的介绍。
原文地址:https://segmentfault.com/a/1190000021220164
相关推荐
-
Java中的四种引用以及ReferenceQueue和WeakHashMap的使用示例 Java基础
2019-3-24
-
Spring Boot 多库分布式事务,最简配置,没有之一 Java基础
2020-5-30
-
Java 编程要点之 I/O 流详解 Java基础
2018-3-26
-
1 入门篇!大白话带你认识 Kafka Java基础
2020-6-18
-
java map和reduce Java基础
2020-5-30
-
程序员应该掌握的7个搜索技巧 Java基础
2019-10-2
-
浅谈tcp socket的backlog参数 Java基础
2019-1-24
-
[面试]Java常见面试题,持续更新… Java基础
2020-6-13
-
再一次生产 CPU 高负载排查实践 Java基础
2019-7-4
-
玩转mybatis中的类型转换 Java基础
2020-7-7