1. 线程池
1.1 使用线程池的好处
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处:
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。
1.2 Executor 框架
1.2.1 简介
Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。
Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。
1.2.2 Executor 框架结构(主要由三大部分组成)
1) 任务( Runnable / Callable )
执行任务需要实现的Runnable 接口 或 Callable 接口。 Runnable 接口或 Callable 接口 实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。
2) 任务的执行( Executor )
这里提了很多底层的类关系,但是,实际上我们需要更多关注的是ThreadPoolExecutor这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。
ThreadPoolExecutor类描述:
ScheduledThreadPoolExecutor类描述:
3) 异步计算的结果( Future )
Executor 框架的使用示意图
1.3 (重要)ThreadPoolExecutor 类简单介绍
线程池实现类ThreadPoolExecutor 是 Executor 框架最核心的类。
1.3.1 ThreadPoolExecutor 类分析
ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是 什么),这里就不贴代码讲了,比较简单。
下面这些对创建非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。
ThreadPoolExecutor3 个最重要的参数:
下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》):
ThreadPoolExecutor饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor定义一些策略:
举个例子:
推荐使用ThreadPoolExecutor构造函数创建线程池
在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
为什么呢?
另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
方式二:通过 Executor 框架的工具类 Executors 来实现
我们可以创建三种类型的 ThreadPoolExecutor:
FixedThreadPool
SingleThreadExecutor
CachedThreadPool
对应 Executors 工具类中的方法如图所示:
1. 4 (重要)ThreadPoolExecutor 使用示例
我们上面讲解了 Executor 框架以及ThreadPoolExecutor类,下面让我们实战一下,来通过写一个ThreadPoolExecutor的小 Demo 来回顾上面的内容
1.4.1 示例代码: Runnable + ThreadPoolExecutor
首先创建一个Runnable接口的实现类(当然也可以是Callable我们上面也说了两者的区别。)
MyRunnable.java
编写测试程序,我们这里以阿里巴巴推荐的使用ThreadPoolExecutor构造函数自定义参数的方式来创建线程池。
ThreadPoolExecutorDemo.java
可以看到我们上面的代码指定了:
Output:
线程池原理分析
我们通过代码输出结果可以看出:线程首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会)
现在,我们就分析上面的输出内容来简单分析一下线程池原理。为了搞懂线程池的原理,我们需要首先分析一下 execute 方法。我们使用executor.execute(worker) 来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:
通过下图可以更好地对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。
addWorker这个方法主要用来创建新的工作线程,如果返回true说明创建和启动工作线程成功,否则的话返回的就是false。
没搞懂的话,也没关系,可以看看我的分析:
1.4.2 几个常见的对比
Runnable vs Callable
Runnable.java
Callable.java
execute() vs submit()
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否; 2. submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get() 方法来获取返回值, get() 方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
我们以AbstractExecutorService 接口中的一个 submit 方法为例子来看看源代码:
上面方法调用的newTaskFor方法返回了一个FutureTask 对象。
我们再来看看execute() 方法:
shutdown() VS shutdownNow()
shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN 。线程池不再接受新任务了,但是队列里的任务得执行完毕。
shutdownNow(): 关闭线程池,线程的状态变为 STOP 。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
isTerminated() VS isShutdown()
isShutDown 当调用 shutdown() 方法后返回为 true。
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
加餐: Callable + ThreadPoolExecutor 示例代码
MyCallable.java
CallableDemo.java
Output:
1.5 几种常见的线程池详解
1.5.1 FixedThreadPool
1.5.1.1 介绍
FixedThreadPool被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现:
另外还有一个 FixedThreadPool 的实现方法,和上面的类似,所以这里不多做阐述:
从上面源代码可以看出新创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。
1.5.1.2 执行任务过程介绍
FixedThreadPool 的 execute() 方法运行示意图(该图片来源:《Java 并发编程的艺术》):
上图说明:
1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入LinkedBlockingQueue;
3. 线程池中的线程执行完 手头的任务后,会在循环中反复从LinkedBlockingQueue中获取任务来执行;
为什么不推荐使用 FixedThreadPool ?
FixedThreadPool 使用无界队列 LinkedBlockingQueue (队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :
1.5.2 SingleThreadExecutor 详解
1.5.2.1 介绍
CachedThreadPool是一个会根据需要创建新线程的线程池。下面通过源码来看看
CachedThreadPool 的实现:
CachedThreadPool 的 corePoolSize被设置为空(0), maximumPoolSize 被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时, CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽cpu 和内存资源。
1.5.2.2 执行任务过程介绍
CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):
上图说明:
1.5.2.3 为什么不推荐使用 CachedThreadPool ?
CachedThreadPool 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
1.6ScheduledThreadPoolExecutor 详解
ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者定期执行任务。 这个在实际项目中基本不会被用到,因为有其他方案选择比如quartz 。大家只需要简单了解一下它的思想。
1.6.1 简介
综上,在 JDK1.5 之后,你没有理由再使用 Timer 进行任务调度了。
1.6.2 运行机制
ScheduledThreadPoolExecutor的执行主要分为两大部分:
ScheduledThreadPoolExecutor为了实现周期性的执行任务,对 ThreadPoolExecutor 做了如下修改:
使用DelayQueue作为任务队列;
获取任务的方不同
执行周期任务后,增加了额外的处理
1.6.3ScheduledThreadPoolExecutor 执行周期任务的步骤
1.7 线程池大小确定
线程池数量的确定一直是困扰着程序员的一个难题,大部分程序员在设定线程池大小的时候就是随心而定。
很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。不清楚什么是上下文切换的话,可以看我下面的介绍。
类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都 会有问题,合适的才是最好。
如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。
但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
有一个简单并且适用面比较广的公式:
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。