概述
5. 线程池
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,
如果线程数量超过了最大数量,超出数量的线程排队等候,等其它线程执行完毕, 再从队列中取出任务来执行。
他的主要特点为:线程复用;控制最大并发数;管理线程。
5.1 为什么要用线程池?
线程池提供了一个线程队列,队列中保存着所有等待状态的线程,避免了创建与销毁的额外开销,提高了响应
的速度。线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池有很多好处:
-
降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-
提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定
性,使用线程池可以进行统一的分配,调优和监控。
5.2 线程池体系结构
5.2.1 线程池的组成
一般的线程池主要分为以下 4 个组成部分:
-
线程池管理器:用于创建并管理线程池
-
工作线程:线程池中的线程
-
任务接口:每个任务必须实现的接口,用于工作线程调度其运行
-
任务队列:用于存放待处理的任务,提供一种缓冲机制
Java 中的线程池是通过 Executor 框架实现的,该框架用到了 Executor,Executors,ExecutorService,
ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。
- 线程池的体系架构
* java.util.concurrent.Executor : 负责线程的使用与调度的根接口
* |–ExecutorService 子接口: 线程池的主要接口
* |–ThreadPoolExecutor 线程池的实现类
* |–ScheduledExecutorService 子接口:负责线程的调度
* |–ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor, 实现 ScheduledExecutorService
5.2.2 ThreadPoolExecutor
5.2.3 ThreadPoolExecutor 的构造方法
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(),
defaultHandler);
}
-
corePoolSize:指定了线程池中的线程数量。
-
maximumPoolSize:指定了线程池中的最大线程数量。
-
keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
-
unit:keepAliveTime 的单位。
-
workQueue:任务队列,被提交但尚未被执行的任务。
-
threadFactory:线程工厂,用于创建线程,一般用默认的即可。
-
handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
5.2.3.1 拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务。同时等待队列也已经排满了,再也塞不下新任务了。这
时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
-
AbortPolicy:直接抛出异常,阻止系统正常运行。
-
CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的 任务。显然这样做不
会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
-
DiscardOldestPolicy:丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
-
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩
展 RejectedExecutionHandler 接口。
5.3 线程池创建方式
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行
线程的工具。真正的线程池接口是 ExecutorService。
为了便于跨大量上下文使用,此类提供了很多可调整的参数和扩展钩子 (hook)。但是,强烈建议程序员使用
较为方便的工具类 Executors 工厂方法 :
-
ExecutorService newFixedThreadPool() : 创建固定大小的线程池,可以进行自动线程回收
-
ExecutorService newCachedThreadPool() : 缓存线程池,线程数量不固定可以根据需求自动的更改数量。
-
ExecutorService newSingleThreadExecutor() : 创建单个线程池。线程池中只有一个线程
-
ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。
-
WorkStealingPoll():内部会创建 ForkJoinPool,利用 working-stealing 算法,并行地处理任务,不保证处理的
顺序
5.3.1 new FixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数
nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务, 则在有可用线程之
前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何 线程终止,那么一个新线程将代替
它执行后续的任务(如果需要)。在某个线程被显式地关闭之 前,池中的线程将一直存在。
5.3.2 new CachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行 很多短期异
步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造 的线程(如果线程可用)。
如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线
程。因此,长时间保持空闲的线程池不会使用任何资源。处理大量短时间工作任务的线程池
试图缓存线程并重用,当无缓存线程可用是,就会创建新的工作线程
如果线程闲置的时间超过了阈值,则会被终止并移除缓存
系统长时间闲置的时候,不会消耗什么资源
5.3.3 new SingleThreadExecutor
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后
(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
5.3.4 new ScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。定时或者周期性的工作调度
5.3.5 new WorkStealingPoll()
内部会创建 ForkJoinPool,利用 working-stealing 算法,并行地处理任务,不保证处理的顺序
5.4 Java 线程池工作过程
-
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也
不会马上执行它们。
-
当调用 execute()方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize(线程池的基本大小),那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize(线程池中允许的最大线程数),
那么还是要创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常
RejectExecutionException。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运 行的线程数大于
corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
5.5 线程池的状态
RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
SHUTDOWN:不在接受新提交的任务,但是可以处理存量任务
STOP:不在接受新提交的任务,也不处理存量任务
TIDYING:所有的任务已经终止
TERMINATED:terminated 方法执行完后进入该状态
5.6 线程池大小的选择
Cup 密集型:线程数=按照核数或者核实+1
IO 密集型:线程数=cpu 核数*(1+平均等待时间/平均工作时间)
5.7 ForkJoinPool 分支/合并框架 工作窃取
5.7.1 Fork/Join 框架
就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任
务运算的结果进行 join 汇总。
Work-stealing 算法:某个线程冲其他队列里窃取任务来执行
5.7.2 Fork/Join 和线程池的区别
Fork/Join 框架采用“工作窃取”模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,
并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
相对于一般的线程池实现,fork/join 框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如
果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在 fork/join 框架实现
中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚
未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。
/**
* 求和,1-n的和
*/
public class TestForkJoinPool {
public static void main(String[] args) {
// 创建forkjoinpool
ForkJoinPool pool = new ForkJoinPool();
// 创建forkjoin的任务
ForkJoinTask<Long> task = new ForkJoinSum(1L,1000L);
// 获得运算结果
Long sum = pool.invoke(task);
System.out.println(sum);
}
}
class ForkJoinSum extends RecursiveTask<Long> {
private long start;
private long end;
private long flag = 500; // 临界值,任务拆分到多个以后就不再拆分
public ForkJoinSum(long start, long end) {
this.start=start;
this.end=end;
}
@Override
protected Long compute() {
long length = end -start;
if (length<=flag){ // 没有到临界值的不用分任务计算
long sum = 0L;
for (long i = start; i <= end; i++) {
sum+=i;
}
return sum;
}else{ // 到达临界值
long middle = (start+end)/2;
ForkJoinSum left = new ForkJoinSum(start, middle);
ForkJoinSum right = new ForkJoinSum(middle + 1, end);
left.fork();
right.fork();
return left.join()+right.join();
}
}
}
最后
以上就是鲤鱼月亮为你收集整理的线程池简介5. 线程池的全部内容,希望文章能够帮你解决线程池简介5. 线程池所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复