Appearance
一、使用线程池的好处
池化技术:线程池、数据库连接池、Http连接池等等都是对这个思想的应用。
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
- 降低资源消耗。通过重复利用已创建的线程减低线程的创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等待线程的创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
二、Executor框架
1、简介
Executor框架是 java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。
补充:this逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能会引发令人疑惑的错误。
Executor框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor框架让并发编程变的更加简单。
2、Executor框架结构
任务(Runnable/Callable)
执行任务需要实现的
Runnable接口或Callable接口。这两个接口的实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行任务的执行(Executor)
任务执行机制的核心接口
Executor,以及继承自Executor接口的ExecutorService接口。ThreadPoolExecutor和ScheduledThreadPoolExecutor这两个关键类实现了ExecutorService接口。ThreadPoolExecutor这个类,这个类在我们实际使用线程池的过程中,使用频率非常高。异步计算的结果(Future)
Future接口以及该接受的实现类FutureTask都可以代表异步计算的结果。当我们把
Runnable接口或Callable接口的实现类提交给ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。调用submit()方法时会返回一个FutureTask对象
三、线程池实现类 ThreadPoolExecutor
1、ThreadPoolExecutor 类分析
java
public ThreadPoolExecutor(int corePoolSize, // 线程池的核心线程数量
int maximumPoolSize, // 线程池的最大线程数
// 当线程数大于核心线程数多余的空闲线程存活的最长时间
long keepAliveTime,
// 时间单位
TimeUnit unit,
// 任务队列,用来存储等待执行任务的队列
BlockingQueue<Runnable> workQueue,
// 线程工厂,用来创建线程,一般默认即可
ThreadFactory threadFactory,
// 拒绝策略,当提交任务过多而不能及时处理时,我们可以定制策略来处理任务
RejectedExecutionHandler handler) {
}ThreadPoolExecutor 三个最重要的参数:
corePoolSize:核心线程数定义了最小可同时运行的线程数量maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数workQueue:当新任务来的时候回显判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
其他参数:
keepAliveTime:当线程池中线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;unit:keepAliveTime参数的时间单位threadFactory: executor创建新线程的时候会用到handler:饱和策略,如果当前同时运行的线程数量达到最大线程数量并且队列也已经放满了任务时,ThreadPoolTaskExecutor定义了一些策略:AbortPolicy:抛出RejectedExecutionExeception来拒绝新任务的处理(默认)CallerRunsPolicy:调用执行自己的线程运行任务。这种策略会降低新任务的提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且不能丢弃任何一个任务请求的话,可以选择此策略DiscardPolicy: 不处理新任务,直接丢弃掉。DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求
2、 推荐使用ThreadPoolExecutor构造函数创建线程池
在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回线程池对象的弊端
FixedThreadPool和SingleThreadExecutor: 允许请求的队列长度为Integer.MAX_VALUE可能堆积大量的请求,从而导致 OOM。CachedThreadPool和ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。
四、几个常见的对比
1、Runnable 与 Callable
Runnable接口不会返回结果或抛出检查异常,但是Callable接口可以。所以,如果不需要返回结果或抛出异常推荐使用Runnable接口
Executors.callable()可以实现Runnable转为Callable
2、execute() 与 submit()
execute()方法用于提交不需要返回值的任务。所以无法判断任务是否被线程池执行成功与否。
submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个对象可以判断任务是否执行成功,并可以通过Future的get()方法获取返回值,get()方法会阻塞当前线程直到任务完成,get(long timeout, TimeUnit unit)方法会阻塞当前线程一段时间后立即返回,这时候可能任务没有执行完。
3、shutdown() 与 showdownNow()
shutdown(): 关闭线程池,线程池的状态变为SHUTDOWN。线程池不再接受新任务,但是队列里的任务得执行完毕。
shutdownNow(): 关闭线程池,线程池的状态变为STOP。线程池会终止当前正在运行的任务,并停止处理派对的任务并返回正在等待执行的 list
4、isTerminated() 与 isShutdown()
isShutDown(): 当调用shutDown()方法后返回true
isTerminated(): 当调用shutDown()方法后,并且所有提交的任务完成后返回true
五、几种常见的线程池
1、Fixed Thread Pool
可重用固定线程数的线程池
1.1 执行任务过程
- 如果当前运行数量小于
corePoolSize,如果再来新任务的话,就创建新的线程来执行任务; - 当前运行线程数等于
corePoolSize后,如果再来新任务,就加入LinkedBlockQueue - 线程池中的线程执行完手头任务后,会循环反复从
LinkedBlockQueue中获取任务执行
1.2 为什么不推荐使用该线程池
FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :
- 当线程池中的线程数达到
corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize; - 由于使用无界队列时
maximumPoolSize将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建FixedThreadPool的源码可以看出创建的FixedThreadPool的corePoolSize和maximumPoolSize被设置为同一个值。 - 由于 1 和 2,使用无界队列时
keepAliveTime将是一个无效参数; - 运行中的
FixedThreadPool(未执行shutdown()或shutdownNow())不会拒绝任务,在任务比较多的时候会导致OOM(内存溢出)。
5.2 SingleThreadExecutor
SingleThreadExecutor只有一个线程的线程池。该线程池极端情况下也会导致
OOM
5.3 CachedThreadPool
CachedThreadPool是一个会根据需要创建新线程的线程池该线程池极端情况下也会导致
OOM
六、线程池大小确定
如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
上下文切换:
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
有一个简单并且适用面比较广的公式:
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
