为什么要用线程池?
简洁的答两点就行。
-
降低系统资源消耗。
-
提高线程可控性。
如何创建使用线程池?
JDK8提供了五种创建线程池的方法:
1.创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
2.(JDK8新增)会根据所需的并发数来动态创建和关闭线程。能够合理的使用CPU进行对任务进行并发操作,所以适合使用在很耗时的任务。
注意返回的是ForkJoinPool对象。
什么是ForkJoinPool:
使用一个无限队列来保存需要执行的任务,可以传入线程的数量;不传入,则默认使用当前计算机中可用的cpu数量;使用分治法来解决问题,使用fork()和join()来进行调用。
3.创建一个可缓存的线程池,可灵活回收空闲线程,若无可回收,则新建线程。
4.创建一个单线程的线程池。
5.创建一个定长线程池,支持定时及周期性任务执行。
上层源码结构分析
Executor结构:
Executor
一个运行新任务的简单接口
ExecutorService
扩展了Executor接口。添加了一些用来管理执行器生命周期和任务生命周期的方法
AbstractExecutorService
对ExecutorService接口的抽象类实现。不是我们分析的重点。
ThreadPoolExecutor
Java线程池的核心实现。
ThreadPoolExecutor源码分析
属性解释
值的注意的是状态值越大线程越不活跃。
线程池状态的转换模型:
构造器
在向线程池提交任务时,会通过两个方法:execute和submit。
本文着重讲解execute方法。submit方法放在下次和Future、Callable一起分析。
execute方法:
总结一下它的工作流程:
-
当workerCount < corePoolSize,创建线程执行任务。
-
当workerCount >= corePoolSize&&阻塞队列workQueue未满,把新的任务放入阻塞队列。
-
当workQueue已满,并且workerCount >= corePoolSize,并且workerCount < maximumPoolSize,创建线程执行任务。
-
当workQueue已满,workerCount >= maximumPoolSize,采取拒绝策略,默认拒绝策略是直接抛异常。
通过上面的execute方法可以看到,最主要的逻辑还是在addWorker方法中实现的,那我们就看下这个方法:
addWorker方法
主要工作是在线程池中创建一个新的线程并执行
参数定义:
-
firstTask the task the new thread should run first (or null if none). (指定新增线程执行的第一个任务或者不执行任务)
-
core if true use corePoolSize as bound, else maximumPoolSize.(core如果为true则使用corePoolSize绑定,否则为maximumPoolSize。 (此处使用布尔指示符而不是值,以确保在检查其他状态后读取新值)。)
为什么需要持有mainLock?
因为workers是HashSet类型的,不能保证线程安全。
那w = new Worker(firstTask);如何理解呢
Worker.java
可以看到它继承了AQS并发框架还实现了Runnable。证明它还是一个线程任务类。那我们调用t.start()事实上就是调用了该类重写的run方法。
Worker为什么不使用ReentrantLock来实现呢?
tryAcquire方法它是不允许重入的,而ReentrantLock是允许重入的。对于线程来说,如果线程正在执行是不允许其它锁重入进来的。
线程只需要两个状态,一个是独占锁,表明正在执行任务;一个是不加锁,表明是空闲状态。
run方法又调用了runWorker方法:
总结一下runWorker方法的执行过程:
1、while循环中,不断地通过getTask()方法从workerQueue中获取任务
2、如果线程池正在停止,则中断线程。否则调用3.
3、调用task.run()执行任务;
4、如果task为null则跳出循环,执行processWorkerExit()方法,销毁线程workers.remove(w);
这个流程图非常经典:
除此之外,ThreadPoolExector还提供了tryAcquire、tryRelease、shutdown、shutdownNow、tryTerminate、等涉及的一系列线程状态更改的方法有兴趣可以自己研究。大体思路是一样的,这里不做介绍。
Worker为什么不使用ReentrantLock来实现呢?
tryAcquire方法它是不允许重入的,而ReentrantLock是允许重入的。对于线程来说,如果线程正在执行是不允许其它锁重入进来的。
线程只需要两个状态,一个是独占锁,表明正在执行任务;一个是不加锁,表明是空闲状态。
在runWorker方法中,为什么要在执行任务的时候对每个工作线程都加锁呢?
shutdown方法与getTask方法存在竞态条件.(这里不做深入,建议自己深入研究,对它比较熟悉的面试官一般会问)