04.ThreadPoolExecutor使用
- 如下所示
ExecutorService service = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
- 对于ThreadPoolExecutor有多个构造方法,对于上面的构造方法中的其他参数都采用默认值。可以通过execute和submit两种方式来向线程池提交一个任务。
- execute
- 当我们使用execute来提交任务时,由于execute方法没有返回值,所以说我们也就无法判定任务是否被线程池执行成功。
service.execute(new Runnable() {
public void run() {
System.out.println(“execute方式”);
}
});
- submit
- 当我们使用submit来提交任务时,它会返回一个future,我们就可以通过这个future来判断任务是否执行成功,还可以通过future的get方法来获取返回值。如果子线程任务没有完成,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候有可能任务并没有执行完。
Future future = service.submit(new Callable() {
@Override
public Integer call() throws Exception {
System.out.println(“submit方式”);
return 2;
}
});
try {
Integer number = future.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
- 线程池关闭
- 调用线程池的
shutdown()
或shutdownNow()
方法来关闭线程池 - shutdown原理:将线程池状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
- shutdownNow原理:将线程池的状态设置成STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行任务的列表。
- 中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。 但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。
05.线程池执行流程
-
大概的流程图如下
-
-
文字描述如下
-
①如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。
-
②如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。
-
③由于任务队列已满,无法将任务插入到任务队列中。这个时候如果线程池中的线程数量没有达到线程池所设定的最大值,那么这时候就会立即启动一个非核心线程来执行任务。
-
④如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。
06.四种线程池类
- Java中四种具有不同功能常见的线程池。
- 他们都是直接或者间接配置ThreadPoolExecutor来实现他们各自的功能。这四种线程池分别是newFixedThreadPool,newCachedThreadPool,newScheduledThreadPool和newSingleThreadExecutor。这四个线程池可以通过Executors类获取。
6.1 newFixedThreadPool
- 通过Executors中的newFixedThreadPool方法来创建,该线程池是一种线程数量固定的线程池。
ExecutorService service = Executors.newFixedThreadPool(4);
- 在这个线程池中 所容纳最大的线程数就是我们设置的核心线程数。
- 如果线程池的线程处于空闲状态的话,它们并不会被回收,除非是这个线程池被关闭。如果所有的线程都处于活动状态的话,新任务就会处于等待状态,直到有线程空闲出来。
- 由于newFixedThreadPool只有核心线程,并且这些线程都不会被回收,也就是它能够更快速的响应外界请求 。
- 从下面的newFixedThreadPool方法的实现可以看出,newFixedThreadPool只有核心线程,并且不存在超时机制,采用LinkedBlockingQueue,所以对于任务队列的大小也是没有限制的。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
6.2 newCachedThreadPool
- 通过Executors中的newCachedThreadPool方法来创建。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
- 通过s上面的newCachedThreadPool方法在这里我们可以看出它的 核心线程数为0, 线程池的最大线程数Integer.MAX_VALUE。而Integer.MAX_VALUE是一个很大的数,也差不多可以说 这个线程池中的最大线程数可以任意大。
- 当线程池中的线程都处于活动状态的时候,线程池就会创建一个新的线程来处理任务。该线程池中的线程超时时长为60秒,所以当线程处于闲置状态超过60秒的时候便会被回收。
- 这也就意味着若是整个线程池的线程都处于闲置状态超过60秒以后,在newCachedThreadPool线程池中是不存在任何线程的,所以这时候它几乎不占用任何的系统资源。
- 对于newCachedThreadPool他的任务队列采用的是SynchronousQueue,上面说到在SynchronousQueue内部没有任何容量的阻塞队列。SynchronousQueue内部相当于一个空集合,我们无法将一个任务插入到SynchronousQueue中。所以说在线程池中如果现有线程无法接收任务,将会创建新的线程来执行任务。
6.3 newScheduledThreadPool
- 通过Executors中的newScheduledThreadPool方法来创建。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
- 它的核心线程数是固定的,对于非核心线程几乎可以说是没有限制的,并且当非核心线程处于限制状态的时候就会立即被回收。
- 创建一个可定时执行或周期执行任务的线程池:
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
service.schedule(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+“延迟三秒执行”);
}
}, 3, TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+“延迟三秒后每隔2秒执行”);
}
}, 3, 2, TimeUnit.SECONDS);
- 输出结果:
pool-1-thread-2延迟三秒后每隔2秒执行
pool-1-thread-1延迟三秒执行
pool-1-thread-1延迟三秒后每隔2秒执行
pool-1-thread-2延迟三秒后每隔2秒执行
pool-1-thread-2延迟三秒后每隔2秒执行
- 部分方法说明
schedule(Runnable command, long delay, TimeUnit unit)
:延迟一定时间后执行Runnable任务;schedule(Callable callable, long delay, TimeUnit unit)
:延迟一定时间后执行Callable任务;scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
:延迟一定时间后,以间隔period时间的频率周期性地执行任务;scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit)
:与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的。- ScheduledExecutorService功能强大,对于定时执行的任务,建议多采用该方法。
6.4 newSingleThreadExecutor
- 通过Executors中的newSingleThreadExecutor方法来创建,在这个线程池中只有一个核心线程,对于任务队列没有大小限制,也就意味着这一个任务处于活动状态时,其他任务都会在任务队列中排队等候依次执行。
- newSingleThreadExecutor将所有的外界任务统一到一个线程中支持,所以在这个任务执行之间我们不需要处理线程同步的问题。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
07.execute和submit区别
- 先思考一个问题
- 为了保证项目中线程数量不会乱飙升,不好管理,我们会使用线程池,保证线程在我们的管理之下。
- 我们也经常说:使用线程池复用线程。那么问题是:线程池中的线程是如何复用的?是执行完成后销毁,再新建几个放那;还是始终是那几个线程(针对 coreSize 线程)。
- execute和submit
- 调用线程池的execute方法(ExecutorService的submit方法最终也是调用execute)传进去的Runnable,并不会直接以new Thread(runnable).start()的方式来执行,而是通过一个正在运行的线程来调用我们传进去的Runnable的run方法的。
- 那么,这个正在运行的线程,在执行完传进去的Runnable的run方法后会销毁吗?看情况。
- 大部分场景下,我们都是通过Executors的newXXX方法来创建线程池的,就拿newCachedThreadPool来说:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue());
}
- 看第三个参数(keepAliveTime):60L,后面的单位是秒,也就是说,newCachedThreadPool方法返回的线程池,它的工作线程(也就是用来调用Runnable的run方法的线程)的空闲等待时长为60秒,如果超过了60秒没有获取到新的任务,那么这个工作线程就会结束。如果在60秒内接到了新的任务,那么它会在新任务结束后重新等待。
- 还有另一种常用的线程池,通过newFixedThreadPool方法创建的:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
- 它跟上面的newCachedThreadPool方法一样,创建的都是ThreadPoolExecutor的对象,只是参数不同而已。
可以看到第三个参数设置成了0,这就说明,如果当前工作线程数 > corePoolSize时,并且有工作线程在执行完上一个任务后没拿到新的任务,那么这个工作线程就会立即结束。
再看第二个参数(maximumPoolSize),它设置成了跟corePoolSize一样大,也就是说当前工作线程数 永远不会大于 corePoolSize了,这样的话,即使有工作线程是空闲的,也不会主动结束,会一直等待下一个任务的到来。 - ThreadPoolExecutor分析
- 来探究一下ThreadPoolExecutor是如何管理线程的,先来看精简后的execute方法:
- 逻辑很清晰:当execute方法被调用时,如果当前工作线程 < corePoolSize(上面ThreadPoolExecutor构造方法的第一个参数)的话,就会创建新的线程,否则加入队列。加入队列后如果没有工作线程在运行,也会创建一个。
private final BlockingQueue workQueue;
public void execute(Runnable command) {
int c = ctl.get();
//当前工作线程还没满
if (workerCountOf© < corePoolSize) {
//可以创建新的工作线程来执行这个任务
if (addWorker(command, true)){
//添加成功直接返回
return;
}
}
//如果工作线程满了的话,会加入到阻塞队列中
if (workQueue.offer(command)) {
int recheck = ctl.get();
//加入到队列之后,如果当前没有工作线程,那么就会创建一个工作线程
if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
}
- 接着看它是怎么创建新线程的:
- 主要操作是再次检查,然后创建Worker对象,并且把worker对象店家到HashSet集合中,最后启动工作线程。
private final HashSet workers = new HashSet<>();
private boolean addWorker(Runnable firstTask, boolean core) {
//再次检查
int wc = workerCountOf©;
if (wc >= CAPACITY || wc >= corePoolSize)
return false;
boolean workerStarted = false;
Worker w = null;
//创建Worker对象
w = new Worker(firstTask);
//添加到集合中
workers.add(w);
final Thread t = w.thread;
//启动工作线程
t.start();
workerStarted = true;
return workerStarted;
}
- 看看Worker里面是怎么样的:
- 可以看到,这个Worker也是一个Runnable。构造方法里面还创建了一个Thread,这个Thread对象,对应了上面addWorker方法启动的那个thread。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
final Thread thread;
Runnable firstTask;
Worker(Runnable firstTask) {
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
}
- 再看Worker类中的run方法,它调用了runWorker,并把自己传了进去:
- Worker里面的firstTask,就是我们通过execute方法传进去的Runnable,可以看到它会在这个方法里面被执行。
- 执行完成之后,接着就会通过getTask方法尝试从等待队列中(上面的workQueue)获取下一个任务,如果getTask方法返回null的话,那么这个工作线程就会结束。
final void runWorker(Worker w) {
Runnable task = w.firstTask;
w.firstTask = null;
while (task != null || (task = getTask()) != null) {
try {
task.run();
} finally {
task = null;
w.completedTasks++;
}
}
}
- 最后看看runWorker方法中的getTask方法
最后
由于文章篇幅原因,我只把面试题列了出来,详细的答案,我整理成了一份PDF文档,这份文档还包括了还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 ,帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
pletedTasks++;
}
}
}
- 最后看看runWorker方法中的getTask方法
最后
由于文章篇幅原因,我只把面试题列了出来,详细的答案,我整理成了一份PDF文档,这份文档还包括了还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 ,帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!