对应《实战java高并发程序设计》第三章3.2章节内容。
1.JDK线程池
1.1基本概念
避免系统频繁地创建和销毁线程,可以让创建的线程进行复用。当需要线程时,可以从池中拿一个空闲的线程,完成工作后,不着急关闭线程,而是还回池中。
1.2.jdk对线程池的支持
在java.util.concurrent包中,ThreadPoolExecutor表示一个线程池,Executors类则扮演着线程池工厂的角色,通过它可以取得一个特定功能的线程池。
可以看到有许多静态方法可以创建不同的线程池,下面介绍几种常见的:
注意:最后两个定时的返回的类型是ScheduledExecutorService ,它是线程池的子类,额外拥有定时等功能
//返回一个固定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
//返回只有一个线程的线程池,相当于上述方法参数为1的情况
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
//数量不固定,有空闲,用空闲的,都没空闲,再创建。运行完成后,在返回
ExecutorService executorService3 = Executors.newCachedThreadPool();
//在给定时间执行某任务,线程池大小为1
ScheduledExecutorService executorService4 = Executors.newSingleThreadScheduledExecutor();
//在给定时间执行某任务,线程池大小为指定大小
ScheduledExecutorService executorService5 = Executors.newScheduledThreadPool(5);
1.3线程池的使用
- 通过submit来提交任务
- 通过schedule来提交任务,后面跟上时间参数
executorService.submit(new Runnable() {
@Override
public void run() {
//....
}
});
executorService4.schedule(new Runnable() {
@Override
public void run() {
// ....
}
},1000,TimeUnit.SECONDS);
可根据需要选择需要的调度方式。
2.线程池的内部实现
2.1内部实现
通过看源码可以看到,上面创建普通线程池的方法,底层都是通过ThreadPoolExecutor来创建的。
我们来看看它的构造方法:
/**
* @param corePoolSize
* 指定数量,也就是说,即便是一个任务都没有,线程池也会至少有该数量个线程存活
* @param maximumPoolSize 最大数量,当任务多
* @param keepAliveTime 顾名思义,多余的空闲线程存活的时间
* @param unit 时间单位
* @param workQueue 任务队列,被提交,但还尚未被执行的任务
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
2.2workQueue详解
参数workQueue指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,用于存放Runnable对象。
- 直接提交的队列:SynchronousQueue,没有容量,每一个插入都要等待一个删除,如果用这个队列,总是将新任务提交给线程去执行,线程数超过最大后,执行拒绝策略。
- 有界的任务队列:ArrayBlockingQueue,有界队列仅在任务队列装满时,才会将线程提升到超过corePoolSize以上
- 无界的任务队列:LinkedBlockingQueue,从上面分析一眼就能看出,它不可能等队列装满啦。当系统达到corePoolSize后,任务进队列,不会将线程继续提升了。
- 优先任务队列:带有优先级的。
2.3线程池实现
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>());
}
3.拒绝策略
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
最后一个参数指定了拒绝策略,任务队列满了,线程也达到了最大值时,这时候拒绝策略就出场了。可以看到,这是个接口,有4中实现,也可以自己去实现。
- 直接抛出异常
- 在调用者线程中,运行当前被丢弃的任务
- 丢弃最老的一个请求,然后再次提交当前任务
- 默默抛弃,不做任务事情
4.自定义线程创建
我们通过ThreadPoolExecutor来创建线程池,在它的内部肯定有一个东西来根据参数生成线程,这是理所应当,从它的参数我们也能看到,有个TreadFactory参数。默认的都是用这个。
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
这个类没多复杂,我们完全可以根据自己的需要重新实现一个。
5.扩展线程池
在ThreadPoolExecutor这个核心类上,我们能够看到三个空方法:都是protected,很明显,是让我们继承然后修改添加自己的逻辑的。
protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }
三个方法分别记录了一个任务的开始,结束和整个线程池的退出。
5.1 线程池的退出
/**
* Initiates an orderly shutdown in which previously submitted
* tasks are executed, but no new tasks will be accepted.
* Invocation has no additional effect if already shut down.
*
* <p>This method does not wait for previously submitted tasks to
* complete execution. Use {@link #awaitTermination awaitTermination}
* to do that.
*
* @throws SecurityException {@inheritDoc}
*/
public void shutdown() {
。。。。。。。。。。
6.优化线程池数量
一般考虑的是CPU数量和内存大小
7.在线程池中寻找堆栈
7.1 线程池中的一个坑
线程池有可能会吃掉程序抛出的异常,导致我们对程序的错误一无所知。异常堆栈对于程序员的重要性就像指南针对于大海的船只。
7.2 用submit()改用execute()?why
直接看源码就可以看出两者区别:
- 最明显的一个区别是submit有返回值,我们可以通过这个Future来得到任务的一些信息。
7.3 扩展ThreadPoolExecutor线程池
让它在调度任务之前,先保存一下提交任务的堆栈信息。
任务的提交交到了我们自己手中,那还不是想怎么做就怎么做,下面就是在execute任务时,包装了一下,出错后打印信息。
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
@Override
public void execute(Runnable command) {
//在这里给任务线程包装下
Runnable traceRunmable = new Runnable() {
@Override
public void run() {
try{
command.run();
}catch (Exception e){
System.out.println(Thread.currentThread().getName()+"出错了");
e.printStackTrace();
throw e;
}
}
};
super.execute(traceRunmable);
}
8.Fork/Join框架
8.1介绍
可以向ForkJoinPool线程池提交一个FolkJoinTask任务,该任务即可以fork()分解,和join等待。
举个简单的例子,当我们需要计算1-10000的和的时候,我们可以把任务分解成1-5000和5001-10000两个子任务,当两个子任务完成后,自己才能将两个和再加起来。分解即fork,等待两个子任务完成即join
8.2可分解任务类 ForkJoinTask
它有两个重要的子类
- RecursiveTask 有返回值
- RecursiveAction 无返回值
下面写一个计算1-10000的和:
/**
* @author tanhao
* @date 2020/10/16 14:35
*/
public class ForkJoinTaskStudy {
public static void main(String[] args) throws Exception{
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> submit = pool.submit(new SumTask(1, 10000));
Long aLong = submit.get();
System.out.println(aLong);
}
}
class SumTask extends RecursiveTask<Long>{
private int start;
private int end;
public SumTask(int start,int end){
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
Long res = 0L;
if (end - start > 1000){
//分解成2个
int count = (end - start)/2;
SumTask first = new SumTask(start,start+count);
SumTask last = new SumTask(start + count + 1, end);
ForkJoinTask<Long> fork = first.fork();
ForkJoinTask<Long> fork2 = last.fork();
//等待子任务
Long res1 = fork.join();
Long res2 = fork2.join();
res = res1+res2;
}else{
for (int i = start;i <= end;i++){
res += i;
}
}
return res;
}
}