线程池
一、线程池介绍
线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。
如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
如果用生活中的列子来说明,我们可以把线程池当做一个客服团队,如果同时有1000个人打电话进行咨询,按照正常的逻辑那就是需要1000个客服接听电话,服务客户。现实往往需要考虑到很多层面的东西,比如:资源够不够,招这么多人需要费用比较多。正常的做法就是招100个人成立一个客服中心,当有电话进来后分配没有接听的客服进行服务,如果超出了100个人同时咨询的话,提示客户等待,稍后处理,等有客服空出来就可以继续服务下一个客户,这样才能达到一个资源的合理利用,实现效益的最大化。
二、Java中的线程池种类
-
newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
-
newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
-
newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
-
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
-
newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
1.自定义一个线程类:
public class MyThread implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "\t开始发车啦...."); } }
2.测试类
public class ThreadPoolTest { public static void main(String[] args) { // 1.单线程的线程池 ExecutorService singlePool = Executors.newSingleThreadExecutor(); // 2.固定大小的线程池 ExecutorService fixedPool = Executors.newFixedThreadPool(10); // 3.可缓存的线程池 ExecutorService cacheddPool = Executors.newCachedThreadPool(); // 4.定时任务的线程池 ExecutorService scheduleddPool = Executors.newScheduledThreadPool(10); // 5.任务队列的线程池jdk1.8才有 ExecutorService stealingdPool = Executors.newWorkStealingPool(); // 自定义线程池的拒绝策略 BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); ThreadPoolExecutor executor = new ThreadPoolExecutor(// 使用ThreadPoolExecutor自定义线程池 10, 100, 10, TimeUnit.SECONDS, workQueue, new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (e.isShutdown()) { r.run(); } } }); for (int i = 0; i < 20; i++) { // singlePool.execute(new MyThread()); // fixedPool.execute(new MyThread()); // cacheddPool.execute(new MyThread()); // scheduleddPool.execute(new MyThread()); // stealingdPool.execute(new MyThread()); // executor.execute(new MyThread()); Future<?> submit = singlePool.submit(new MyThread()); System.out.println(submit); } } }
3.线程池的拒绝策略
当请求任务不断的过来,而系统此时又处理不过来的时候,我们需要采取的策略是拒绝服务。RejectedExecutionHandler接口提供了拒绝任务处理的自定义方法的机会。在ThreadPoolExecutor中已经包含四种处理策略。
- AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
- CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
- DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
- DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。
除了JDK默认为什么提供的四种拒绝策略,我们可以根据自己的业务需求去自定义拒绝策略,自定义的方式很简单,直接实现RejectedExecutionHandler接口即可
比如Spring integration中就有一个自定义的拒绝策略CallerBlocksPolicy,将任务插入到队列中,直到队列中有空闲并插入成功的时候,否则将根据最大等待时间一直阻塞,直到超时。
4.execute和submit的区别
execute适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了
submit方法适用于需要关注返回值的场景
public interface ExecutorService extends Executor { ... <T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); ... }
其子类AbstractExecutorService实现了submit方法,可以看到无论参数是Callable还是Runnable,最终都会被封装成RunnableFuture,然后再调用execute执行。
5.线程池的关闭
关闭线程池可以调用shutdownNow和shutdown两个方法来实现
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表
shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务
还有一些业务场景下需要知道线程池中的任务是否全部执行完成,当我们关闭线程池之后,可以用isTerminated来判断所有的线程是否执行完成,千万不要用isShutdown,isShutdown只是返回你是否调用过shutdown的结果。
6.自定义线程池
在实际的使用过程中,大部分我们都是用Executors去创建线程池直接使用,如果有一些其他的需求,比如指定线程池的拒绝策略,阻塞队列的类型,线程名称的前缀等等,我们可以采用自定义线程池的方式来解决。
如果只是简单的想要改变线程名称的前缀的话可以自定义ThreadFactory来实现,在Executors.new…中有一个ThreadFactory的参数,如果没有指定则用的是DefaultThreadFactory。
自定义线程池核心在于创建一个ThreadPoolExecutor对象,指定参数
下面我们看下ThreadPoolExecutor构造函数的定义:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) ;
-
corePoolSize
线程池大小,决定着新提交的任务是新开线程去执行还是放到任务队列中,也是线程池的最最核心的参数。一般线程池开始时是没有线程的,只有当任务来了并且线程数量小于corePoolSize才会创建线程。 -
maximumPoolSize
最大线程数,线程池能创建的最大线程数量。 -
keepAliveTime
在线程数量超过corePoolSize后,多余空闲线程的最大存活时间。 -
unit
时间单位 -
workQueue
存放来不及处理的任务的队列,是一个BlockingQueue。 -
threadFactory
生产线程的工厂类,可以定义线程名,优先级等。 -
handler
拒绝策略,当任务来不及处理的时候,如何处理, 前面有讲解。
阿里java编码规范明确说明:禁止使用Executors创建线程池,而应该使用ThreadPollExecutor来创建,并且使用ThreadFactory给出线程名称,以便跟踪。这样做的好处:让创建线程池的开发人员明确线程池运行规则,避免资源耗尽!
1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM(Out Of Memory)。
2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
所以,明确指名线程池核心线程数、最大线程数,以及队列最大持有任务数,能够更自主控制线程池。