线程池的创建(I)
我们可以通过ThreadPoolExecutor创建一个线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
创建线程池时有几个核心参数需要了解下:
- corePoolSize :线程池的核心数量。当一个任务提交到线程池时,线程池会创建一个线程来执行任务,即使当前有空闲的线程能够执行新任务,线程池还是会创建新的线程,直到需要执行的任务数大于核心数量就不再创建
- maximumPoolSize:线程池最大数量。线程池所能允许创建的最大线程数,如果队列满了,并且已创建的线程小于最大线程数,线程池还会继续创建线程
- keepAliveTime:线程活动保持时间。线程池中的线程工作结束后的保持存活的时间,超过这个时间还没有任务执行,线程就会被销毁
- TimeUnit: 时间单位。线程活动保持时间的时间单位
- workQueue :任务队列,用于保存等待执行的任务的阻塞队列,当任务的数量大于核心数量时,就会将任务暂时保存在任务队列中,如果队列也满了且没超过最大数量,线程池就会创建新的线程执行任务
有四个阻塞队列可以选择
ArrayBlockingQueue | 基于数组的有界阻塞队列,按照FIFO的原则 |
---|---|
LinkedBlockingDeque | 基于链表的无界阻塞队列,也可设置界限 |
SynchronousQueue | 不存储元素的阻塞队列,每个插入元素必须等到另一个线程调用移除操作,否则会一直阻塞 |
PriorityBlockingQueue | 具有优先级的无限阻塞队列 |
- handler:拒绝策略。当队列和线程池都满了,说明线程池已经饱和了,当再有新的任务提交时,就要采取一种策略来处理这些新任务,这就是拒绝策略,一共有四种策略
AbortPolicy | 直接抛出异常 |
---|---|
CallerRunsPolicy | 只用调用者所线程来运行任务 |
DiscardPolicy | 不处理,丢弃掉 |
DiscardOldestPolicy | 丢弃队列中最近的一个任务,并执行当前的任务 |
除了上面四个拒绝策略,我们还可以通过实现RejectedExecutionHandler接口来自定义拒绝策略
public static class MyTask implements Runnable {
private int i;
public MyTask(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(System.currentTimeMillis()
+ ":Thread ID:" + Thread.currentThread().getId()+" , "+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]) throws InterruptedException {
ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0L,
TimeUnit.SECONDS,
new PriorityBlockingQueue<>(2),
(r,executor)-> System.out.println(r.toString() + " 我被抛出5555555")
);
for (int i = 0; i < 6; i++) {
executorService.submit(new MyTask(i));
Thread.sleep(10);
}
}
线程池的创建(II)
除了通过上面的方式创建线程池,还可以通过Executor框架的工具类Executors创建线程池
通过Executors创建的线程池内部还是使用ThreadPoolExecutor来创建,所以参数与上面的相同就不再赘述
- Executors.newFixedThreadPool:可设置线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
通过参数nThreads设置核心数量和最大数量的大小,阻塞队列为LinkedBlockingQueue没有设置大小,所以是无界的队列,而线程存活时间设置为0L,意味线程一结束任务就被销毁,不会有多余的空闲线程
- Executors.newSingleThreadExecutor:使用单个工作线程的线程池
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
核心数量和最大数量都被设置为1,其余的与FixedThreadPool相同
- Executors.newCachedThreadPool:会根据需要创建新线程的线程池
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
核心数量被设置为0,即为空。最大数量被设置为Integer.MAX_VALUE,即无界的,同时存活时间设置为60秒意味着空闲线程等待新任务的最长时间为60秒,使用没有容量的SynchronousQueue作为线程池的任务队列,意味着如果主线程提交任务速度高于线程处理任务的速度时,线程池就会不断的创建线程,最终可能会导致耗尽资源
为什么建议不用Java提供的线程池创建方法
在《阿里巴巴Java手册》中有一条这样规定。
通过我们上面所学的可以知道
- FixedThreadPool和SingleThreadExecutor:这两个线程池的实现方式,我们可以看到它设置的工作队列都是LinkedBlockingQueue,我们知道此队列是一个链表形式的队列,此队列是没有长度限制的,是一个无界队列,那么此时如果有大量请求,就有可能造成OOM。
- CachedThreadPool:这个线程池的实现方式,我们可以看到它设置的最大线程数都是Integer.MAX_VALUE,那么就相当于允许创建的线程数量为Integer.MAX_VALUE。此时如果有大量请求来的时候也有可能造成OOM。
而使用ThreadPoolExecutor方式去创建线程池,可以让人很好的了解到线程池的使用规则,同时定制程度高
向线程池提交任务和关闭线程池
可以使用exceute()和submint() 方法来提交任务
- exceute():用于提交不需要返回值的任务
- submint():用于提交需要返回值的任务
线程池的关闭: 使用 shutdown或shutdownNow 方法来关闭线程池
ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2),
(r,executor)-> System.out.println(r.toString()+"我被抛弃了5555555")
);
for (int i = 0; i < 6; i++) {
executorService.execute(new MyTask(i));//提交任务
Thread.sleep(10);
}
executorService.shutdown();//关闭线程池
}
学习自Java并发编程的艺术