(一)线程池简介:
1构建线程池原因:
- 当一个程序里面有多个线程同时运行时,即使要处理的任务可能处于闲置状态,但线程还在运行,若销毁线程则可能会造成资源的频繁消耗,并且多个线程会不方便管理,此时,需要将线程放置于一个“池”中,重复利用线程,当有任务时把任务放入执行即可;
- 线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息
池化技术:线程池,数据库连接池,HTTP连接池,其思想是减少每次获取资源是对资源的消耗,提高资源的利用率;
线程池的基本结构如下图:
由图片可知,线程池的好处为:
- 通过利用已经创建的资源处理任务,减少频繁创建,销毁线程带来的资源消耗
- 将要处理的任务队列直接放入线程池处理即可,不需要再等待线程创建即可处理任务,提高响应速度
- 线程是系统的稀缺资源,减少其创建和消耗不仅能减少资源的消耗,还可以使用线程池可以进行统一的分配,调优和监控,是系统更稳定
2.具体代码实现:
线程池类:
线程池类中用于存放线程并初始化线程,并提交任务到任务线程
public class ThreadPool { //添加的线程
private List<Runnable>listRun=new LinkedList<>(); //保存提交的线程
private List<WorkPool>listWork=new LinkedList<>(); //处理任务的线程
public ThreadPool(int size){ //初始化线程个数
for(int i=0;i<size;i++) { //启动任务线程
WorkPool wt=new WorkPool(listRun);
new Thread(wt).start();
}
}
public void addWork(Runnable runnable) { // 提交任务到任务线程
synchronized (listRun)
{
listRun.add(runnable);
listRun.notify(); // 唤醒任务线程
}
}
}
任务队列类:
继承Runnable重写run方法,使没有任务时线程处于休眠状态,有任务时可以取出任务并执行;
public class WorkPool implements Runnable{ //处理提交任务的线程
private List<Runnable>listRun; //待处理的任务
public WorkPool(List<Runnable>listRun) {
this.listRun=listRun;
}
public void run() {
Runnable runnable=null;
while (true) {
synchronized (listRun) {
System.out.println(Thread.currentThread().getName() + " 线程进入等待...");
System.out.println(Thread.currentThread().getName() + " 执行任务");
while (listRun.size() <= 0) {
try {
listRun.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
runnable=listRun.get(0);
System.out.println(Thread.currentThread().getName() + " 执行任务");
// 取出ListRun线程任务让该线程执行
}
runnable.run();
}
}
}
Test类:
public class Test {
public static void main(String[] args) throws InterruptedException {
ThreadPool tp = new ThreadPool(5); // 添加线程
for (int i = 20; i < 20; i++) {
tp.addWork(new Work(i)); // 添加任务
}
}
}
class Work implements Runnable { //新任务类
int workint;
public Work(int i) {
this.workint = i;
}
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程处理完任务" + workint);
}
}
以上三个类大致模拟了线程池的原理,其中要对listRun加锁
(二)Executor框架和Fork/Join框架:
1.Excutor:
JDK1.5时引入的,引入该接口的主要目的是解耦任务本身和任务的执行。我们之前通过线程执行一个任务时,往往需要先创建一个线程,然后调用线程的start方法来执行任务。而Executor接口解耦了任务和任务的执行,该接口只有一个方法,入参为待执行的任务
通过Executor 来启动线程比使用 Thread 的start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题
- this逃逸是指再构造函数返回引用之前,其它对象就有该对象的引用,可能会调用尚未构造完全的对象的方法
public interface Executor {
/**
* 执行给定的Runnable任务.
* 根据Executor的实现不同, 具体执行方式也不相同.
void execute(Runnable command);
}
有三个实现类:
- 同步执行任务:DirectExecutor,对于传入的任务,只有执行完成后execute才会返回
- 异步执行任务:ThreadPerTaskExecutor ,对于每个任务,执行器都会创建一个新的线程去执行任务。
- 对任务进行排队执行: SerialExecutor,会对传入的任务进行排队(FIFO顺序),然后从队首取出一个任务执行
2. ExecutorService
Executor接口提供的功能很简单,为了对它进行增强,出现了ExecutorService接口,ExecutorService继承了Executor,它在Executor的基础上增强了对任务的控制,同时包括对自身生命周期的管理,主要有四类:
- 关闭执行器,禁止任务的提交;
- 监视执行器的状态;
- 提供对异步任务的支持;
- 提供对批处理任务的支持。
3.ScheduledExecutorService
ScheduledExecutorService提供了一系列schedule方法,可以在给定的延迟后执行提交的任务,或者每个指定的周期执行一次提交的任务,该接口继承了ExecutorService
利用Exectors创建不同场景下的线程池:
- newScheduledThreadPool (int corePoolSize)创建“可调度线程池”
- 定期会议周期性的工作调度,有多个线程
- newSingleThreadExecutor 创建“单线程化线程池”
- 创建唯一的工作者线程执行任务,如果线程异常结束,就会有另一个线程取代
- newFixedThreadPool (int nThreads)创建“固定数量的线程池”
- newCachedThreadPool 创建“可缓存线程池”:(处理大量短时间工作任务的线程池)
- 试图缓存线程并重用,当无缓存线程可用时,就会创建新的线程
- 若线程长时间限制或处于阈值,则会被移除
- 系统长时间闲置,也不会消耗什么资源
- newWorkStealingPool:内部构建ForkJoinPool,利用workingstealing并行处理任务,不保证处理顺序
Fork/Join框架:
由于有些队列的任务线程已完成,但有些队列的线程还处于运行,造成有些线程闲置
因此,把大任务分割为若干个小任务并行执行,最终汇总到每个任务结果得到大任务结果的框架
- 运用的分治思想:“分治”,所谓“分治”就是将一个难以直接解决的大问题,分割成一些规模较小的子问题,以便各个击破,分而治
- 使用work_stealing算法:某个线程从其它队列中窃取任务来执行,并将任务使用双端队列(deque)取出
该框架主要涉及三大核心组件:ForkJoinPool(线程池)、ForkJoinTask(任务),ForkJoinWorkerThread(工作线程),外加WorkQueue(任务队列):
- ForkJoinPool:ExecutorService的实现类,负责工作线程的管理、任务队列的维护,以及控制整个任务调度流程;
- ForkJoinTask:Future接口的实现类,fork是其核心方法,用于分解任务并异步执行;而join方法在任务结果计算完毕之后才会运行,用来合并或返回计算结果;
- ForkJoinWorkerThread:Thread的子类,作为线程池中的工作线程(Worker)执行任务;
- WorkQueue:任务队列,用于保存任务;
- fork操作:当你把任务分成更小的任务和使用这个框架执行它们。
- join操作:当一个任务等待它创建的任务的结束。
第一步分割任务。首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。
第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据
(三)实现类ThreadPoolExecutor
- 当有任务需要执行时,线程池会给该任务分配线程,如果当前没有可用线程,一般会将任务放进一个队列中,当有线程可用时,再从队列中取出任务并执行
Executors可以创建不同类型的线程池,其中有以下几个参数:
maximumPoolSize限定了整个线程池的大小,
corePoolSize限定了核心线程池的大小,
corePoolSize≤maximumPoolSize(当相等时表示为固定线程池);
maximumPoolSize-corePoolSize表示非核心线程池。
1.配置核心线程池的大小:
- 如果任务是 CPU 密集型(需要进行大量计算、处理,比如计算圆周率、对视频进行高清解码等等),则应该配置尽量少的线程,比如 CPU 个数 + 1,这样可以避免出现每个线程都需要使用很长时间但是有太多线程争抢资源的情况;
- 如果任务是 IO密集型(主要时间都在 I/O,即网络、磁盘IO,CPU 空闲时间比较多),则应该配置多一些线程,比如 CPU 数的两倍,这样可以更高地压榨 CPU。
公式:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
2.ThreadPoolExecutor一共定义了5种线程池状态
- RUNNING : 接受新任务, 且处理已经进入阻塞队列的任务
- SHUTDOWN : 不接受新任务, 但处理已经进入阻塞队列的任务
- STOP : 不接受新任务, 且不处理已经进入阻塞队列的任务, 同时中断正在运行的任务
- TIDYING : 所有任务都已终止, 工作线程数为0, 线程转化为TIDYING状态并准备调用terminated方法
- TERMINATED : terminated方法已经执行完成
如图所示:
3.工作原理:
3.1Work工作线程:
当我们向线程池提交一个任务时,将创建一个工作线程——Worker,它是ThreadPoolExecutor定义的内部类,同时封装着Runnable任务和执行该任务的Thread对象,称它为工作线程,它也是ThreadPoolExecutor唯一需要进行维护的线程,
每个Worker对象都有一个Thread线程对象与它相对应,当任务需要执行的时候,实际是调用内部Thread对象的start方法,而Thread对象是在Worker的构造器中通过 getThreadFactory().newThread(this)方法创建的,创建的Thread将Worker自身作为任务,所以当调用Thread的start方法时,最终实际是调用了Worker.run()方法,该方法内部委托给runWorker方法执行任务
execute方法:
execute方法内部内部调用了addWorker方法来添加工作线程并调用runWork执行任务:
- (1).调用了addWorker方法来添加工作线程并执行任务,整个addWorker的逻辑并不复杂,分为两部分:
- 第一部分是一个自旋操作,主要是对线程池的状态进行一些判断,如果状态不适合接受新任务,或者工作线程数超出了限制,则直接返回false。
- 第二部分才真正去创建工作线程并执行任务:首先将Runnable任务包装成一个Worker对象,然后加入到一个工作线程集合中(名为workers的HashSet),最后调用工作线程中的Thread对象的start方法执行任务,其实最终是委托到Worker的下面方法执行:
- (2).runWoker用于执行任务,整体流程如下:
- while循环不断地通过getTask()方法从队列中获取任务(如果工作线程自身携带着任务,则执行携带的任务);
- 控制执行线程的中断状态,保证如果线程池正在停止,则线程必须是中断状态,否则线程必须不是 中断状态;
- 调用task.run()执行任务;
- 处理工作线程的退出工作
- (3)processWorkerExit的作用就是将该退出的工作线程清理掉,然后看下线程池是否需要终止。
submit方法
ExecutorService的核心方法是submit方法——用于提交一个待执行的任务.execute的执行流程如下:
- 如果工作线程数小于核心线程池上限(CorePoolSize),则直接新建一个工作线程并执行任务;
- 如果工作线程数大于等于CorePoolSize,则尝试将任务加入到队列等待以后执行。如果加入队列失败了(比如队列已满的情况),则在总线程池未满的情况下(CorePoolSize ≤ 工作线程数 < maximumPoolSize)新建一个工作线程立即执行任务,否则执行拒绝策略。
3.2任务队列
阻塞队列就是在我们构建ThreadPoolExecutor对象时,在构造器中指定的。由于队列是外部指定的,所以根据阻塞队列的特性不同,Worker工作线程调用getTask方法获取任务的执行情况也不同
- 1.直接提交:即直接将任务提交给等待的工作线程,这时可以选择SynchronousQueue。因为SynchronousQueue是没有容量的,而且采用了无锁算法,所以性能较好,但是每个入队操作都要等待一个出队操作,反之亦然。
使用SynchronousQueue时,当核心线程池满了以后,如果不存在空闲的工作线程,则试图把任务加入队列将立即失败(execute方法中使用了队列的offer方法进行入队操作,而SynchronousQueue在调用offer时如果没有另一个线程等待出队操作,则会立即返回false),因此会构造一个新的工作线程(未超出最大线程池容量时)。
由于,核心线程池是很容易满的,所以当使用SynchronousQueue时,一般需要将 maximumPoolSizes设置得比较大,否则入队很容易失败,最终导致执行拒绝策略,这也是为什么Executors工作默认提供的缓存线程池使用SynchronousQueue作为任务队列的原因。
- 2.无界任务队列:无界任务队列我们的选择主要有LinkedTransferQueue、LinkedBlockingQueue(近似无界,构造时不指定容量即可),从性能角度来说LinkedTransferQueue采用了无锁算法,高并发环境下性能相对更好,但如果只是做任务队列使用相差并不大。
使用无界队列需要特别注意系统资源的消耗情况,因为当核心线程池满了以后,会首先尝试将任务放入队列,由于是无界队列所以几乎一定会成功,那么系统瓶颈其实就是硬件了。如果任务的创建速度远快于工作线程处理任务的速度,那么最终会导致系统资源耗尽。Executors工厂中创建固定线程池的方法内部就是用了LinkedBlockingQueue。
- 3.有界任务队列:有界任务队列,比如ArrayBlockingQueue ,可以防止资源耗尽的情况。当核心线程池满了以后,如果队列也满了,则会创建归属于非核心线程池的工作线程,如果非核心线程池也满了 ,才会执行拒绝策略
3.3拒绝策略
ThreadPoolExecutor在以下两种情况下会执行拒绝策略:
- 当核心线程池满了以后,如果任务队列也满了,首先判断非核心线程池有没满,没有满就创建一个工作线程(归属非核心线程池), 否则就会执行拒绝策略;
- 提交任务时,ThreadPoolExecutor已经关闭了。
所谓拒绝策略:就是在构造ThreadPoolExecutor时,传入的RejectedExecutionHandler对象
ThreadPoolExecutor一共提供了4种拒绝策略:
- AbortPolicy(默认):抛出一个RejectedExecutionException异常
- DiscardPolicy:无为而治,什么都不做,等任务自己被回收
- DiscardOldestPolicy:丢弃任务队列中的最近一个任务,并执行当前任务
- CallerRunsPolicy:以自身线程来执行任务,这样可以减缓新任务提交的速度