Java 中 Executors.newSingleThreadExecutor() 与Executors.newFixedThreadPool(1)有什么区别
在研究Executors提供的线程池时自然会想到标题这个问题,既然已经有了newFixedThreadPool,为什么还要存在newSingleThreadExecutor这个方法。难道newFixedThreadPool(1)不是只有一个线程(Single Thread)的?本文将通过分析JDK中的相关源码回答这个问题。
源码分析
写JDK代码的大佬们早就预料到了我们会有此疑问,在newSingleThreadExecutor给我们解释了一下:Unlike the otherwise equivalent newFixedThreadPool(1) the returned executor is guaranteed not to be reconfigurable to use additional threads.
这个解释说明newSingleThreadExecutor和newFixedThreadPool(1)确实是有区别的,区别在于newSingleThreadExecutor返回的线程池保证不能被重新配置(重新调整线程池大小等)。这又引出了新的问题,难 newFixedThreadPool(1) 创建的线程池是可配置的?它不是线程池数量固定的么?为什么newSingleThreadExecutor是不可重新配置的?
带着这些问题,找到了这两个方法的源码:
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 gc(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue < Runnable > ()));
}
从源码可以看出,两者及其相似,newFixedThreadPool返回了一个ThreadPoolExecutor对象,newSingleThreadExecutor返回了一个被FinalizableDelegatedExecutorService包装过的ThreadPoolExecutor对象,连ThreadPoolExecutor对象的参数值都一样的。问题就在了包装上,一层层的查看代码,发现最里面的一层是DelegatedExecutorService。可以知道的是,ThreadPoolExecutor和包装ThreadPoolExecutor对象的类都直接或间接实现了ThreadPoolExecutor接口。为了方便分析,我们先生成新相关类的类图。

