线程池
本篇基于JDK
1.8。
一 为什么需要线程池?
- 直接创建线程的缺点:
- 每次通过new Thread()创建对象性能不佳。
- 可能会无限制的创建新的线程,造成系统资源匮乏,严重可能导致OOM。
- 缺乏管理性,没有一个统一的东西去管理线程的生命周期。
- 使用线程池的好处:
- 可重用存在的空闲线程,减少线程的多次创建,提升性能。
- 可设置其线程的最大创建数量和核心线程的数量,避免无限制的创建。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 可管理线程的生命周期,需要时创建,在空闲等待事件到达之后销毁线程。
- 扩展线程的功能,可定时执行,单线执行或者并发执行任务。
二 Executor
Java 1.5
中提供了Executor
框架用于把任务的提交和执行解耦,任务的提交,交给了Runable
或者Callable
,而Executor
框架用来处理任务。该框架包括三个重要的部分:
- 任务。也就是工作的单元,Runnable、Callable及其子类。
- 任务的执行。包括任务执行机制的核心接口
Executor
,以及继承自Executor
的ExecutorService
接口。Executor
框架有两个关键类实现了ExecutorService
接口(ThreadPoolExecutor
和ScheduledThreadPoolExecutor
)。 - 异步计算的结果。包括
Future
接口及实现了Future
接口的FutureTask
类。
Excutor
是一个接口类型,其子接口为ExecutorService
,核心实现类是ThreadPoolExecutor
。ThreadPoolExecutor
的子类是ScheduledThreadPoolExecutor
,它可以延迟或者是定期执行任务。继承关系如图:
三 ThreadPoolExecutor
:
ThreadPoolExecutor
则是Executor
框架的核心实现类。它的构造方法中,有很多很重要的参数。参数将决定该线程池的主要用途:
//ThreadPoolExecutor.java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//。。。省略代码。
}
-
参数
corePoolSize
:核心线程池的大小。也就是就算是处于空闲状态,线程也不会被回收,如果设置了
allowCoreThreadTimeOut = true
核心线程还是会被回收。如果调用了prestartAllCoreThreads()
方法,那么线程池会提前创建并启动所有基本线程。//ThreadPoolExecutor.java //启动所有核心线程,导致它们空闲地等待工作。这将覆盖仅在执行新任务时启动核心线程的默认策略。 public int prestartAllCoreThreads() { int n = 0; while (addWorker(null, true)) ++n; return n; }
在
SingleThreadExecutor
类型的线程池中,核心线程数默认为1。 -
参数
maximumPoolSize
:线程池中的最大线程数。线程池在执行任务的时候,所允许的最大并发数量。一般设置为
Integer.MAX_VALUE
。 -
参数
keepAliveTime
:线程空闲后等待的时间,等待时间结束之后会回收线程。
OkHttp
中设置线程的空闲时间是60s。 -
参数
unit
:线程空闲后等待的事件单位。
-
参数
workQueue
:阻塞队列。在执行任务之前用于保存任务的队列。此队列将仅包含由
execute
方法提交的Runnable
任务。常用的有ArrayBlockingQueue
、LinkedBlockingQueue
、SynchronousQueue
、PriorityBlockingQueue
。
-
在
CachedThreadPool
类型的线程池中采用的是SynchronousQueue
。 -
在
SingleThreadExecutor
类型的线程池中采用的是LinkedBlockingQueue
。 -
在
FixedThreadPool
类型的线程池中采用的是LinkedBlockingQueue
。 -
在
ScheduledThreadPool
类型的线程池中采用的是DelayedWorkQueue
。
-
参数
threadFactory
:线程池中创建Thread的工厂,可以自行实现。在实现的时候可以给线程命名。按照阿里的规范,创建的线程池都要实现自己的
ThreadFactory
。Executors
工厂类中的默认ThreadFactory
://Executors.java private static class DefaultThreadFactory implements ThreadFactory { //。。。省略代码。 DefaultThreadFactory() { //。。。省略代码。 } public Thread newThread(Runnable r) { Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }
-
参数
handler
:由于达到线程边界和队列容量而阻止执行时要使用的处理程序。也就是饱和策略,当线程池满了的时候,任务无法得到处理,这时候需要饱和策略来处理无法完成的任务。在
ThradPoolExecutor
类中有一下几种策略,默认的是AbortPolicy
:AbortPolicy
:直接抛异常,如果当前没有线程能执行新任务的时候。CallerRunsPolicy
:使用调用者的线程执行当前被拒绝的任务。这里要看实际的用途了。DiscardPolicy
:放弃这个被拒绝的任务。一般的最好不是这样的。DiscardOldestPolicy
:该处理程序丢弃最旧的未处理请求,然后重试execute
,除非该执行程序关闭,否则该任务将被丢弃。
3.1 newScheduledThreadPool
:
用途: 可以实现延迟运行或者是定期执行的线程池。
概念: ScheduledThreadPoolExecutor
线程池比较特殊,是ThreadPoolExecutor
的子类,实现了ScheduledExecutorService
接口,该接口可以安排任务在给定的时间之后运行,或定期执行。
//ScheduledThreadPoolExecutor.java
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
再其构造方法中必须设置核心线程数,最大的线程数是Integer.MAX_VALUE
。其阻塞队列为DelayedWorkQueue
,是其内部类,采用堆结构是一种优先队列,也就是后进先出。
延迟执行任务:
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(3);
executor.schedule(new Runnable() {
@Override
public void run() {
}
}, 1, TimeUnit.SECONDS);
周期性执行任务:
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(3);
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Log.e("WANG","FlashScreenPageActivity.run");
}
},1,5,TimeUnit.SECONDS);
当想要延迟执行某个任务的时候,调用的是schedule()
方法:
//ScheduledThreadPoolExecutor.java
public ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit) {
//。。。省略代码。
return t;
}
public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit) {
//。。。省略代码。
return t;
}
//参数1:任务类型。要么执行Runnable对象,要么执行Callable对象。Callable对象是有返回值的。
//参数2:延迟时间。
//参数3:延迟时间的单位。
//返回值:ScheduledFuture对象,里面有个isPeriodic()方法,返回true 表示是周期性的操作,false 表示是一次性的操作。
当要执行周期性的操作时调用的是scheduleAtFixedRate()
方法:
//ScheduledThreadPoolExecutor.java
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,
long period,TimeUnit unit){
//。。。省略代码。
return t;
}
//参数1:可执行对象。
//参数2:第一次任务执行时候的延迟时间。
//参数3:任务周期的间隔时间,从第一个任务的开始算起。
//参数4:时间单位,也是周期的时间单位。
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,
long delay,TimeUnit unit) {
//。。。省略代码。
return t;
}
//参数1:可执行对象。
//参数2:第一次任务执行时候的延迟时间。
//参数3:第一次任务结束到第二次任务开始时的延迟时间。
//参数4:时间单位,也是周期的时间单位。
ScheduledThreadPoolExecutor
在执行周期行任务的时候有两种类型的策略:
- 调用
scheduleAtFixedRate()
方法,该方法的周期计时是在一个任务开始之后。也就是在任务开始之后,就开始记录时间,当时间到达设置的period
的时候,判断前一个任务是否已经执行结束,如果执行结束了那就开启下一个任务执行,如果没有执行结束那就一直等到上一个任务执行结束。会出现上个任务的开始和下一个任务的开始之间的间隔大于period
值。这主要取决于每个任务的耗时情况。直白一点就是倒计时period
值包括了任务执行期间的耗时。 - 调用
scheduleWithFixedDelay()
方法,该方法的延迟计时是在一个任务执行结束之后。也就是在任务执行完毕之后才开始计时,当计时时间达到delay
值之后,才会开启下一个任务。不用判断前一个任务的执行情况,因为是在前一个任务结束之前才倒计时。
我们看到两种周期性执行任务的用法不太一样,在做网络轮询的时候最好采用scheduleWithFixedDelay()
方法。
ThreadPoolExecutor
参数: ScheduledThreadPoolExecutor
是ThreadPoolExecutor
的子类,所以在创建的时候,还是通过其后者的构造方法。核心线程数需要自行设置,最大线程数为 Integer.MAX_VALUE
,空闲线程等待时间是10ms
,不设置的话使用默认的饱和策略。
执行: 任务的执行队列是DelayedWorkQueue
,是其内部类。是一种优先级的队列,会对其插入的数据做下优先级的处理,保证优先级高的数据先被处理。内部采用的是堆结构。
3.2 newCachedThreadPool
:
用途: 创建一个线程池,该线程池根据需要创建新线程,但在可用时将重用先前构造的线程,线程的等待时间是60秒,所以如果太长时间没用使用,那么缓存线程池就跟普通的线程池一致。OkHttp使用的就是这种缓存线程池。也就是线程使用之后会存活60s,超过60s就会回收其线程。
ThreadPoolExecutor
参数: 核心线程数为0,最大线程数为 Integer.MAX_VALUE
,空闲线程等待时间是60s,使用默认的饱和策略。
执行: 使用的是同步阻塞队列,SynchronizedQueue
是一个没有数据缓冲的阻塞队列,take() & put()
是会阻塞线程,poll()&offer()
非阻塞操作,也就是任务的put()
需要先等take()
操作结束,反过来也一样。所以该队列中始终只会有一个任务。但是并不是说值存储一个任务,该线程池的模式就只能运行一个任务,它的存储和获取是很快的,当有任务经过put()
添加进来的时候,就会唤起线程进行take()
操作。并不是等待上一个任务结束才去取下一个任务,而是put()
一个任务就会去取一个任务,任务也是并发执行的。
3.3 newSingleThreadExecutor
:
用途: 该线程池用单个工作线程在无边界队列上操作。(但是,请注意,如果此单线程在关闭之前的执行过程中由于失败而终止,则在需要执行后续任务时将替换新线程)任务保证按顺序执行,并且在任何给定时间都不会有多个任务处于活动状态。与其他等价的newFixedThreadPool(1)
不同,返回的线程池不可重新配置新的线程。也就是该线程池只有一个核心线程,且线程一直在运行中,等待着任务。如果在执行任务期间,出现异常使线程中断,会继续创建一个新的线程继续执行任务。
ThreadPoolExecutor
参数: 核心线程数为1,最大线程数为 1,空闲线程等待时间是0毫秒,使用默认的饱和策略。
执行: 使用的是LinkedBlockingQueue
,任务是按照加入的顺序,顺序执行的。如果队列中有多个任务,也是下一个任务等到上一个结束之后才会执行的。不适合做并发处理。
3.4 newFixedThreadPool
:
用途: 该线程池将创建一定数量的核心线程数,将会一直运行直到显示的调用了shutdown
方法。该线程的创建需要传入核心线程数(nThreads
变量),该变量决定了核心线程数和总线程数量。
ThreadPoolExecutor
参数: 核心线程数等于总线程数等于nThreads
变量,空闲线程等待时间是0毫秒,使用默认的饱和策略。
执行: 使用的是LinkedBlockingQueue
,任务是按照加入的顺序,顺序执行的。如果队列中有多个任务,也是下一个任务等到上一个结束之后才会执行的。不适合做并发处理。
3.5 线程池的终止:
线程池的终止通过shutdown()
和shutdownNow()
方法,方法定义在ExecutorService
接口中,所以以上几种用途的线程池采用的都是同样的方法。
-
shutdown()
:启动有序关闭,在该关闭中执行以前提交的任务,但不接受新任务。如果已关闭,则调用没有其他效果。
-
shutdownNow()
:尝试停止所有正在执行的任务,停止处理等待的任务,并返回等待执行的任务列表。从该方法返回时,这些任务将从任务队列中排出(移除)。