什么是线程池?
为了避免系统频繁的创建和销毁线程,我们可以让创建好的线程重复使用,类似与数据库开发中经常接触到的数据库连接池。在线程池中,会有几个活跃着的线程,当我们需要线程的时候直接从里面拿取空闲的线程来使用,使用完以后,直接归还即可。
JDK中的Executor框架
为了能更好的控制多个线程,JDK中java.util.cocurrent 包下提供了Executor框架,帮助发开人员控制线程的启动、执行和关闭,可以简化并发编程的操作。其本质就是一个线程池。
核心成员如下:
其中ThreadPoolExecutor表示一个线程池,Executors就是线程池工厂,通过它可以获得各种线程池。任何实现了Runnable的对象都可以被ThreadPoolExecutor执行。
用Executors产生各种线程池的常用方法如下:
static ExecutorService newCachedThreadPool();
static ExecutorService newFixedThreadPool(int nThreads) ;
static ExecutorService newSingleThreadExecutor() ;
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) ;
- newCachedThreadPool():该方法返回一个根据实际情况可以调整数量的线程池,线程池中的线程数量是不固定的
- newFixedThreadPool(int nThreads):该方法返回一个线程数量为nThreads的固定的线程池,池子里面的线程不会改变数量,若线程中没有空闲的线程,此时提交任务则会被放在任务列队中,直到有空闲的线程再执行列队中的任务。
- newSingleThreadExecutor():该方法返回一个线程数量为1的线程池,若线程中没有空闲的线程,此时提交任务则会被放在任务列队中,直到有空闲的线程再执行列队中的任务。
- newScheduledThreadPool(int corePoolSize):返回数量为corePoolSize的线程池,ScheduledExecutorService扩展了ExecutorService,用来完成在未来的某一个是个需要完成的任务或者周期性的完成某个任务。
具体的使用也非常简单:
public class UseExecutor {
public static class ExeTask implements Runnable{
@Override
public void run() {
System.out.println("执行任务的线程ID为:"+Thread.currentThread().getId());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExeTask task = new ExeTask();
//创建CachedThreadPool
ExecutorService service = Executors.newCachedThreadPool();
//创建大小为2的线程池
//ExecutorService service = Executors.newFixedThreadPool(2);
//创建单线程
//ExecutorService service = Executors.newSingleThreadExecutor();
for (int i = 0; i <5; i++) {
//提交线程任务
service.execute(task);
}
//关闭ExecutorService
service.shutdown();
}
}
/*
CachedThreadPool output:
执行任务的线程ID为:9
执行任务的线程ID为:10
执行任务的线程ID为:13
执行任务的线程ID为:12
执行任务的线程ID为:11
FixedThreadPool(2):
执行任务的线程ID为:10
执行任务的线程ID为:9
执行任务的线程ID为:10
执行任务的线程ID为:9
执行任务的线程ID为:9
SingleThreadExecutor:
执行任务的线程ID为:9
执行任务的线程ID为:9
执行任务的线程ID为:9
执行任务的线程ID为:9
执行任务的线程ID为:9
*/
Executor除了可以执行Runnable对象,还可以执行Callable对象。我将在后面的章节中好好的说一说Callable。
线程池的核心
对于常用到的线程池:CachedThreadPool、FixedThreadPool、newSingleThreadExecutor,虽然功能不同,但是内部都实现了ThreadPoolExecutor。下面给出三个线程池的实现方式:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
由此可以看出,它们都是对ThreadPoolExecutor类的简单封装。我们再来看ThreadPoolExecutor最重要的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数的具体含义:
- corePoolSize:线程池大小
- maximumPoolSize:线程池最大容量
- keepAliveTime:超过corePoolSize的那些线程空闲多少时间会被销毁
- unit:keepAliveTime的时间单位
- workQueue:任务列队,存放被提交但是没有被执行的任务
- threadFactory:线程工厂,一般用默认的即可。
- handler:拒绝策略,就是如何拒绝处理不了的任务。
大部分参数比较简单,需要解释的就是workQueue。根据不同的功能,workQueue也有多种类型,常用的如下: - SynchronousQueue:直接提交的任务队列,没有容量,他总是把加入的任务交给线程来执行,如果没有空闲的线程,就会创建线程。因此使用SynchronousQueue,需要设置非常大的maximumPoolSize。一句话:任务不需要等待,直接由线程执行。
- ArrayBlockingQueue:有界任务队列,它有一个具体的容量capacity,如果此时线程数量大于了corePoolSize,新加入的任务会被提交到ArrayBlockingQueue中等待,当队列中任务数量大于capacity才会创建新的线程,知道线程数为maximumPoolSize。
- LinkedBlockingQueue:无界任务队列,也就是说队列可以无限制的加入新的任务。
我们再看一下ThreadPoolExecutor线程池的核心调度代码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < 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))
//加入失败,执行拒绝策略
reject(command);
}
调度逻辑可以总结为如下:
线程池扩展
ThreadPoolExecutor是一个可以扩展的线程池,他提供了如下的借口:
void beforeExecute(Thread t, Runnable r){}
void afterExecute(Runnable r, Throwable t){}
void terminated() { }
默认的ThreadPoolExecutor实现,beforeExecute和afterExecute方法是空的,我们可以根据自己的需要对其进行扩展。比如输出一些有用的调试信息,或者是日志。