前言
前文java中的阻塞队列和非阻塞队列我们介绍了常用的几种队列,队列的使用很广泛,特别是一些需要生产消费模式的场景以及需要对全局的集合进行操作的场景。今天我们来讲讲其中的一种应用——线程池。
我们从java 多线程实现方式知道,有三种常见的创建线程的方法:继承Thread类、实现Runnable接口和实现Callable接口。这些线程在运行结束后都会被虚拟机销毁,如果线程数量多的话,频繁的创建和销毁线程会大大浪费时间和效率。更重要的是浪费内存,当线程执行完毕后死亡,线程对象就变成垃圾,造成GC的频繁收集和停顿。
我们使用线程池来解决这个问题,让线程运行完不立即销毁,并且重复使用,继续执行其他的任务。使用线程池来管理线程,一方面使线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。
核心类
在java.util.concurrent包中我们能找到线程池的定义,其中ThreadPoolExecutor是我们线程池的核心类,我们先看下构造函数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler;}
构造函数的参数含义:
- corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
- maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
- keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
- unit:keepAliveTime的单位;
- workQueue:存放提交的任务,实现队列的方式有:BlockingQueue、LinkedBlockingQueue、SynchronousQueue、SynchronousQueue等,关于队列的选择要根据实际情况来确定;
- threadFactory:线程工厂,用于创建线程,一般用默认即可;
- handler:拒绝策略;创建线程时,为了防止资源被耗尽,任务队列都会选择创建有界任务队列,但这种模式下如果出现队列已满且线程池创建的线程数达到最大的线程数时,就需要用拒绝策略来处理线程池“超载”的情况。jdk默认的处理方式是AbortPolicy,抛出异常阻止程序(除非是安全性要求极高,否则在大并发情况下使用这种做法不是很明智);DiscardPolicy,丢弃无法处理的任务;DiscardOledesPolicy,也是丢弃任务,只不过丢弃的是队列最先被添加进去,马上要执行的任务;CallerRunsPolicy,由调用者所在线程来运行任务。除了使用jdk提供的这四种策略之外,我们还可以通过实现RejectExecutionHandler来自定义拒绝策略。
一般流程图:
- 当线程池中线程数小于corePoolSize时,新提交的任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程;
- 当线程池中线程数达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 ;
- 当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程执行任务;
- 当workQueue已满,且提交任务数超过maximumPoolSize,任务由RejectedExecutionHandler处理;
- 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程;
- 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。
提交线程池
ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new SynchronousQueue(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());pool.submit(xxRunnble); //方式1pool.execute(xxRunnble); //方式2
两种方式的区别:execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能比较好;submit返回一个Future对象如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。
关闭线程池
pool.shutdown();//方式1pool.shutdownNow();//方式2
两种方式的区别:shutdown不再接受新的任务,之前提交的任务等执行结束再关闭线程池;shutdownNow不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程List列表。
总结
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到多个任务上,而且由于在请求到达时线程已经存在,所以消除线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足。