前言
初步学完线程池后的总结
一、显式创建线程池的弊端?
学完线程后,除了直接new Thread()、实现Runnable接口和实现Callable接口的三种方法。我们还知道第四种的方法:使用线程池。这比我们一个一个new Thread更加高效,降低资源消耗。线程池能够线程复用;控制最大并发数;管理线程。
但当我美滋滋的用着的时候,突然有个不影响运行的红色波浪线提示(后来才知道这是阿里巴巴的Java代码规范插件,"alibaba java code guidelines "一个很好用的规范代码的插件),它让我手动创建线程池。这是为什么呢?
我们不是可以直接使用Executors来创建我们需要的线程池吗,JDK中都写好了,为什么还要多此一举呢?于是我去看了下源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
//注意看LinkedBlockingQueue该阻塞队列
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
//注意看LinkedBlockingQueue该阻塞队列
new LinkedBlockingQueue<Runnable>()));
}
//LinkedBlockingQueue的无参构造方法
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
我们在显式创建线程池(Executors.newFixedThreadPool(X) Executors.newSingleThreadExecutor())这两线程池时,源码显示会创建一个LinkedBlockingQueue对象,LinkedBlockingQueue使用其无参构造方法。
然后我们又看到LinkedBlockingQueue的无参构造居然允许的请求队列长度居然达到了Integer.MAX_VALUE,21亿,这和无界又有什么区别呢,这可能会堆积大量的请求任务。然后我看到Executors.newCachedThreadPool()中不是使用LinkedBlockingQueue呀。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
//使用SynchronousQueue阻塞队列
new SynchronousQueue<Runnable>());
}
可我在上面的参数中又发现了Integer.MAX_VALUE这个玩意,这个有界和无界没有区别数字,在ThreadPoolExecutor的参数中第二个参数就是线程池能够容纳同时执行的最大线程数,ThreadPoolExecutor的参数我在下面第二点会说明。从字面意思上来看我们就是该线程池能容纳21亿个线程,基本等于无上限。这就意味着这可能会堆积大量的线程。
所以在阿里巴巴开发手册中也是强制不允许显式创建线程池。因为可能会导致OOM(Out Of Memory)。
线程池中不允许使用Executors创建,而是通过ThreadPoolExecutor的方式创建,这样的处理方式能让编写的代码的工程师更加明确线程池的运行规则,规避资源耗尽的风险。
二、手写线程池
从上面三个显式创建线程池就能发现,他们底层都是创建ThreadPoolExecutor这个对象,并传入了五个参数:
public static ExecutorService newCachedThreadPool() {
//创建一个ThreadPoolExecutor对象
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
进入到ThreadPoolExecutor的构造方法中,发现它底层还调用了自身另外的构造方法。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
//调用自身另外的构造方法
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
于是再进入到ThreadPoolExecutor真正的构造方法:
能够发现它一共有七个参数,若是使用显式线程池调用则最后两个参数底层会使用默认的。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
我们来说明下这七个参数的代表的意义:
1 corePoolSize:核心线程数 线程池的常驻线程数。
在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务。
当线程池中的线程数目达到corePoolSize后,就会把到达的队列放到缓存队列中。
2 maximumPoolSize:线程池能够容纳同时执行的最大线程数 maximumPoolSize不能小于corePoolSize
3 keepAliveTime:多余的空闲线程存活时间
当线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余的空闲线程会被销毁,直到只剩下corePoolSize个线程为止
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用
4 unit:keepAliveTime的单位
5 workQueue:任务队列,被提交的但未被执行的任务
6 threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程池 一般用默认即可
7 handler:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize)时,如何来拒绝请求执行的Runnable的策略
我看完解释也是有点懵,但是用代码解释一下就好了
public class WriteThreadPool {
public static void main(String[] args) {
//2个核心线程、一共能容纳五个线程(即2个常驻核心线程,3个非核心线程、多余空闲线程存活时间为1个单位、单位为秒、默认线程工厂、使用默认拒绝策略
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 1L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 1; i <= ?; i++) {
int num = i;
threadPoolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 给用户:" + num + " 办理业务");
//模拟线程池中的线程执行任务的时间
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
}
}
现在手写了一个线程池,2个核心线程、一共能容纳五个线程(即2个常驻核心线程,3个非核心线程、多余空闲线程存活时间为1个单位、单位为秒、默认线程工厂、使用默认拒绝策略。所以就分为一下几种情况了。
可以注意到我把循环中的次数写了"?",因为不同的请求线程会有不同的效果。
1、当 ?<= 2时,直接是2个常驻的核心线程进行处理。
2、当 2 < ?<=5时,这时候常驻的核心线程不够用了,那就将这三个请求任务放到阻塞队列中。当前核心线程两个执行完后在进行执行队列的任务。
3、当 5 < ? <= 8 时,这时候核心线程中有任务,队列中也满了。那就唤醒非核心线程来进行处理。这时候就相当于6 、7、8这几个任务是先于队列中的任务执行的(在队列中的任务还在等待的前提下),而队列中等待的任务需要等到核心线程或是非核心线程执行完成才能执行。这样就意味着当我们手写线程池时,需要更加注意阻塞队列,若是使用默认的LinkedBlockingQueue阻塞队列,那么队列就一直要等核心队列执行完成。如下图所示。
4.1、当 ? > 8时 这时候 请求任务>队列中的线程数+容纳同时执行的最大线程数 就完全装不下了。那咋办嘛?这时候就靠着ThreadPoolExecutor构造方法中的最后一个参数,拒绝策略:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
4.2、实现了该接口的类有四个:
4.2.1、ThreadPoolExecutor.AbortPolicy() 默认的拒绝策略 当 ? > 8时 抛出任务队列溢出异常退出。
4.2.2、ThreadPoolExecutor.CallerRunsPolicy() 回退策略 让调用execute函数的上层线程去执行被拒绝的任务。上面情况下就是有main线程调用execute()方法,于是就是由main线程负责执行。
4.2.3、ThreadPoolExecutor.DiscardOldestPolicy() 将队列中等待时间最久的请求任务丢弃 上面情况下就会丢弃多余的耗时最久的任务
4.2.4、ThreadPoolExecutor.DiscardPolicy() 将队列中多余的线程丢弃
第一次写博客,若有错误麻烦指出。