背景
作为几年工作经验的java程序员肯定知道java中通过线程池来调度线程的。线程池分为几种,为什么会设计这几种线程池各自的实现算法是什么,适用场景是什么?这些疑问其实是脱离java语言,其他语言设计线程池也会遇到同样的问题。所以这里对这线程池设计原理需要考虑的方面进行分析。
线程池的作用
1.线程池即预先创建线程的技术,一个线程执行完后重新放回不会销毁掉提高了线程的利用率。
2.由于我们要使用线程来执行任务的时候直接从线程池中去现成的所以提高了程序的相应速度。
3.线程池可以对里面的线程进行管理,至于如何管理XXXX(如何销毁线程、如何结束线程状态等等)。
创建线程池需要考虑的
从这里我们知道线程池的一些基本配置参数。比如 线程池的大小,执行任务队列,线程池满了新任务的执行策略,工作线程空闲后存活时间(如果想提高线程利用率提议调大该时间)。
所以它的构造函数为:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
这里为什么有一个corePoolSize和maximumPoolSize呢? 级别关系是 coreSize -> 队列 ->(无法使用队列则创建新线程) maximumPoolSize。
a).corePoolSize意思是基本大小,比如线程池corePoolSize=10,而此时线程池里有5个线程且都是空闲的,由于还没有达到corePoolSize,如果提交一个任务会从线程池里选择一个线程来执行任务。当达到了corePoolSize时Executor默认会先把任务添加进队列中,如果无法加入队列则创建新线程直到达到maximumPoolSize。
b).maximumPoolSize使用场景,如果线程池里的线程数量达到了maximumPoolSize且其中的线程没有空闲的。当新任务到达的时候会新建线程,如果无限地创建会消耗系统的资源,所以这里有一个maximumPoolSize参数,当线程数量达到maximumPoolSize的时候即时没有空闲线程了也不会重新创建线程。
不重新创建线程那怎么办呢?这就需要使用RejectedExecutionHandler(饱和策略)。现有的饱和策略有,策略分两种执行与不执行:
对于不执行的,我可能会有以下情况:a.丢弃 b.抛出异常 c.丢弃但是记录日志或持久化到数据库(通过实现RejectedExecutionHandler接口来处理)。
对于执行该任务会有如下的情况:a.腾出空间,替换最老未执行的任务。
1).丢弃该任务 2).丢弃最老未执行的腾出空间执行该任务
加入队列的几种情况
当我们创建线程池需要指定队列的时候必须,而不同队列线程池会有不同的表现。
有3种常见的队列:
a).ArrayBlockingQueue 有界队列,创建时候必须制定大小(构造函数要求制定)
b).LinkedBlockingQueue 无界队列
public LinkedBlockingQueue() {
}
c).SynchronousQueue 同步队列,每新增一个任务的线程必须等待另一个线程取出任务。 //还是不是很理解同步队列怎么实现的背后的实现原理-怎么做到同步的。
这3种队列的使用场景是什么?
当资源有限的时候使用有界队列,使用有界队列的过程中,队列大小和最大池大小可能需要互相折衷。大队列小线程池大小可以降低CPU使用率和线程之间的切换。
使用无界队列时候maxSize参数无用,因为当线程数超过coresize的时候会一直不停的往LinkedBlockingQueue里放。这个可以用于web服务器访问量突发的情况。
线程池如何处理任务
这里讲线程池如何提交任务,任务提交后如何跟踪结果。
execute方式提交,这里没有返回结果。所以无法获取任务执行结果。
public void execute(Runnable command) {
......
addWorke(command,true)
}
private boolean addWorker(Runnable firstTask, boolean core) {
w = new Worker(firstTask); //这里会把Runable接口包装城worker接口
works.add(w);
}
*addWorker 怎么判断线程池已经满了涉及到二进制操作,以后专门写博客来阐述。
submit()方式提交可以通过future获取任务执行结果,当调用future.get()时候如果任务执行未完成则会阻塞。
<T> Future<T> submit(Callable<T> task);
线程池如何关闭
线程池关闭的时候需要考虑其所处的状态,即如果有任务未执行完怎么办?什么时候应该关闭线程池。
常见的办法就是一个个遍历线程,如果不等待执行完就sotp停止线程或者中断现在执行的线程。
线程池关闭的状态中有几个中间状态可以根据 队列只否有正在执行的线程,有的话是否继续执行来划分。
线程池的状态有:Running 可以接收新的任务和执行队列任务,shutdown 不接收新的任务和已有队列任务还需要执行,stop 不接收新任务且 已有队列任务也停止(interrupt in-process task),terminate 线程池已经停止了
这里shutdown()与shutdownNow的区别就是shutdown只会interrputIdleWork,即只会终端没有非运行时的线程,正在执行的线程等待执行完。
代码区别如下:
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
使用篇
前面介绍了线程池的基本功能,这里就对其如何使用进行分析。
使用涉及到配置、启动、状态监控
线程池的配置选型
即对各种类型的任务使用什么样的策略。我暂时想到的任务类型划分标准有,1.执行时间长短 2.优先级 3.cpu型的还是IO型的 4.任务是否依赖其他特性
原则是对于CPU秘籍型的任务,线程池内的线程不宜过多避免频繁切换,可以设置为 N cpu+1
对于IO密集型的任务,线程池内的线程可以设置为2*N cpu
对于优先级可以使用PriorityBlockingQueue队列,但是如果一直有高优先级的任务那么低优先级的任务永远执行不了。
对于执行时间过长的(比如数据库)需要一定时间才能返回所以空闲时间比较长,这样的话可以把线程数量设置大一些。
线程池的监控
我们想监控线程池所有线程是否执行完,线程池里的线程使用状态,已经完成的线程数。
ThreadPoolExecutor提供了一些变量来存储线程池的状态,比如taskCount(线程池),completedTaskCount,largestPoolSize(曾经创建过的最大线程数)。
public class ThreadPoolExecutor extends AbstractExecutorService {
private int largestPoolSize;
private long completedTaskCount;
}
写完后的想法
1.通过从线程池的维度主动检索其知识来理解execute(),submit()方法,这种方式层次清晰,在信息维度就是从高纬往低维度去找是一种好的方式。
2.任何框架的描述都可以自己提出很多的问题, 原理、结构、如何使用等等,通过这些提问来掌握知识是一种很好的办法,从另一角度来说你能够提出多少问题你对这一领域抽象层次就了解多少。
3.至于各种线程池还没有阐述分析等待下一篇吧。