阻塞队列
阻塞队列,顾名思义,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下所示:
下面是一些阻塞队列(BlockingQueue的子类):
ArrayBlockingQueue:是一个基于数组结构的有届阻塞队列,此队列按FIFO(先进先出)原则对元素进行操作
方法类型 | 异常组 | 特殊组 | 阻塞 | 超时 |
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除
检查 |
remove()
element() |
poll()
peek() |
take()
不可用 |
poll(time,unit)
不可用 |
抛出异常 | 当阻塞队列满时,再往队列里add插入元素会抛出java.lang.IllegalStateException: Queue full 当阻塞队列空时,再往队列里remove插入元素会抛出java.util.NoSuchElementException |
特殊值 | 插入方法,成功true失败false 移除方法,成功返回出队列元素,队列里面没有就返回null |
一直阻塞 | 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用 |
超时退出 | 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限定时间后生产者线程会退出 |
public static void main(String[] args) {
BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.add("c"));
}
结果:
true
true
true
Exception in thread "main" java.lang.IllegalStateException: Queue full
at java.util.AbstractQueue.add(AbstractQueue.java:98)
at java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:312)
at com.yi23.springboot.interview.BlockingQueueTest.main(BlockingQueueTest.java:20)
-------------------------------
public static void main(String[] args) {
BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
}
结果:
true
true
true
a
b
c
Exception in thread "main" java.util.NoSuchElementException
at java.util.AbstractQueue.remove(AbstractQueue.java:117)
at com.yi23.springboot.interview.BlockingQueueTest.main(BlockingQueueTest.java:24)
==============================offer(e) poll peek===============
BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("d"));
System.out.println(blockingQueue.peek());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
结果:
true
true
true
false
a
a
b
c
null
null
SynchronousQueue没有容量(同步队列)。与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
public static void main(String[] args) {
BlockingQueue blockingQueue = new SynchronousQueue<>();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t put 1");
try {
blockingQueue.put("1");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t put 2");
try {
blockingQueue.put("2");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t put 3");
try {
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"put线程").start();
new Thread(()->{
try {
Thread.sleep(5);
System.out.println(Thread.currentThread().getName()+"\t take 1:"+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(5);
System.out.println(Thread.currentThread().getName()+"\t take 2:"+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(5);
System.out.println(Thread.currentThread().getName()+"\t take 3:"+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"take线程").start();
}
结果:
put线程 put 1
take线程 take 1:1
put线程 put 2
take线程 take 2:2
put线程 put 3
take线程 take 3:3
public class Consumer {
public static void main(String[] args) {
ShareData product = new ShareData();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
product.product();
}
}, "生产者").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
product.consumer();
}
}, "消费者").start();
}
}
class Product {
//传统模式的生产者消费者
Integer count = 0;
public synchronized void product() {
while (count > 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println("生产一个:" + count);
notify();
}
public synchronized void consumer() {
while (count <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费一个count:" + count);
count--;
notify();
}
}
class ShareData {
Integer data = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void product() {
lock.lock();
try {
while (data > 0) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
data++;
System.out.println("生产一个:" + data);
condition.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void consumer() {
lock.lock();
try {
while (data <= 0) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费一个count:" + data);
data--;
condition.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
结果:
生产一个:1
消费一个count:1
生产一个:1
消费一个count:1
生产一个:1
消费一个count:1
生产一个:1
消费一个count:1
生产一个:1
消费一个count:1
Synchronized与Lock有什么区别?
1、原始构成
2、使用方法
3、等待是否可中断
4、加锁是否公平
5、锁绑定多个条件Condition
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量的线程排队等待,等其他线程执行完毕,再从队列中取出任务来执行。
他的主要特点为:
ExecutorService executorService1 = Executors.newFixedThreadPool(4);,
主要特点如下:
下面是源码:
//Executors里面的方法
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
//ThreadPoolExecutor的方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue 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;
}
Executors.newFixedThreadPool(4)使用的阻塞队列是LinkedBlockingQueue,
核心线程数和最大线程数一样(maximumPoolSize=corePoolSize),
存活的时间keepAliveTime
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
源码(和上面比较说明):
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
ExecutorService executorService = Executors.newCachedThreadPool();
源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
源码:
//workQueue:任务队列,被提交但尚未被执行但任务。
private final BlockingQueue workQueue;
//示生成线程池中工作线程的工厂,用于创建线程一般用默认的即可。
private volatile ThreadFactory threadFactory;
//存活时间,多线程可见
private volatile long keepAliveTime;
private volatile boolean allowCoreThreadTimeOut;
/**
核心线程数
*/
private volatile int corePoolSize;
/**
最大的线程数
*/
private volatile int maximumPoolSize;
/**
拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)
*/
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue 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;
}
ThreadPoolExecutor线程池的7大参数介绍:
corePoolSize:线程池中常驻核心线程数
maximumPoolSize:线程持能够容纳同时执行的最大线程数,此值必须大约等于1
keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到
keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
TimeUnit:keepAliveTime的单位
workQueue:任务队列,被提交但尚未被执行但任务。
ThreadFactory:示生成线程池中工作线程的工厂,用于创建线程一般用默认的即可。
handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize);
默认情况下:只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize
1、在创建了线程池后,等待提交过来的任务请求。
2、当调用execute()方法添加一个请求任务时,线程池会做如下判断:
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
JDK默认的四种拒绝策略:
1、RUNNING:
2、 SHUTDOWN
3、STOP
4、TIDYING
5、 TERMINATED
ThreadPoolExecutor中有个int型变量(其实是AtomicInteger类型)ctl:它的作用是存储线程池的状态和工作线程数量,原理是如何实现:
第一个问题是一个变量如何存储两个数据:
首先,int类型是4个字节,也就是32位,例如一个int值在计算机中的表示:
00000000 01000100 11111111 00000000
因为ThreadPoolExecutor中定义的状态有5种(RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED)
所以状态值至少要用3位,那么就可以用int的高3位来表示(最左边3个),剩下29个就可以表示线程数量(所以
线程数量最大值就是29位上全是1)。
每当线程池中的线程数量或状态发生变化时,具体操作的便是ctl变量,如以下方法:
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
那么又是读取线程状态和数量的值呢:
读取状态利用以下方法:
private static int runStateOf(int c) { return c & ~CAPACITY; }
CAPACITY是个常量00011111 11111111 11111111 11111111,通过 &(按位与)运算,
可以保留高3位,把低29位全部变为0;读取数量利用以下方法:
private static int workerCountOf(int c) { return c & CAPACITY; }
可以把高3位变为0,低29位保留。
其中传入的参数c,就是ctl。
下面是取自阿里的开发手册:
3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决 资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或 者“过度切换”的问题。
4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
1、CPU密集
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程)
CPU密集型任务配置尽可能少的线程数量:一般公式:CPU核数+1个线程的线程池。
2、IO密集
IO密集型:
第一种:由于IO密集型任务并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
第二种:IO密集型,即任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/1-阻塞系数(阻塞系数在0.8-0.9之间) 比如8核CPU:8/(1-0.9)=80个线程数
在Java 7中引入了一种新的线程池:ForkJoinPool。
它同ThreadPoolExecutor一样,也实现了Executor和ExecutorService接口。它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。
ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。
那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。
比如,我们需要统计一个double数组中小于0.5的元素的个数,那么可以使用ForkJoinPool进行实现如下:
public class ForkJoinTest {
private double[] d;
private class ForkJoinTask extends RecursiveTask {
private int first;
private int last;
public ForkJoinTask(int first, int last) {
this.first = first;
this.last = last;
}
protected Integer compute() {
int subCount;
if (last - first < 10) {
subCount = 0;
for (int i = first; i <= last; i++) {
if (d[i] < 0.5)
subCount++;
}
}
else {
int mid = (first + last) >>> 1;
ForkJoinTask left = new ForkJoinTask(first, mid);
left.fork();
ForkJoinTask right = new ForkJoinTask(mid + 1, last);
right.fork();
subCount = left.join();
subCount += right.join();
}
return subCount;
}
}
public static void main(String[] args) {
d = createArrayOfRandomDoubles();
int n = new ForkJoinPool().invoke(new ForkJoinTask(0, 9999999));
System.out.println("Found " + n + " values");
}
}
以上的关键是fork()和join()方法。在ForkJoinPool使用的线程中,会使用一个内部队列来对需要执行的任务以及子任务进行操作来保证它们的执行顺序。
那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢?
首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。
当然,在上面的例子中,也可以不使用分治法,因为任务之间的独立性,可以将整个数组划分为几个区域,然后使用ThreadPoolExecutor来解决,这种办法不会创建数量庞大的子任务。代码如下:
public class ThreadPoolTest {
private double[] d;
private class ThreadPoolExecutorTask implements Callable {
private int first;
private int last;
public ThreadPoolExecutorTask(int first, int last) {
this.first = first;
this.last = last;
}
public Integer call() {
int subCount = 0;
for (int i = first; i <= last; i++) {
if (d[i] < 0.5) {
subCount++;
}
}
return subCount;
}
}
public static void main(String[] args) {
d = createArrayOfRandomDoubles();
ThreadPoolExecutor tpe = new ThreadPoolExecutor(4, 4, Long.MAX_VALUE, TimeUnit.SECONDS, new LinkedBlockingQueue());
Future[] f = new Future[4];
int size = d.length / 4;
for (int i = 0; i < 3; i++) {
f[i] = tpe.submit(new ThreadPoolExecutorTask(i * size, (i + 1) * size - 1);
}
f[3] = tpe.submit(new ThreadPoolExecutorTask(3 * size, d.length - 1);
int n = 0;
for (int i = 0; i < 4; i++) {
n += f.get();
}
System.out.println("Found " + n + " values");
}
}
在分别使用ForkJoinPool和ThreadPoolExecutor时,它们处理这个问题的时间如下:
线程数 ForkJoinPool ThreadPoolExecutor
1 3.2s 0.31s
4 1.9s 0.15s
对执行过程中的GC同样也进行了监控,发现在使用ForkJoinPool时,总的GC时间花去了1.2s,而ThreadPoolExecutor并没有触发任何的GC操作。这是因为在ForkJoinPool的运行过程中,会创建大量的子任务。而当他们执行完毕之后,会被垃圾回收。反之,ThreadPoolExecutor则不会创建任何的子任务,因此不会导致任何的GC操作。
ForkJoinPool的另外一个特性是它能够实现工作窃取(Work Stealing),在该线程池的每个线程中会维护一个队列来存放需要被执行的任务。当线程自身队列中的任务都执行完毕后,它会从别的线程中拿到未被执行的任务并帮助它执行。
可以通过以下的代码来测试ForkJoinPool的Work Stealing特性:
for (int i = first; i <= last; i++) {
if (d[i] < 0.5) {
subCount++;
}
for (int j = 0; j < d.length - i; j++) {
for (int k = 0; k < 100; k++) {
dummy = j * k + i; // dummy is volatile, so multiple writes occur
d[i] = dummy;
}
}
}
因为里层的循环次数(j)是依赖于外层的i的值的,所以这段代码的执行时间依赖于i的值。当i = 0时,执行时间最长,而i = last时执行时间最短。也就意味着任务的工作量是不一样的,当i的值较小时,任务的工作量大,随着i逐渐增加,任务的工作量变小。因此这是一个典型的任务负载不均衡的场景。
这时,选择ThreadPoolExecutor就不合适了,因为它其中的线程并不会关注每个任务之间任务量的差异。当执行任务量最小的任务的线程执行完毕后,它就会处于空闲的状态(Idle),等待任务量最大的任务执行完毕。
而ForkJoinPool的情况就不同了,即使任务的工作量有差别,当某个线程在执行工作量大的任务时,其他的空闲线程会帮助它完成剩下的任务。因此,提高了线程的利用率,从而提高了整体性能。
这两种线程池对于任务工作量不均衡时的执行时间:
线程数 ForkJoinPool ThreadPoolExecutor
1 54.5s 53.3s
4 16.6s 24.2s
注意到当线程数量为1时,两者的执行时间差异并不明显。这是因为总的计算量是相同的,而ForkJoinPool慢的那一秒多是因为它创建了非常多的任务,同时也导致了GC的工作量增加。
当线程数量增加到4时,执行时间的区别就较大了,ForkJoinPool的性能比ThreadPoolExecutor好将近50%,可见Work Stealing在应对任务量不均衡的情况下,能够保证资源的利用率。
所以一个结论就是:当任务的任务量均衡时,选择ThreadPoolExecutor往往更好,反之则选择ForkJoinPool。
另外,对于ForkJoinPool,还有一个因素会影响它的性能,就是停止进行任务分割的那个阈值。比如在之前的快速排序中,当剩下的元素数量小于10的时候,就会停止子任务的创建。下表显示了在不同阈值下,ForkJoinPool的性能:
线程数 ForkJoinPool
20 17.8s
10 16.6s
5 15.6s
1 16.8s
可以发现,当阈值不同时,对于性能也会有一定影响。因此,在使用ForkJoinPool时,对此阈值进行测试,使用一个最合适的值也有助于整体性能。
在Java 8中,引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,前提是使用了ForkJoinPool。
Java 8为ForkJoinPool添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。它是ForkJoinPool类型上的一个静态元素,它拥有的默认线程数量等于运行计算机上的处理器数量。
当调用Arrays类上添加的新方法时,自动并行化就会发生。比如用来排序一个数组的并行快速排序,用来对一个数组中的元素进行并行遍历。自动并行化也被运用在Java 8新添加的Stream API中。
比如下面的代码用来遍历列表中的元素并执行需要的计算:
Stream stream = arrayList.parallelStream();
stream.forEach(a -> {
String symbol = StockPriceUtils.makeSymbol(a);
StockPriceHistory sph = new StockPriceHistoryImpl(symbol, startDate, endDate, entityManager);
});
对于列表中的元素的计算都会以并行的方式执行。forEach方法会为每个元素的计算操作创建一个任务,该任务会被前文中提到的ForkJoinPool中的通用线程池处理。以上的并行计算逻辑当然也可以使用ThreadPoolExecutor完成,但是就代码的可读性和代码量而言,使用ForkJoinPool明显更胜一筹。
对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。如果需要调整线程数量,可以通过设置系统属性:-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
下面的一组数据用来比较使用ThreadPoolExecutor和ForkJoinPool中的通用线程池来完成上面简单计算时的性能:
线程数 ThreadPoolExecutor(秒) ForkJoinPool Common Pool(秒)
1 255.6 135.4
2 134.8 110.2
4 77.0 96.5
8 81.7 84.0
16 85.6 84.6
注意到当线程数为1,2,4时,性能差异的比较明显。线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor的性能十分接近。
出现这种现象的原因是,forEach方法用了一些小把戏。它会将执行forEach本身的线程也作为线程池中的一个工作线程。因此,即使将ForkJoinPool的通用线程池的线程数量设置为1,实际上也会有2个工作线程。因此在使用forEach的时候,线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor是等价的。
所以当ForkJoinPool通用线程池实际需要4个工作线程时,可以将它设置成3,那么在运行时可用的工作线程就是4了。
总结
当需要处理递归分治算法时,考虑使用ForkJoinPool。
仔细设置不再进行任务划分的阈值,这个阈值对性能有影响。
Java 8中的一些特性会使用到ForkJoinPool中的通用线程池。在某些场合下,需要调整该线程池的默认的线程数量。
参考:https://blog.csdn.net/dm_vincent/article/details/39505977