线程池是使用场景最多的并发框架,几乎所有需要异步和并发执行任务的程序都可以使用线程池。类似于数据库链接池,使用线程池能为程序带来如下好处:
1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2、提高相应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3、提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
线程池的实现原理
与数据库连接池类似,线程池也一句话概括:通过将事先创建好的线程存放起来,在需要的时候直接拿过来使用就可以了。但是,为了提高线程池的性能,实际的线程池要比这复杂得多。当接受到一个新任务到线程池的时候,线程池会有如下处理流程:
1.线程池首先判断核心线程池中线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果都在执行任务,也就是没有空闲线程的话就进入下个流程
2.线程池继续判断工作队列是否已经满了。如果工作队列没有满,则把新提交的任务放入该工作队列中,如果工作队列已经满了,则进入下个流程
3.线程池判断线程池中的线程是否都处于工作状态。如果不是,则创建一个新的线程执行提交的任务,如果是,则执行饱和策略。
执行流程:
在Java中实现的线程池的核心类是ThreadPoolExecutor,该类的execute方法的执行流程就是上面的过程。注意三个关键字:核心线程池、工作队列和饱和策略
细化到ThreadPoolExecutor执行execute方法的过程,对上面的过程补充如下:
- 核心线程池对应corePoolSize变量的值,如果运行的线程小于corePoolSize,则创建新的线程执行任务(这个过程需要获取全局锁);
- 如果运行的线程大于corePoolSize,则将任务加入BlockingQueue(对应工作队列,阻塞队列);
- 如果无法加入则创建新的线程执行任务,这个步骤中,如果创建新线程后当前运行的线程数大于maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExcution()方法。
ThreadPoolExecutor为了避免执行新提交的任务获取全局锁,ThreadPoolExecutor在创建后会执行一个预热过程,所谓预热就是让当前运行的线程数大于等于corePoolSize。这样,后面新提交的任务都将直接加入到BlockingQueue。而这个过程是不需要获取全局锁的,自然就能提高线程池的性能。
源码分析根据上面的分析,在查看ThreadPoolExecutor源码来加深理解
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果当前正在运行的线程数小于corePoolSize,则创建新的线程
//执行当前任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果当前运行的线程数大于等于corePoolSize或者线程创建失败
//则把当前任务放入工作队列
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);
}
//如果线程池任务无法加入到工作队列(说明工作队列满了)
//创建一个线程执行任务。如果新创建后当前运行的线程数大于
//maximumPoolSize则拒绝执行任务
else if (!addWorker(command, false))
reject(command);
}
如果线程池能够创建线程执行任务,那么将调用addWorker方法,将线程池创建的线程封装为Worker,Worker在执行完任务后还会循环获取队列中任务来执行。看看addWorker方法的源码:
private boolean addWorker(Runnable firstTask, boolean core){
//省略部分代码
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//这里就将提交的任务封装成为Worker了
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
//使用加锁的方式原子添加工作线程
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//在获得锁期间再次检查线程池的运行状态:如果
//线程池已经关闭或者任务为空则抛出异常
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive())
throw new IllegalThreadStateException();
//加入Worker数组
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//如果添加成功则启动线程执行任务
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
之后我们看看执行t.start()后会发生的事,因为Worker本身实现了Runnable,所以start后将调用Worker的run方法,源码如下:
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
try {
beforeExecute(wt, task);
Throwable thrown = null;
task.run();
afterExecute(task, thrown);
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
以上源码其实就干了一件事:创建的线程在执行完提交的任务后会反复从BlockingQueue中获取任务来执行。
使用线程池
线程池的创建,使用ThreadPoolExecutor来创建:
构造方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
参数说明:
corePoolSize: 线程池的基本大小
提交一个任务到线程池时,线程池会创建一个线程来执行任务。即使线程池中有空闲的线程可以执行该任务,线程池也会创建线程。等到需要执行的任务数大于线程池的基本大小时就不会再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程就会提前创建并启动所有的基本线程。
maximumPoolSize:线程池维护线程的最大数量
线程池允许创建的最大线程数。如果队列满了,并且创建的线程数小于最大线程数,则线程池会创建新的线程执行任务。如果使用了无界任务队列,则这个参数就没什么用。
keepAliveTime: 线程池维护线程所允许的空闲时间
线程池的工作线程空闲后,保持的存活时间。如果任务多,可以增大时间提高线程的利用率。
unit: 线程池维护线程所允许的空闲时间的单位
单位有天 小时 分钟 毫秒 微秒 千分之一毫秒 千分之一微妙
workQueue: 线程池所使用的任务队列
用来保存等待执行任务的阻塞队列。可以选择的有:
ArrayBlockingQueue
LinkedBlockingQueue(吞吐量高于ArrayBlockingQueue,静态工厂Executors.newFixedThreadPool()默认使用它)
SynchronousQueue(吞吐量高于LinkedBlockingQueue,静态工厂Executors.newCachedThreadPool()默认使用它)
PriorityBlockingQueue 无界阻塞队列,使用它则会让maximumPoolSize参数无效
handler: 线程池对拒绝任务的处理策略(饱和策略)
当队列和线程池都满了,说明线程池处于饱和状态。那么必须采用一种策略处理提交的新任务,这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛异常。在jdk 1.5中主要提供了如下几种策略。
- AbortPolicy 直接抛出异常
- DiscardOldestPolicy 丢弃队列中最近的一个任务,并执行当前任务
- CallerRunsPolicy 只用调用者所在线程来运行任务
- DiscardPolicy 不处理,丢弃掉
当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志,持久化存储不能处理的任务等等。
向线程池提交任务
使用两个方法来为线程池提交任务:execute() submit()
execute方法用于提交不需要返回值的任务,所以无法判断是否被线程池成功。实例代码:
threadspool.execute(new Runnable{
@Override
public void run(){
doSomething();
}
});
submit方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过它可以判断执行是否成功
future有个阻塞式的get()方法,通过它可以获取线程的返回值,它还有个重载方法get(long timeout,TimeUnit unit)方法会阻塞一段时间后返回,但这个时候有可能任务没有执行完。
关闭线程池
调用线程池的shutdown或者shutdownNow方法来关闭线程池。它们的原理是遍历线程池的工作线程,然后逐个调用interrupt方法来中断线程,所以没有响应中断的任务永远无法停止。它们两者还是有点区别,shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。
线程池的监控
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
- largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
*getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。- getActiveCount:获取活动的线程数。
通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,让我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。
建议使用有界队列
有界队列能增加系统的稳定性和预警能力。尤其是依赖与数据库操作的任务,因为等待数据库返回结果比较耗时,此时建议使用容量大一点的队列,线程数也建议设大,这样才能更好利用CPU。
现在我们编码来联系线程池:
public static void main(String[] str){
//创建一个顺序存储的阻塞队列,并指定大小为10
BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(10);
//创建线程池的饱和策略,AbortPolicy抛异常
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
//创建线程池,线程池基本大小3 最大线程数为5 线程最大空闲时间10分钟
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,10, TimeUnit.MINUTES,blockingQueue,handler);
/*
*我们给线程池的基本是3,现在我们给线程池提交四个任务,并不断的查询线程数。
*/
//使用execution方法提交任务
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("当前线程ID"+Thread.currentThread().getId());
System.out.println("我是线程1号");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//获取活动线程数
System.out.println("启动一个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount());
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("当前线程ID"+Thread.currentThread().getId());
System.out.println("我是线程2号");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//获取活动线程数
System.out.println("启动二个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount());
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("当前线程ID"+Thread.currentThread().getId());
System.out.println("我是线程3号");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//获取活动线程数
System.out.println("启动三个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount());
/*
* 第四个任务应该是不会被执行的,而被存储在任务队列中。等待其他任务被执行完。
*/
//提交四号线程后。此线程应该存储在队列中
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("当前线程ID"+Thread.currentThread().getId());
System.out.println("我是线程4号");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//获取活动线程数
System.out.println("启动四个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount());
}
运行结果:
启动一个线程后当前活动线程数量:1
启动二个线程后当前活动线程数量:2
当前线程ID11
当前线程ID12
启动三个线程后当前活动线程数量:3
启动四个线程后当前活动线程数量:3
当前线程ID10
我是线程1号
我是线程3号
我是线程2号
可以看到。在任务队列没有满的情况下,线程池只会创建基本数量的线程3个。对于这三个线程没有空闲的时候。第四个线程则会被存储到任务队列,等待空闲线程。
现在我们对上述代码做如下修改:
将任务队列容量设置为1
BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(1);
对线程池再提交一个任务,即在任务队列为1情况下提交5个任务
此时的打印为:
启动一个线程后当前活动线程数量:1
当前线程ID10
我是线程1号
启动二个线程后当前活动线程数量:2
当前线程ID11
我是线程2号
启动三个线程后当前活动线程数量:3
启动四个线程后当前活动线程数量:3
当前线程ID12
我是线程3号
启动五个线程后当前活动线程数量:4
当前线程ID13
我是线程5号
可以看的到,当线程池的基本线程数量满了以后,继续提交的线程会保持在任务队列,上代码中四号任务被保存在任务队列等待基本线程来执行它。此时继续提交线程给线程池,由于我们的任务队列已经满了(只初始化了1,并只保存了四号任务),此时对于第5号线程则会创建工作线程来执行它。如果继续不断的添加任务给线程池,但任务数超过参数二的时候,则会执行饱和策略。
参考 《Java并发编程的艺术》