目录
3.4ScheduledThreadPoolExecutor
Java中的线程池是使用场景最多的框架,几乎所有的异步或者并发任务都可以使用线程池,合理的使用线程池可以带来以下3点好处:
- 降低消耗:通过重复利用可以降低创建和销毁线程带来的系统损耗。
- 提高响应速度:任务执行时(后面会讲为什么是执行的时候)可不用等待线程创建。
- 提高线程的可管理性:线程是操作系统的稀缺资源,如果无限制的创建,会带来极大的系统负担,利用线程池可以统一的管理、监控线程,提高了可管理性。
1,线程池实现原理
当向线程池提交一个任务后,线程池是如何处理这个任务的呢,下图展示了线程池的主要处理流程:
从上图可以看出当一个任务提交到线程池后处理的主要过程:
- 判断核心线程数是否已满(corePoolSize),如果没有满,则创建一个核心线程并让该线程处理任务,如果核心线程数已满,则进入下一个环节。
- 当核心线程数已满后,需要判断工作队列(BlockQueue)是否已满,如果没满,则将任务添加到任务队列。如果队列已满,则进入下一个环节。
- 如果任务队列已满,则判断当前线程个数是否已达到线程线程个数上限,如果没达到则创建新线程执行任务。如果达到上限则执行响应的拒绝策略。
敲黑板:
在执行步骤1时,当poolSize<corePoolSize时,如果当前核心线程有空闲的,也不会去执行当前任务,这么做的目的是为了对线程池进行预热即将corePoolSize的值打满,因为在创建线程时需要获取全局锁(这可能会是系统伸缩性的瓶颈),而预热过后,大部分时间都在加入任务队列并等待执行,这一点我们也可以从ThreadPoolExecutor的源码中看到。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 对应步骤1
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 对应步骤2
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 对应步骤3
else if (!addWorker(command, false))
reject(command);
}
我们再用一张图来描述下ThreadPoolExceutor.exceute()方法的执行,请看下图:
从上述我们还得出线程池中的线程执行任务分一下两种情况:
- 创建线程时,会让该线程执行当前任务。
- 线程会反复从BlockQueue中获取任务并执行。
至此线程池的实现原理基本描述完了!
2,线程池的使用
2.1创建线程池
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
5,
6,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue(1000),
new DemoThreadFactory("测试线程池"),
new ThreadPoolExecutor.CallerRunsPolicy() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
super.rejectedExecution(r, e);
// 由调用者线程决定被拒绝的该如何处理,比如放入消息队列、数据库等
}
});
poolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("我是线程执行单元,我被执行了"+Thread.currentThread().getName());
}
});
}
static class DemoThreadFactory implements ThreadFactory{
private final ThreadGroup group;
private String threadName;
DemoThreadFactory(String threadName){
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
this.threadName = threadName;
}
@Override
public Thread newThread(Runnable r) {
return new Thread(group, r, threadName, 0);
}
}
}
创建线程池的各个参数详解:
- corePoolSize(核心线程数):当提交一个任务时如果工作线程数小于核心线程数时会创建新的线程并执行当前任务,即使有空闲的核心线程也是如此,直到核心线程数达到配置的corePoolSize,这么设计的原因上面说了,可以通过poolExecutor.prestartAllCoreThreads()方法来提前创建好所有的核心线程。
- maximumPoolSize(最大线程数):线程池允许创建的最大线程数量,当核心线程数已满且队列已满,如果此时当前线程数小于最大线程数则会创建新的线程来执行任务,需要注意的是如果队列是无限的,则这个参数无效。因为永远也不会满足队列已满的条件,就导致了只会创建corePoolSize个线程。
- keepAliveTime(空闲线程存活时间):当线程空闲时,保持多久的存活时间。如果线程池处理的任务多,且每个任务执行时间短,可以设置的相对长一些,这样能提高利用率。
- timeUnit:keepAliveTime的时间单位,秒、毫秒啥的。
- blockingQueue(工作阻塞队列):用于保存任务的阻塞队列,可以选择一下几个队列:
ArrayBlockingQueue:基于数组结构的有界阻塞队列,按照FIFO规则对元素进行排序。
LinkedBlockingQueue:基于链表结构的阻塞队列,按照FIFO规则对元素进行排序,吞吐量高于ArrayBlockingQueue。
SynchronousQueue:一个不存储元素的同步阻塞队列,但是每一个插入动作必须等到另一个线程执行移除操作,这个队列起到的作用是将主线程提交的任务传递给空闲线程(具体后面会将),吞吐量高于LinkedBlockingQueue。
PriorityBlockingQueue:一个具有优先级的阻塞队列。
- threadFactory(线程工厂):创建线程的工厂,可以通过该工厂给线程设置一个有意义的名字,这么做可以在问题追溯上带来便利。
- RejectedExecutionHandler:拒绝策略,jdk提供了AbortPolicy:直接抛出异常、CallerRunsPolicy:只用调用者所在线程来运行任务、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务、DiscardPolicy:不处理,丢弃掉,我上面的示例中使用了CallerRunsPolicy,在生产环境中如果默认的这几种处理方式都不合适,我们可以捕捉拒绝,并保存便于后续继续。
敲黑板:
创建线程池有多种方法,可以像上述代码一样 new ThreadPoolExecutor()来实现,也可以通过Executors工具类来创建如:Executors.newCachedThreadPool(),Executors是一个工具类,好比Arrays这个类是集合工具一样。不管一般不直接使用Executors工具类来创建,推荐像上面这样自定义的方式去创建,原因如下。
- 可以通过ThreadFactory设置更有意义的名字,便于出问题后的追溯。
- 通过自定义线程数来提高CPU利用率。
- 通过初始化BlockingQueue来合理配置队列大小,因为过大的队列或者无界的队列是一件很危险的事儿,容易导致OOM,Executors中有很多工厂方法,里面要么队列是Integer.maxValue,要么是无界的,所以不推荐。
- 通过自定义拒绝策略可以更合理的处理无法及时得到处理的任务。
2.2向线程池提交任务
向线程池提交任务除了像上面示例代码那样 调用execute()方法,还可以调用submit()方法,且该方法可以支持任务返回值如下所示:
Future future = poolExecutor.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
System.out.println("我是线程执行单元,我被执行了"+Thread.currentThread().getName());
return 1;
}
});
Object result = future.get();//此处会阻塞
2.3关闭线程池
关闭线程池有2个方法:
- shutdown:该方法首先设置线程池状态为ShutDown状态,而后尝试停止所有正在或停止执行任务的线程。
- shutdownNow:该方法先设置线程池状态为ShutDown状态,而后中断所有没有在执行任务的线程。
具体使用哪个可以根据业务决定,但从2个方法的区别上可以看出,shutdownNow不会终止正在执行任务的线程,而shutdown会。而且判断一个线程是否已经终止的方法应该使用isTerminated(),而不是isShutdown(),因为上诉2个方法都是先设置状态为shutDown。
2.4如何配置线程池
如何配置线程池,没有一定之规,最终可以通过压力测试调整,如果一定要说,我们可以先从几个角度划分一下任务特性:
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
- 任务的优先级:高、中和低。
- 任务的执行时间:长、中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
比如CPU密集型任务,可以设置较少的线程数(如:Ncpus个线程),减少CPU执行任务的切换,而IO密集型任务可以尽量设置相对较多的线程数(如:2*Ncpus),因为IO密集型一个任务要执行10秒,但其中9秒在执行IO,这9秒CPU会空闲如果所以要设置较多线程数,降低CPU空闲时间提高利用率。
3,Executor框架
在java中Runnable和Callable 标识了工作单元,Executor则提供了线程的执行机制。
3.1Executor的2级调度模型
由上图可知上层使用用户级调度器将这些任务映射为固定数量的线程;底层操作系统内核将这些任务映射到硬件上去执行,底层是有操作系统内核控制,不受到应用程序的控制。
3.2Executor框架的组成
executor框架主要有以下3个方面组成:
- 任务:需要实现Runnable或者Callable接口。
- 任务执行:Executor或ExecutorService(该接口继承Executor)核心接口及实现类。
- 任务结果:Future接口及其实现类FutureTask。
下面用一个类图看以下全貌,不难看出这里核心的类就几个ThreadPoolExecutor、ScheduledThreadPoolExecutor、FutureTask。
3.3ThreadPoolExecutor
这应该是使用最多也是最核心的一个类了,关于该类初始化的各种参数上面讲过了,这里就不赘述了,重点说下由Executors工具类创建出来的该类型的类(newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool)。
newFixedThreadPool
// Executors 源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
上面是源码从中可知FixedThreadPool的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。当线程池中的线程数大于corePoolSize时,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把keepAliveTime设置为0L,意味着多余的空闲线程会被立即终止,最坑的是LinkedBlockingQueue 这个参数,其默认的大小为Integer.MAX_VALUE这个数量跟无界差不多了。
newSingleThreadExecutor
// Executors 源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
除了核心线程数和最大线程数都是1以外,其余与上面FixedThreadPool一样。
newCachedThreadPool
//Executors 源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
FixedThreadPool和SingleThreadExecutor使用相当于无界队列LinkedBlockingQueue作为线程池的工作队列。CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。
前面提到过SynchronousQueue队列是一个没有容量的队列,但是每个插入动作offer()必须等待另一个线程的移除动作poll(),该队列的主要作用不是为了存储任务,而是为了将任务传递给空闲线程,用一个示意图说明下:
对上图的一些说明:
- 任务进来后,先执行offer操作,此时如果有空闲线程在执行poll操作时,任务会传递给该线程执行。
- 任务进来后,先执行offer操作,如果此时pool为空或者没有空闲线程在执行poll操作,则创建新线程去执行该任务。
- 线程在执行完任务后会继续调用poll(keepAliveTime)去等待,最多等待60秒,如果60秒之内有任务提交则重复上述步骤,不然会被系统回收,也就是说,如果这个线程池一直空闲貌似不占用任何资源,因为队列是空的,线程也是空的。
3.4ScheduledThreadPoolExecutor
运行机制
ScheduledThreadPoolExecutor周期执行任务线程池,继承自ThreadPoolExecutor,与父类不同的是,队列换成了DelayQueue,任务类型也不是原生的Runnalbe了,包装成了ScheduledFutureTask,ScheduledThreadPoolExecutor的执行流程大致如下图所示。
实现原理
从一段简单的代码开始:
public class ScheduledThreadPoolDemo {
public static void main(String[] args) {
LocalTime lt = LocalTime.now();
System.out.println(lt.getSecond());
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(10);
scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
LocalTime lt = LocalTime.now();
System.out.println(lt.getSecond() + " 我被执行了");
}
},10,10, TimeUnit.SECONDS);
}
}
// scheduleAtFixedRate源码
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
上述代码说明:
- 由构造方法可知(这里没贴出来),maxPoolSize设置为Integer.MAX_VALUE,内部使用的是DelayQueue队列,该队列初始容量16,但是会一直扩容,所以相当于一个无界队列。
- scheduleAtFixedRate过程实际上只做了2件事儿,将Runnable包装成ScheduledFutureTask对象,延迟执行,下面我们通过一张图说明下延迟执行的步骤。
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
// 添加到队列实际调用的是offer方法
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
// 创建线程
ensurePrestart();
}
}
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
由上述代码可知比较重要的就是2个方法了:添加队列(add)、线程获取任务(take)执行 ,我们先来看下线程执行某个任务的过程如下图所示:
一个4个步骤:
- 获取已经到期的任务 time大于等于当前时间的。
- 执行任务。
- 修改ScheduledFutureTask的time参数以便于后续继续执行。
- 将该任务放入队列。
这里面涉及到的具体方法比如队列的offer,take方法,这里就不展开讲了,后面我计划单独写一个专门分析下ThreadPoolExecutor相关源码的文章。
3.5FutureTask