一、ThreadPoolExecutor的方法说明
为了方便测试,先学习一下相关的方法
1.继承关系
Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的
ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等抽象类
AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法
ThreadPoolExecutor继承了类AbstractExecutorService。
2.方法说明
(1)向线程池提交任务
execute():实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行
executor.execute(() -> {
// do something 1
});
submit():是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果
executor.submit(new Runnable() {
@Override
public void run() {
//方法
}
});
(2)关闭线程池
shutdown():优雅关闭线程池,使用局部变量的时候必须关闭,否则会造成线程泄露,使用全局变量的时候一般不关闭线程池
shutdownNow():强制关闭线程池
上图是线程池的五种状态,如果线程池被定义为局部变量,则用完之后必须关闭线程池。在实际应用中,线程池通常被定义为静态全局变量,这个时候不需要关闭
(3)获取线程池中线程和任务的数目
getQueue() :获取阻塞队列,可以通过调用size()方法获取队列中等待执行的任务数目
getPoolSize() :线程池中线程数目
getActiveCount():线程池中的活跃线程数目
getCorePoolSize():线程池中的核心线程数目
getCompletedTaskCount():已执行完毕的任务数目
二、线程池的参数
public ThreadPoolExecutor(int corePoolSize, // 线程池的核心线程数
int maximumPoolSize, // 线程池的最大线程数
long keepAliveTime, // 当线程数大于核心时,多余的空闲线程等待新任务的存活时间。
TimeUnit unit, // keepAliveTime的时间单位
ThreadFactory threadFactory, // 线程工厂
BlockingQueue<Runnable> workQueue,// 用来储存等待执行任务的队列
RejectedExecutionHandler handler // 拒绝策略
)
1.corePoolSize
线程池保留的最小线程数。如果线程池中的线程少于此数目,则在执行execut()时创建。
2.maximumPoolSize
线程池中允许拥有的最大线程数。
如果对于和核心线程和最大线程依然有疑惑,不用急,后面会有详细的说明
3&4.keepAliveTime、unit
当线程闲置时,保持线程存活的时间。
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
5.threadFactory
使用默认的即可
6.workQueue
工作队列,存放提交的等待任务,其中有队列大小的限制。
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,而在未指明容量时,容量默认为Integer.MAX_VALUE。
LinkedBlockingDeque: 使用双向队列实现的双端阻塞队列,双端意味着可以像普通队列一样FIFO(先进先出),可以以像栈一样FILO(先进后出)
PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现Comparable接口也可以提供Comparator来对队列中的元素进行比较,跟时间没有任何关系,仅仅是按照优先级取任务。
DelayQueue:同PriorityBlockingQueue,也是二叉堆实现的优先级阻塞队列。要求元素都实现Delayed接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用take()方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用put()方法的时候就会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
缓存队列(阻塞队列)及排队策略
(1)直接提交(同步提交),用SynchronousQueue。特点是不放在队列里,直接提交给线程,如果没有线程,则新建一个。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
(2)无限提交,用类似LinkedBlockingQueue无界队列。特点是保存所有核心线程处理不了的任务,队列无上限,maximumPoolSize没有意义。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
(3)有限提交,用类似ArrayBlockingQueue有界队列。特点是可以保存超过核心线程的任务,并且队列也是有上限的。最常用的排队策略。可以避免资源浪费,在一定程度上降低了吞吐量,当队列饱和就会执行拒绝任务
7.handler
拒绝策略,有以下四种取值:
AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
CallerRunsPolicy:由调用线程处理该任务。(例如io操作,线程消费速度没有NIO快,可能导致阻塞队列一直增加,此时可以使用这个模式)
DiscardPolicy:丢弃任务,但是不抛出异常。 (可以配合这种模式进行自定义的处理方式)
DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务(重复执行)
三、线程池的工作原理
(1)如果当前运行的线程少于corePoolSize(核心线程数),则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
(2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue(阻塞队列/任务队列)。
(3)如果无法将任务加入BlockingQueue(队列已满),则在非corePool中创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
(4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并执行线程饱和策略,如:RejectedExecutionHandler.rejectedExecution()方法。
注意:
(1)线程池初始化时,是空的。如果线程池中的线程数少于核心线程数,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务。
(2)如果阻塞队列已满,且当前线程数<maximumPoolSize,则新建线程执行该任务。而不是新建线程,从阻塞队列里take任务来执行,所以这里并不是先来先执行的。
(3)阻塞队列用于存放任务,无法执行任务,起到一个缓冲的作用。阻塞队列的长度与最大线程数目无关。
1.为什么要区分核心线程和非核心线程
创建线程是有代价的,不能每次要执行一个任务时就创建一个线程,但是也不能在任务非常多的时候,只有少量的线程在执行,这样任务是来不及处理的,而是应该创建合适的足够多的线程来及时的处理任务。随着任务数量的变化,当任务数明显很小时,原本创建的多余的线程就没有必要再存活着了,因为这时使用少量的线程就能够处理的过来了,所以说真正工作的线程的数量,是随着任务的变化而变化的。
2.这种设计方式有什么好处
ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。
3.线程池核心线程数配置推荐
CPU密集型任务:尽量压榨CPU,参考值设置为CPU的个数+1
IO密集型任务:参考值可以设置为CPU的个数 * 2
四、下面给出一个测试的用例
package thread.threadPool;
import java.util.concurrent.*;
public class NewFixedThreadPoolTest {
// 创建一个可重用固定个数的线程池
private static ThreadPoolExecutor fixedThreadPool = new ThreadPoolExecutor(1, 2,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(1));
public static void main(String[] args) {
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
printCount();
System.out.println("加入第一个任务,线程池刚刚初始化,没有可以执行任务的核心线程,创建一个核心线程来执行任务");
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
printCount();
System.out.println("加入第二个任务,没有可以执行任务的核心线程,且任务数大于corePoolSize,新加入任务被放在了阻塞队列中");
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
printCount();
System.out.println("加入第三个任务,此时,阻塞队列已满,新建非核心线程执行新加入任务");
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
printCount();
System.out.println("第一个任务执行完毕,核心线程空闲,阻塞队列的任务被取出来,使用核心线程来执行");
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
printCount();
System.out.println("第二个任务执行完毕,核心线程空闲,非核心线程在执行第三个任务");
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
printCount();
System.out.println("第三个任务执行完毕,非核心线程被销毁,核心线程保留");
}
private static void printCount() {
System.out.println("------------------------------------");
System.out.println("当前活跃线程数:"+fixedThreadPool.getActiveCount());
System.out.println("当前核心线程数:"+fixedThreadPool.getCorePoolSize());
System.out.println("阻塞队列中的任务数:"+fixedThreadPool.getQueue().size());
}
}
五、Executors框架创建四种线程池
使用不同的参数,可以创造不同的线程池,常用的以下四种:
(不建议使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式可以让各位更加明确线程池的运行规则,规避资源耗尽的风险。)
1.newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
2.newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。
3.newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
表示延迟3秒执行。定期执行示例代码如下:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);
// scheduledThreadPool.scheduleAtFixedRate(()-> {
// System.out.println("delay 3 seconds"+Thread.currentThread().getName());
// }, 1,1, TimeUnit.SECONDS);
特别提示:通过ScheduledExecutorService执行的周期任务,如果任务执行过程中抛出了异常,那么ScheduledExecutorService就会停止执行任务,而且也不会再周期地执行该任务了。所以如果想保持任务周期执行,需要catch一切可能的异常。
4.newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。