想要了解的部分
- 线程池创建的七大参数
- 如何创建线程池
- 拒绝策略如何选择
- 什么场景需要使用什么线程池
- 如何使用线程池
- 为什么阿里规范不能使用Executors创建线程池?
参考资料+ 图灵学院的直播公开课
翻看源码可以看到线程池对象的构造函数如下:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数int maximumPoolSize, // 最大线程数long keepAliveTime, // 线程可存活时间TimeUnit unit, // 时间单位BlockingQueue workQueue, // 阻塞队列ThreadFactory threadFactory, // 线程工厂RejectedExecutionHandler handler // 拒绝策略) {
同时比较常用的可以看到5参数版的构造函数:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);
}
线程公司使用了默认的,如下
new DefaultThreadFactory()
拒绝策略也指向了默认的,如下
RejectedExecutionHandler defaultHandler = new AbortPolicy();
七大参数之间的关系看这个视频。
关于执行的顺序,涉及到两个概念 【提交优先级】和【执行优先级】
简单理解,当一个新的线程被提交时,会优先提交给核心线程,然后是队列,队列不足时才会创建新的线程(直到最大线程数)
可以查看源码 ,出处
看图
但是在执行线程池,顺序就不一样了,优先执行核心线程,再执行临时线程,最后再执行队列中的线程
先了解一下Executors提供的几种常用的线程池配置:
源码如下:
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue());
}
优点:
缺点:
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
}
优点:
缺点:
public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()));
}
优点:
缺点:
(API)一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有.
简单理解:在用不到队列的使用,选择该队列
表示无限长的队列,所以FixedThreadPool中即使没有临时线程,超出部分的线程就会全部存入内存中进行等待
简单理解:在大量使用队列时,选择该队列
使用该队列有一个极大的风险:存在内容溢出问题!
(API) 这是一个典型的**“有界缓存区”**,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素
可以声明长度大小的队列,声明时必须传入长度int
简单理解:比较普遍情况使用的队列,一般可以用于自定义声明线程池时使用。放置内容溢出。
从前面的提交优先级分析就可以看到,只有当提交任务出现【核心线程、队列、临时线程】都满了的情况时,就会触发拒绝策略
目前所有的拒绝策略都实现于RejectedExecutionHandler
接口
实现类有以下四种拒绝策略
永远都是直接抛出异常(一旦进入拒绝就抛出异常),该任务将会直接被忽略跳过
(api)用于被拒绝任务的处理程序,它直接在 execute
方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
简单理解:让线程池的调用者自己去执行这个线程的run()方法
测试一下:写一个简单的demo,不难发现当触发拒绝策略时,main线程执行了run方法
(api)用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试 execute
;如果执行程序已关闭,则会丢弃该任务。
(api)用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
思路如下
两个方法都可以将任务提交给线程池进行管理并执行,区别在于:
Callable的返回类型是Future,调用future.get()就可以实现异常的抛出了。实例如下
public class TestThreadPoolBegin {public static void main(String[] args) throws Exception{ExecutorService es = Executors.newSingleThreadExecutor();Callable callable = new Callable() {@Overridepublic Object call() throws Exception {System.out.println("线程处理开始...");int a = 0;int b = 3;System.out.println("除以0的结果为:" + b/a);System.out.println("线程处理结束...");return "0";}};Future future = es.submit(callable);System.out.println("任务执行完成,结果为:" + future.get());es.shutdown();}
}
future的get方法在未获得返回值之前会一直阻塞,我们可以使用future的isDone方法判断任务是否执行完成,然后再决定是否get,因此上述代码我们可以优化如下:
public class TestThreadPoolBegin {public static void main(String[] args) throws Exception{ExecutorService es = Executors.newSingleThreadExecutor();Callable callable = new Callable() {@Overridepublic String call() throws Exception {System.out.println("线程处理开始...");int a = 2;int b = 3;System.out.println("3/2的结果为:" + b/a);System.out.println("线程处理结束...");return "0";}};Future future = es.submit(callable);while(true) {//idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。if(future.isDone()) {System.out.println("任务执行完成:" + future.get());break;}}es.shutdown();}
}
比如:异步发邮件、心跳线程、或者是对效能优化的一些多线程场景
参考地址
创建一个线程池,如果线程池中的线程数量过大,它可以有效的回收多余的线程,如果线程数不足,那么它可以创建新的线程。
**应用场景:**用于线程数不固定,但是启动时间间隔较长的场景,可以极大的复用线程。
固定线程数量的线程池,线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器打到最大的使用率,同事又可以保证及时流量突然增大也不会占用服务器过多的资源。
应用场景:用于需要执行的线程数固定的场景,比如已经明确了n条任务并行的场景
该线程池支持定时,以及周期性的任务执行,我们可以延迟任务的执行时间,也可以设置一个周期性的时间让任务重复执行。以下特意详解该类型的线程池。
构造函数如下:
public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory,RejectedExecutionHandler handler) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue(), threadFactory, handler);
}
corePoolSize
( - 池中所保存的线程数(包括空闲线程))。(api:ScheduledExecutorService)schedule方法使用各种延迟创建任务,并返回一个可用于取消或检查执行的任务对象。scheduleAtFixedRate
和 scheduleWithFixedDelay
方法创建并执行某些在取消前一直定期运行的任务。
创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;
也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推。
如果任务的任何一个执行遇到异常,则后续执行都会被取消。否则,只能通过执行程序的取消或终止方法来终止该任务。
如果此任务的任何一个执行要花费比其周期更长的时间,则将推迟后续执行,但不会同时执行。写一个测试demo
package com.thread;import java.text.DateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;public class ScheduleThreadDemo {public static void main(String[] args) {ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);System.out.println("开始执行时间:" +DateFormat.getTimeInstance().format(new Date()));
// executor.scheduleAtFixedRate(new ThreadDemoS() , 1, 5, TimeUnit.SECONDS);executor.scheduleAtFixedRate(new ThreadDemoS() , 1, 5, TimeUnit.SECONDS);}}class ThreadDemoS extends Thread{private final Random random = new Random();private long[] sleeps = {10000l,5000l,1000l};@Overridepublic void run() {long start = System.currentTimeMillis();System.out.println("scheduleAtFixedRate 开始执行时间:" +DateFormat.getTimeInstance().format(new Date()) + "||"+ this.getName());try {Thread.sleep(sleeps[random.nextInt(3)]);} catch (InterruptedException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("scheduleAtFixedRate 执行花费时间=" + (end - start));System.out.println("scheduleAtFixedRate 执行完成时间:" + DateFormat.getTimeInstance().format(new Date()));System.out.println("======================================");}
}
测试结论如下:
创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。
如果任务的任一执行遇到异常,就会取消后续执行。否则,只能通过执行程序的取消或终止方法来终止该任务。
测试:同样使用上面的例子,测试结果如下:
不难发现,每次任务结束后会固定停止delay秒后再开始下一次任务。和任务的执行时间无关。
Executors 返回的线程池对象的弊端前面也已经做了分析,如下:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM应该是CPU占用过高。