在学习线程池之前,如果对Java线程的知识还比较模糊,建议先阅读一下《浅谈Java线程》
1. 线程池简介
(1)什么是线程池
简单来说,线程池就一些线程的集合,通过使用线程池来管理线程。在系统启动时线程池内部就会创建大量空闲的线程,这些线程长期存在,不会随单次任务执行结束而死亡,只会在进入“空闲”状态等待线程池分配任务。
(2)为什么使用线程池
使用线程池最大的作用就是节约系统开销。线程池限制系统线程的数量,让单个线程能反复利用,避免了线程的频繁创建和销毁。
(3)线程池如何工作
当一个任务需要独立线程执行的时候,系统会将任务提交给线程池,由线程池在内部寻找空闲的线程来执行任务,当没有空闲线程的时候会根据线程池自身机制选择创建新的线程执行或将任务加入等待队列,等有空闲线程时再执行。
(4)线程池的组成
一个线程池由线程池管理池、工作线程、任务接口、任务队列四部分内容组成。
- 线程池管理器(ThreadPool):用于创建并管理线程池,功能包括线程池的创建和销毁、添加新任务等。
- 工作线程(PoolWorker):即线程池管理线程,工作线程会长期存活,没有分配任务线程处于空闲状态,线程池接收到任务时会分配给空闲线程,线程执行完毕不会销毁,会再次进入空闲状态。
- 任务接口(Task):每个任务必须实现的接口,以供线程执行任务。它主要规定了任务的入口,任务执行后的收尾工作,任务的执行状态等。
- 任务队列(TaskQueue):当线程池中没有空闲线程时,该队列用于存放等待执行的任务。
2. 创建一个线程池
(1)了解java.util.concurrent.ThreadPoolExecutor
一个线程池实际就是实现了ExecutorService接口的一个底层实现类。通常我们可以用JDK的java.util.concurrent.ThreadPoolExecutor自定义创建一个线程池。ThreadPoolExecutor的完整构造方法是:ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)。
在创建一个线程池之前,我们必须了解构造方法的每一个参数:
参数 | 作用 |
---|---|
corePoolSize | 池中所保存的核心线程数 |
maximumPoolSize | 池中允许的最大线程数,包括多余空闲线程 |
keepAliveTime | 当线程数大于核心时,该参数为多余空闲线程销毁前等待的最大时长。已创建的核心线程不会被销毁 |
unit | keepAliveTime参数的时间单位 |
workQueue | 待执行任务队列,当任务提交至线程池且未被执行时会进入等待队列等待线程池分配线程执行。它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种。不能为空,否则抛出空指针异常 |
threadFactory | 线程池创建线程的工厂,为ThreadFactory接口子类实例。不能为空,否则抛出空指针异常 |
handler | 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序,简单来说就是任务来不及处理时的拒绝任务策略。不能为空,否则抛出空指针异常 |
(2)JDK定义好的常见线程池
Executors中提供了一些静态方法用于快速创建一些常用的线程池:
newSingleThreadExecutor:创建一个单线程的线程池,这个线程池中只会有一个线程,当这个唯一的线程因为异常结束,那么会自动创建一个新的线程。该线程池的队列为无界队列,所有提交到线程池的任务都会进入按提交顺序任务队列,再按队列的顺序逐一执行任务。该线程池通常用于按顺序串行任务。但要注意的是无界队列意味着能无限添加任务,若任务量过大而唯一的线程来不及执行会导致资源消耗持续增加,有资源耗尽的风险。
newFixedThreadPool:该线程池与newSingleThreadExecutor相似,唯一不同的是可在调用静态方法时传入线程数量,创建一个固定大小的线程池。
newCachedThreadPool:创建一个可缓存的线程池,这个线程池没有核心线程即默认不会创建任何线程,当第一个任务提交至线程池时就会创建一个线程来执行该任务。任务执行完毕不会持续存在也不会立即销毁,会在线程池中缓存60秒,若60秒内没有任务分配给该线程执行,该线程将被回收。当有新任务提交时线程池会在内部寻找空闲的缓存线程,若没有空闲的线程就会创建一个新的,此线程池不会对线程数量做限制,理论上可创建Integer.MAX_VALUE个线程,故能创建的最大线程数量受制于系统资源。值得一提的是该线程池的任务队列为直接提交队列,该队列没有容量不会缓存任何任务,故每当有任务提交时都会立即寻找线程或创建新线程执行。
newScheduledThreadPool:创建一个支持定时及周期性执行任务的线程池,该线程池不限制大小,通过一些扩展的方法实现任务的周期性执行。当上一周期任务未执行完成,下一周期的任务会延迟执行,不会出现同一任务并发执行的情况。
下面我们以newScheduledThreadPool为例创建一个可周期性执行任务的线程池
public class ScheduledThreadPoolTest {
public static void main(String[] args) {
//创建一个核心线程数为2的周期性线程池
ScheduledExecutorService es = Executors.newScheduledThreadPool(2);
//发起一个初始延迟为1秒、执行周期为2秒的任务
es.scheduleAtFixedRate(new CachedRun("任务1"), 1000, 2000, TimeUnit.MILLISECONDS);
es.scheduleAtFixedRate(new CachedRun("任务2"), 1000, 2000, TimeUnit.MILLISECONDS);
es.scheduleAtFixedRate(new CachedRun("任务3"), 1000, 2000, TimeUnit.MILLISECONDS);
}
}
class CachedRun implements Runnable{
String name;
public CachedRun(String name){
this.name = name;
}
@Override
public void run() {
System.out.println(name+"调用"+Thread.currentThread().getName());
}
}
运行结果
从运行结果可以看出,可以调用的线程只有两个,且任务1、2、3会周期性执行,如果把代码拷贝运行,可以观察到其周期性运行之间的时间间隔。
3. 自定义线程池
JDK中包含的线程池能适用于对性能要求不高业务场景不复杂的情况,但都有其局限性,不够灵活。因此可以使用ThreadPoolExcutor的构造函数定义一个适合个性化业务场景的线程池,合理利用系统资源。
(1)选择合适的任务队列
在创建线程池时必不可少的就是创建一个任务队列用于管理提交至线程池的任务,任务队列一般分为:直接提交队列、有界任务队列、无界任务队列、优先任务队列。
直接提交队列:在创建线程池的构造器中传入一个SynchronousQueue对象。SynchronousQueue是一个特殊的BlockingQueue,他没有容量,每接收到一个任务就马上提交给线程执行,若当前线程池中没有空闲线程且线程数量已达到最大数量限制,则执行拒绝策略,默认策略为抛弃任务并抛出异常。
public class SynchronousQueueTest {
public static void main(String[] args){
ExcutorService pool = new ThreadPoolExcutor(1,3,2000,TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>());
pool.executor(new MyRun("任务1"));
pool.executor(new MyRun("任务2"));
pool.executor(new MyRun("任务3"));
pool.executor(new MyRun("任务4"));
}
}
class MyRun implements Runnable{
String name;
public MyRun(String name){
this.name = name;
}
@Overide
public void run(){
System.out.println(name+"调用"+Thread.currentThread()