线程池(thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
想象这么一个场景:
在学校附近新开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人,而是每次有业务来
了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来一个任务,起一个线程进行处理的模
式。
很快老板发现问题来了,每次招聘 + 解雇同学的成本还是非常高的。老板还是很善于变通的,知道了为什么大家都要
雇人了,所以指定了一个指标,公司业务人员会扩张到 3 个人,但还是随着业务逐步雇人。于是再有业务来了,老板
就看,如果现在公司还没 3 个人,就雇一个人去送快递,否则只是把业务放到一个本本上,等着 3 个快递人员空闲
的时候去处理。这个就是我们要带出的线程池的模式
使用线程池的优势
:1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。
接下来看一段代码:
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor( //线程池--快递公司
3, //核心线程数(正式员工),创建好线程池,正式员工就开始取快递
5, //最大线程数(最多数量的员工(正式工+临时工)
//临时工雇佣:正式员工忙不过就会创建临时工
//临时工解雇:空闲时间超出设置的时间范围,就解雇
30, //时间单位
TimeUnit.SECONDS, //时间单位(时间数量+时间单位表示一定范围时间)
//阻塞队列: 存放包裹的仓库(存放任务的数据结构)
new ArrayBlockingQueue<>(1000),
// //(了解)线程池创建Thread线程的工厂类,没有提供的话,就是用线程池内部默认的创建线程的方式
// new ThreadFactory(){
// @Override
// public Thread newThread(Runnable r) {
// return null;
// }
// },
//拒绝策略
//CallerRunsPolicy():谁(execute代码行所在的线程)让我(快递公司)送快递,不好意思,你自己去送。我们拒绝服务。
//new ThreadPoolExecutor.CallerRunsPolicy()
//AbortPolicy(): 直接抛出RejectedExecutionException异常
//new ThreadPoolExecutor.AbortPolicy()
//DiscardPolicy():从阻塞式队列丢弃最新的任务(队尾)
//new ThreadPoolExecutor.DiscardPolicy()
//DiscardOldestPolicy():从阻塞式队列丢弃最老的任务(队尾)
new ThreadPoolExecutor.DiscardOldestPolicy()
);
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("送快递到北京,A同学");
}
});
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("送快递到新疆,B同学");
}
});
System.out.println("我正在做事情");
}
}
其中 ExecutorService pool = new ThreadPoolExecutor()表示创建自定义线程池:我们先看ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
其中corePoolSize表示核心线程数 : 当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize.
maximumPoolSize表示最大线程数:表示线程数量的上限,当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数 .
keepAliveTime表示线程存活时间:当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数.
unit表示时间单位 : 时间单位(时间数量+时间单位表示一定范围时间)
workQueue表示任务队列:用于传输和保存等待执行任务的阻塞队列
threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
handler表示拒绝任务处理时的策略:
AbortPolicy:丢弃任务并抛出RejectedExecutionException
CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
DiscardPolicy:丢弃任务,不做任何处理。
使用线程池:
execute()和Submit():
1、execute(),执行一个任务,没有返回值。
2、submit(),提交一个线程任务,有返回值。
submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用。submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。
Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果.
Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM(内存用完)。
2)newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。