简介
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后已经创建好的线程会在队列中获取并自动启动这些任务。
为什么要使用线程池
- 提高线程的可管理性,如果使用
new Thread
的方式,那么每一个任务就会新建一个线程,线程数量不可控,使用线程池,可以明确池子中创建多少个线程。 - 省去线程创建和销毁时占用的资源。
- 降低线程的执行时间,省去了线程创建和销毁的时间。使用
new Thread
的方式,任务执行时间为线程创建T1 + 任务执行T2 + 线程销毁T3,使用线程池为只有T2。
实现自己的线程池
线程池中保存一个线程数组,线程数组中的线程都是不断运行的,这些线程会尝试从阻塞队列中获取任务,然后执行任务。
public class MyThreadPool2 {
// 线程池中默认线程的个数为5
private static int WORK_NUM = 5;
// 队列默认任务个数为100
private static int TASK_COUNT = 100;
// 工作线程组
private WorkThread[] workThreads;
// 任务队列,作为一个缓冲
private final BlockingQueue<Runnable> taskQueue;
private final int worker_num;//用户在构造这个池,希望的启动的线程数
// 创建具有默认线程个数的线程池
public MyThreadPool2() {
this(WORK_NUM,TASK_COUNT);
}
// 创建线程池,worker_num为线程池中工作线程的个数
public MyThreadPool2(int worker_num,int taskCount) {
if (worker_num<=0) worker_num = WORK_NUM;
if(taskCount<=0) taskCount = TASK_COUNT;
this.worker_num = worker_num;
taskQueue = new ArrayBlockingQueue<>(taskCount);
workThreads = new WorkThread[worker_num];
for(int i=0;i<worker_num;i++) {
workThreads[i] = new WorkThread();
workThreads[i].start();
}
Runtime.getRuntime().availableProcessors();
}
// 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
public void execute(Runnable task) {
try {
taskQueue.put(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 销毁线程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁
public void destroy() {
// 工作线程停止工作,且置为null
System.out.println("ready close pool.....");
for(int i=0;i<worker_num;i++) {
workThreads[i].stopWorker();
workThreads[i] = null;//help gc
}
taskQueue.clear();// 清空任务队列
}
// 覆盖toString方法,返回线程池信息:工作线程个数和已完成任务个数
@Override
public String toString() {
return "WorkThread number:" + worker_num
+ " wait task number:" + taskQueue.size();
}
/**
* 内部类,工作线程
*/
private class WorkThread extends Thread{
@Override
public void run(){
Runnable r = null;
try {
while (!isInterrupted()) {
r = taskQueue.take();
if(r!=null) {
System.out.println(getId()+" ready exec :"+r);
r.run();
}
r = null;//help gc;
}
} catch (Exception e) {
// TODO: handle exception
}
}
public void stopWorker() {
interrupt();
}
}
}
原理
ThreadPoolExecutor
jdk所有线程池实现的父类
ThreadPoolExecutor中各个参数
private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(比如线程池大小
//、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集
private volatile long keepAliveTime; //线程存货时间
private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间
private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int maximumPoolSize; //线程池最大能容忍的线程数
private volatile int poolSize; //线程池中当前的线程数
private volatile RejectedExecutionHandler handler; //任务拒绝策略
private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程
private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount; //用来记录已经执行完毕的任务个数
假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。
因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;
当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;
如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;
然后就将任务也分配给这4个临时工人做;
如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。
当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。
提交任务
execute(Runnable command) 不需要返回
Future submit(Callable task) 需要返回
处理流程分析
执行execute方法,传入Runnable对象,出现的各种情况:
- 当当前线程数小于corePoolSize时,就直接创建一个新的线程,并且把任务作为该线程的第一个任务执行。
- 当当前线程数等于corePoolSize时,而BlockingQueue没有满时,就把任务加到阻塞队列中。
- 当当前线程数等于corePoolSize时,BlockingQueue也满时,但是当前线程数小于maximumPoolSize,就会新建线程来处理。
- 当当前线程数等于corePoolSize时,BlockingQueue也满时,线程数也等于maximumPoolSize时,就执行饱和策略。
饱和策略
- AbortPolicy :丢弃任务并抛出RejectedExecutionException异常。
- CallerRunsPolicy :使用调用者线程处理任务。
- DiscardPolicy : 直接丢弃。
- DiscardOldestPolicy : 丢弃对列中最老的任务。
线程池的状态
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
当创建线程池后,初始时,线程池处于RUNNING状态;
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。`
execute方法
if (command == null)
throw new NullPointerException(); //空指针校验
int c = ctl.get(); //获得线程池状态
if (workerCountOf(c) < corePoolSize) { //当前存在线程小于corePoolSize,直接创建添加线程
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { //阻塞队列没有满时,往阻塞队列中推入任务
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false)) //小于maximumPoolSize时,也会新建一个线程去尝试
reject(command);
}
关闭线程池
shutdownNow():设置线程池的状态,还会尝试停止正在运行或者暂停任务的线程
shutdown()设置线程池的状态,只会中断所有没有执行任务的线程
线程池配置
线程池的配置,主要是配置CorePoolSize的大小,我们需要根据自身的业务来配置。
- 计算密集型(CPU密集型):这类任务要进行大量的计算,消耗CPU资源。比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。由于CPU存在页缺失的情况,当出现页缺失时线程会阻塞,因此,对于计算密集型的,可以把CorePoolSize配置为CPU数+1;
- IO密集型,IO密集包括文件读取,网络IO,数据库,RPC。这类操作普遍用时长,因此一般配置为cpu * 2;