故事
某公司
一、
工作内容:搬箱子。
每天搬一个箱子(搬完就下班,任性),新来的箱子怎么办?招人!招人!某公司急需搬箱子员工若干(新来的箱子数量)名,
朝九晚五,工作轻松,有意者联系。每天箱子数量不固定,好了公司一天很多时间不是结算工资就是招人。
二、
老板想了个办法,调研一下市场,看看公司每个月能来多少个箱子,每天需要处理多少箱子,每个员工一天最多处理多少个箱子。
回来做一下整理,开始想办法,箱子随时都回来?大半夜让我起来上班怎么能行,建个仓库来了就放在哪里上班时间再弄。每个
员工每天不能搬一个箱子就下班了,一天搬....就搬6个吧。老板拍了拍肚子上的一块腹肌开心的笑了,第二天就去招固定员工签
合同。
三、
公司几个员工开始上班,“滴滴”,来箱子了,员工甲冲了上去开始工作;滴滴”,又来箱子了,员工甲冲了上去开始工作.......
核心工作人员都开始忙了,新来的箱子就放在了仓库。如果仓库也满了,老板就会去把轮休的几个员工叫回来加快处理。还忙不
过来,老板就会想别的解决办法了。
类比
- 公司 = 应用程序
- 员工 = 工作线程
- 招人 = 创建线程
- 箱子 = 任务
- 仓库 = 任务队列
上述故事中公司刚开始对于工作的处理方式,呆呆的,箱子少轻轻松松,箱子多忙的一批。类似传统BIO编程中socket接入的处理方式,监听到新来的socket就创建新线程处理socket IO读写。
当连接数小的时候可以正常运行,当连接数大的时候会创建很多的线程占用内存(可能导致内存溢出),如此多的线程要运行会导致频繁的CPU调度,即频繁的上下文切换(将当前CPU中运行的线程现场保存,按照调度方式找到下一个幸运的线程来运行)。
如何解决上述问题呢?——使用线程池
Java线程池
通过ThreadPoolExecutor进行分析
Executor接口
作为线程池的顶级接口,只提供了一个方法。
void execute(Runnable command)
在未来某个时间执行给定的命令。提交任务到线程池。
ExecutorService接口
提供了操作执行程序的方法:
-
添加了线程池生命周期管理的方法。判断当前执行程序状态是否关闭、关闭后任务是否都完成。shutdown()是拒绝新任务,shutdownNow()拒绝新任务的提交并且试图停止正在执行的所有任务,暂停处理正在等待的任务,返回等待执行的任务列表。
-
提交一个带返回结果(Future)的任务(Runnable、Callable)的方法submit(…)。
AbstractExecutorService抽象类
提供ExecutorService的默认实现。此类使用 newTaskFor 返回的 RunnableFuture 实现 submit、invokeAny 和 invokeAll(批量提交) 方法,实现了如何提交任务和提交的任务类型。
ThreadPoolExecutor类
是Java线程池框架的主要实现类。
构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize - 基本大小,池中所保存的线程数,包括空闲线程。核心线程数
maximumPoolSize - 最大大小,池中允许的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
threadFactory - 执行程序创建新线程时使用的工厂。
handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
keepAliveTime指的是当任务执行结束大于核心线程数的空闲线程的存活时间,验证代码如下,等执行一会后,手动在控制台输入111回车即可知道当前池中当前的线程数:
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1,
3,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1));
//指定空闲线程存活时间2s
for (int i = 0; i < 20; i++) {
try {
executor.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("running....");
System.out.println(executor.getPoolSize());
});
} catch (RejectedExecutionException e) {
System.out.println(e.getMessage());
}
}
Scanner scanner = new Scanner(System.in);
String res = scanner.nextLine();
if (res.equals("111")) {
System.out.println(executor.getPoolSize());
}
}
线程池运行状态
- RUNNING:接受新任务并处理排队任务
- SHUTDOWN:不接受新任务,但处理排队任务
- STOP:不接受新任务,不处理排队任务,并中断正在进行的任务
- TIDYING:整理,所有任务都已终止,workerCount为零,线程转换到状态TIDYING将运行terminate()钩子方法
- TERMINATED:终止,terminate()已完成
状态切换条件:
RUNNING -> SHUTDOWN:在调用shutdown()时,可能隐含在finalize()中
(RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()
SHUTDOWN -> TIDYING:当队列和池都为空时
STOP -> TIDYING:当池为空时
TIDYING -> TERMINATED: terminate()钩子方法完成后
提交任务
1、如果当前运行线程数小于corePoolSize,创建新的线程执行任务。
2、否则添加到阻塞任务队列。
3、如果任务队列满了,尝试创建新线程执行任务,如果当前工作线程数>maximumPoolSize线程池允许的最大线程数。则执行RejectedExecutionHandler。
拒绝策略
- AbortPolicy,中止策略,直接抛出异常。
- CallerRunsPolicy,调用者执行,由调用线程执行任务r.run()。
- DiscardPolicy,直接删除任务。
- DiscardOldestPolicy,废弃最旧的任务(排队最久的,即队列头元素),然后重试execute(重复此过程)。
参考资料
- Java 1.8 API 中文文档
- JDK源码