从类图中可以看出,ThreadPoolExecutor和DeletagedExecutorService之间是并列关系,并非继承关系。再查看二者的方法,会发现DeletagedExecutorService只有一个构造方法,构造方法可以传入ExecutorService的引用。其它方法都仅仅是调用构造方法传入对象中对应的方法。而ThreadPoolExecutor中有很多的其它具体实现的方法。
/**
* A wrapper class that exposes only the ExecutorService methods
* of an ExecutorService implementation.
*/
static class DelegatedExecutorService extends AbstractExecutorService {
private final ExecutorService e;
DelegatedExecutorService(ExecutorService executor) { e = executor; }
public void execute(Runnable command) { e.execute(command); }
public void shutdown() { e.shutdown(); }
public List<Runnable> shutdownNow() { return e.shutdownNow(); }
public boolean isShutdown() { return e.isShutdown(); }
public boolean isTerminated() { return e.isTerminated(); }
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
return e.awaitTermination(timeout, unit);
}
public Future<?> submit(Runnable task) {
return e.submit(task);
}
public <T> Future<T> submit(Callable<T> task) {
return e.submit(task);
}
public <T> Future<T> submit(Runnable task, T result) {
return e.submit(task, result);
}
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
return e.invokeAll(tasks);
}
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
return e.invokeAll(tasks, timeout, unit);
}
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
return e.invokeAny(tasks);
}
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return e.invokeAny(tasks, timeout, unit);
}
}
DelegatedExecutorService 类源码
/**
* Sets the core number of threads. This overrides any value set
* in the constructor. If the new value is smaller than the
* current value, excess existing threads will be terminated when
* they next become idle. If larger, new threads will, if needed,
* be started to execute any queued tasks.
*
* @param corePoolSize the new core size
* @throws IllegalArgumentException if {@code corePoolSize < 0}
* @see #getCorePoolSize
*/
public void setCorePoolSize(int corePoolSize) {
if (corePoolSize < 0)
throw new IllegalArgumentException();
int delta = corePoolSize - this.corePoolSize;
this.corePoolSize = corePoolSize;
if (workerCountOf(ctl.get()) > corePoolSize)
interruptIdleWorkers();
else if (delta > 0) {
// We don't really know how many new threads are "needed".
// As a heuristic, prestart enough new workers (up to new
// core size) to handle the current number of tasks in
// queue, but stop if queue becomes empty while doing so.
int k = Math.min(delta, workQueue.size());
while (k-- > 0 && addWorker(null, true)) {
if (workQueue.isEmpty())
break;
}
}
}
ThreadPoolExecutor类中的setCorePoolSize方法
推论与实验验证
由此可以得知:
DelegatedExecutorService 其实是专门对实现了ExecutorService接口的类的对象进行包装的,包装之后的对象仅仅暴露ExecutorService接口中的方法。上面的 newSingleThreadExecutor() 方法中,FinalizableDelegatedExecutorService(继承DelegatedExecutorService)对ThreadPoolExecutor对象进行了包装,把诸如setCorePoolSize等方法给拿掉了,因此也就不具备ThreadPoolExecutor设置线程池属性的功能,所以说 newSingleThreadExecutor() 返回的线程池能够保证不能被重新配置。
// 获取一个 Single Thread Executor
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
System.out.println(singleThreadExecutor instanceof ThreadPoolExecutor);//输出:false
那为什么 newFixedThreadPool(1) 返回的线程池是可以重新配置呢?这个问题很简单,newFixedThreadPool返回的是一个ThreadPoolExecutor对象,ExecutorService引用指向该对象。因此可以通过强转的方式将它专为ThreadPoolExecutor的引用,然后通过该引用来对线程池重新进行配置。
// 获取一个容量为 1 的 FixedThreadPool
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
// 定义任务组 tasks1
List<Runnable> tasks1 = Arrays.asList(
()->{System.out.println("Thread ID:" + Thread.currentThread().getId() + "---> Task1");},
()->{System.out.println("Thread ID:" + Thread.currentThread().getId() + "---> Task2");});
// 往 FixedThreadPool 中提交 tasks1。此时因为线程池的容量为1,所以两个任务由1个线程执行。
tasks1.forEach(fixedThreadPool::submit);
// 等待前面两个任务结束
Thread.sleep(1000L);
// 定义任务组 tasks2
List<Runnable> tasks2 = Arrays.asList(
()->{System.out.println("Thread ID:" + Thread.currentThread().getId() + "---> Task3");},
()->{System.out.println("Thread ID:" + Thread.currentThread().getId() + "---> Task4");});
System.out.println(fixedThreadPool instanceof ThreadPoolExecutor); // 输出 true
// 将 ExecutorService 强转为 ThreadPoolExecutor
ThreadPoolExecutor configurableFixedThreadPool = (ThreadPoolExecutor) fixedThreadPool;
// 改变容量
configurableFixedThreadPool.setCorePoolSize(2);
// 提交任务组 tasks2。此时由于线程池的容量变成了2,所以tasks2中的两个任务将分别由不同的线程执行(极端情况下也可能由一个线程执行,但此时线程池容量切切实实变成了2)。
tasks2.forEach(fixedThreadPool::submit);
// 关闭线程池
fixedThreadPool.shutdown();
// 等待任务执行结束
fixedThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
输出:
Thread ID:14---> Task1
Thread ID:14---> Task2
true
Thread ID:14---> Task4
Thread ID:15---> Task3
上面的两个实验可以回答前面的问题。容量为1的FixedThreadPool的属性(容量等)可以通过将其强转为ThreadPoolExecutor而被重新进行配置;而SingleThreadPool实际是一个FinalizableDelegatedExecutorService类的对象,由于该类没有继承任何可以配置线程池的类,因此可以保证它不能被再次配置。
小结
虽然SingleThreadPool与容量为1的FixedThreadPool的区别只在于一个可重新配置,一个不可重新配置;但是在按照需求写代码的时候:如果确实要用到容量为1的线程池,应该使用SingleThreadPool而不是用容量为1的FixedThreadPool。后者有一个隐患,如果开始设置的任务数是1,任务与任务之间本质是串行执行的,也就是说,一个任务得等到前面一个任务执行结束之后再执行。而如果后面有人写代码的时候扩大了容量为1的FixedThreadPool,那么修改之前,已经提交的但还未被执行的任务,可能被分到其它线程中去执行。这样,原本应该串行执行任务变成了并行执行,如果任务之间没有依赖还好,一旦有依赖,逻辑就错乱了。
上面的第2个实验中,将“等待前面两个任务执行结束”的那行sleep代码注释掉就可以验证这个问题。task1 和 task2本来是要在1个线程中执行的,而后面由于修改了容量,这两个任务也有一定几率在不同的线程中执行。
// 获取一个容量为 1 的 FixedThreadPool
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
// 定义任务组 tasks1
List<Runnable> tasks1 = Arrays.asList(
()->{System.out.println("Thread ID:" + Thread.currentThread().getId() + "---> Task1");},
()->{System.out.println("Thread ID:" + Thread.currentThread().getId() + "---> Task2");});
// 往 FixedThreadPool 中提交 tasks1。此时因为线程池的容量为1,所以两个任务由1个线程执行。
tasks1.forEach(fixedThreadPool::submit);
// 等待前面两个任务结束
//Thread.sleep(1000L);
// 定义任务组 tasks2
List<Runnable> tasks2 = Arrays.asList(
()->{System.out.println("Thread ID:" + Thread.currentThread().getId() + "---> Task3");},
()->{System.out.println("Thread ID:" + Thread.currentThread().getId() + "---> Task4");});
System.out.println(fixedThreadPool instanceof ThreadPoolExecutor); // 输出 true
// 将 ExecutorService 强转为 ThreadPoolExecutor
ThreadPoolExecutor configurableFixedThreadPool = (ThreadPoolExecutor) fixedThreadPool;
// 改变容量
configurableFixedThreadPool.setCorePoolSize(2);
// 提交任务组 tasks2。此时由于线程池的容量变成了2,所以tasks2中的两个任务将分别由不同的线程执行(极端情况下也可能由一个线程执行,但此时线程池容量切切实实变成了2)。
tasks2.forEach(fixedThreadPool::submit);
// 关闭线程池
fixedThreadPool.shutdown();
// 等待任务执行结束
fixedThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
一种输出:
true
Thread ID:14---> Task1
Thread ID:16---> Task3
Thread ID:16---> Task2
Thread ID:16---> Task4
Java 中 Executors.newSingleThreadExecutor() 与Executors.newFixedThreadPool(1)有什么区别的更多相关文章
- <Java中的继承和组合之间的联系和区别>
//Java中的继承和组合之间的联系和区别 //本例是继承 class Animal { private void beat() { System.out.println("心胀跳动...& ...
- Java中 a+=b 和 a=a+b 有什么区别?
今天舍友突然问我"在java中 a+=b 和a=a+b 有什么区别",说这是一道面试题.当时就不假思索的回答:"一样啊",然后他说有位面试者也回答说一样,所以被 ...
- java中super()和this()、super和this的区别
1.super()和this()区别: super():调用父类无形参的构造方法: super(形参):调用父类中某个带形参的构造方法: this(形参):调用本类中另一种形式的构造方法: 注意:放在 ...
- 关于java中接口定义常量和类定义常量的区别
/** * * @author YZJ * @Description java中定义常量的最佳方法 */ public final class Contants{ /** * @Description ...
- Java中继承thread类与实现Runnable接口的区别
Java中线程的创建有两种方式: 1. 通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中 2. 通过实现Runnable接口,实例化Thread类 在实际应用中, ...
- Java基础知识强化之多线程笔记05:Java中继承thread类 与 实现Runnable接口的区别
1. Java中线程的创建有两种方式: (1)通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中. (2)通过实现Runnable接口,实例化Thread类. 2. ...
- [转] Java中继承thread类与实现Runnable接口的区别
Java中线程的创建有两种方式: 1. 通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中 2. 通过实现Runnable接口,实例化Thread类 在实际应用中, ...
- Java中String直接赋字符串和new String的区别
解析Java中的String对象的数据类型 1. String是一个对象. 因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些特性. 2. ...
- 【转】Java中super和this的几种用法与区别
1. 子类的构造函数如果要引用super的话,必须把super放在函数的首位. class Base { Base() { System.out.println("Base&qu ...
随机推荐
- 题解-洛谷P7114 字符串匹配
题面 洛谷P7114 字符串匹配 \(T\) 组测试数据.给定字符串 \(S\),问有多少不同的非空字符串 \(A\),\(B\),\(C\) 满足 \(S=ABABAB...ABC\) 且 \(A\ ...
- 【JSOI2019】精准预测(2-SAT & bitset)
Description 现有一台预测机,可以预测当前 \(n\) 个人在 \(T\) 个时刻内的生死关系.关系有两种: \(\texttt{0 t x y}\):如果 \(t\) 时刻 \(x\) 死 ...
- 图论-zkw费用流
图论-zkw费用流 模板 这是一个求最小费用最大流的算法,因为发明者是神仙zkw,所以叫zkw费用流(就是zkw线段树那个zkw).有些时候比EK快,有些时候慢一些,没有比普通费用流算法更难,所以学z ...
- TimSort源码详解
Python的排序算法由Peter Tim提出,因此称为TimSort.它最先被使用于Python语言,后被多种语言作为默认的排序算法.TimSort实际上可以看作是mergeSort+binaryS ...
- 在Nuxt中使用react-id-swiper封装公共的轮播图组件(移动端
首先就是引入swiper import Swiper from 'react-id-swiper': 一个轮播图首先要考虑到一种情况就是当只有一张图的时候是不是需要按轮播图来处理 一般情况下,一张图是 ...
- Intellij idea 创建java web
https://blog.csdn.net/qq_41592771/article/details/80577791 将tomcat服务器lib目录下面的servlet-api.jar复制到lib文件 ...
- Spark性能优化指南
1 Spark开发调优篇 原则一:避免创建重复的RDD 原则二:尽可能复用同一个RDD 原则三:对多次使用的RDD进行持久化 原则四:尽量避免使用shuffle类算子 因此在我们的开发过程中,能避免则 ...
- Python批量创建word文档(1)- 纯文字
Python创建word文档,任务要求:小杨在一家公司上班,每天都需要给不同的客户发送word文档,以告知客户每日黄金价格.最后贴上自己的联系方式.代码如下: 1 ''' 2 #python根据需求新 ...
- iOS常见遍历方法汇总
一.for循环 NSArray *iosArray = @[@"L", @"O", @"V", @"E", @" ...
- C# 链表 二叉树 平衡二叉树 红黑树 B-Tree B+Tree 索引实现
链表=>二叉树=>平衡二叉树=>红黑树=>B-Tree=>B+Tree 1.链表 链表结构是由许多节点构成的,每个节点都包含两部分: 数据部分:保存该节点的实际数据. 地 ...