【1】常见接口和实现类

① 什么是线程池

首先可以联想一下数据库连接池,Redis中的pool。

线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。


② 常见的体系结构

常见的线程池体系结构:

java.util.concurrent.Executor : 负责线程的使用与调度的根接口
|--ExecutorService 子接口: 线程池的主要接口
|--ThreadPoolExecutor 线程池的实现类
|--ScheduledExecutorService 子接口:负责线程的调度
|--ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor, 实现 ScheduledExecutorService

如下图所示:
多线程并发之线程池Executor与Fork/Join框架_线程池

线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。


③ 线程池的创建

为了便于跨大量上下文使用,此类提供了很多可调整的参数和扩展钩子(hook)。但是,强烈建议程序员使用较为方便的Executors 工厂方法:

  • Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)
  • Executors.newFixedThreadPool(int)(固定大小线程池)
  • Executors.newSingleThreadExecutor()(单个后台线程)
  • Executors.newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。

需要注意的是,它们均为大多数使用场景预定义了设置,也就是说通常情况下可以直接使用无需额外配置。但是实际上还有一个创建线程池的方法那就是手动构造线程池(ThreadPoolExecutor)

如下所示:

ScheduledExecutorService scheduledExecutorService = 
Executors.newScheduledThreadPool(5);

ExecutorService executorService = Executors.newCachedThreadPool();

ExecutorService executorService1 = Executors.newFixedThreadPool(5);

ExecutorService executorService2 = Executors.newSingleThreadExecutor();

Executors.newCachedThreadPool():

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

Executors.newFixedThreadPool(int):

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

Executors.newSingleThreadExecutor():

public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

Executors.newScheduledThreadPool():

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}

【2】线程池使用实例

线程池提交任务的三个方法:

<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);

实例代码如下:

package com.jane.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
* Created by Janus on 2018/9/28.
*/
public class TestThreadPool {

public static void main(String[] args) throws Exception {
//1.创建线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
//参数为Runnable
ThreadPoolRunnable threadPoolRunnable = new ThreadPoolRunnable();
// 2.为线程池中的线程分配任务
// for (int i = 0; i <10 ; i++) {
// pool.submit(threadPoolRunnable);
// }

//参数为Callable
ThreadPoolCallable threadPoolCallable = new ThreadPoolCallable();
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 0; i <10 ; i++) {
Future<Integer> future = pool.submit(threadPoolCallable);
futures.add(future);
}
for (Future<Integer> future : futures) {
System.out.println(future.get());
}
//3.关闭线程池
pool.shutdown();
}
}

class ThreadPoolRunnable implements Runnable{

private int i = 0;

@Override
public void run() {
while(i <= 100){
System.out.println(Thread.currentThread().getName() + " : " + i++);
}
}
}

class ThreadPoolCallable implements Callable<Integer>{

@Override
public Integer call() throws Exception {
int sum =0;
for(int i=1;i<=100;i++){
sum+=i;
}
System.out.println(Thread.currentThread().getName() + " : " +sum);
return sum;
}
}

【3】线程池调度实例

ScheduledExecutorService使用实例代码如下:

package com.jane.controller;

import java.util.Date;
import java.util.Random;
import java.util.concurrent.*;

/**
* Created by Janus on 2018/9/29.
*/
public class TestScheduledThreadPool {

public static void main(String[] args){

System.out.println("项目启动 : "+new Date());
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);

// 延迟两秒执行ScheduledThread
ScheduledThread scheduledThread = new ScheduledThread();
scheduledExecutorService.schedule(scheduledThread,2, TimeUnit.SECONDS);

//项目启动时延迟一秒,然后以2秒为周期执行
// ScheduledRunnable scheduledRunnable = new ScheduledRunnable();
// scheduledExecutorService.scheduleAtFixedRate(scheduledRunnable,1,2,TimeUnit.SECONDS);

// 上一次结束后 延迟一秒,然后以2秒为周期执行
// scheduledExecutorService.scheduleWithFixedDelay(scheduledRunnable,1,2,TimeUnit.SECONDS);
}
}

class ScheduledThread implements Callable<Integer>{

@Override
public Integer call() throws Exception {
int i = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName()+" : "+i+" , "+new Date());
return i;
}
}

class ScheduledRunnable implements Runnable{

@Override
public void run() {
int i = (int) (Math.random()*1000);
System.out.println(Thread.currentThread().getName()+" : "+i+" , "+new Date());
}
}

测试一,单独执行schedule:

ScheduledThread scheduledThread = new ScheduledThread();
scheduledExecutorService.schedule(scheduledThread,2, TimeUnit.SECONDS);

效果如下:

项目启动 : Sat Sep 29 10:37:16 CST 2018
pool-1-thread-1 : 69 , Sat Sep 29 10:37:19 CST 2018

在主程序启动2秒后执行,效果正常。


测试二,单独执行scheduleAtFixedRate:

ScheduledRunnable scheduledRunnable = new ScheduledRunnable();
scheduledExecutorService.scheduleAtFixedRate(scheduledRunnable,1,2,TimeUnit.SECONDS);

效果如下:

项目启动 : Sat Sep 29 10:38:51 CST 2018
pool-1-thread-1 : 475 , Sat Sep 29 10:38:52 CST 2018
pool-1-thread-1 : 444 , Sat Sep 29 10:38:54 CST 2018
pool-1-thread-2 : 37 , Sat Sep 29 10:38:56 CST 2018
pool-1-thread-1 : 619 , Sat Sep 29 10:38:58 CST 2018
pool-1-thread-3 : 296 , Sat Sep 29 10:39:00 CST 2018
pool-1-thread-3 : 304 , Sat Sep 29 10:39:02 CST 2018

主程序启动,一秒延迟后,以2秒为间隔周期执行。


测试三,单独执行scheduleWithFixedDelay:

ScheduledRunnable scheduledRunnable = new ScheduledRunnable();
scheduledExecutorService.scheduleWithFixedDelay(scheduledRunnable,1,2,TimeUnit.SECONDS);

效果如下:

项目启动 : Sat Sep 29 10:42:41 CST 2018
pool-1-thread-1 : 473 , Sat Sep 29 10:42:42 CST 2018
pool-1-thread-1 : 667 , Sat Sep 29 10:42:44 CST 2018
pool-1-thread-2 : 788 , Sat Sep 29 10:42:46 CST 2018
pool-1-thread-1 : 836 , Sat Sep 29 10:42:48 CST 2018
pool-1-thread-3 : 457 , Sat Sep 29 10:42:50 CST 2018
pool-1-thread-3 : 923 , Sat Sep 29 10:42:52 CST 2018
pool-1-thread-3 : 120 , Sat Sep 29 10:42:54 CST 2018

看起来和测试二一样?


测试四,添加ScheduledRunnable2 ,run中sleep一秒:

添加一个类ScheduledRunnable2 如下:

class ScheduledRunnable2 implements  Runnable{

@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int i = (int) (Math.random()*1000);
System.out.println(Thread.currentThread().getName()+" : "+i+" , "+new Date());
}
}

测试代码如下:

ScheduledRunnable2 scheduledRunnable2 = new ScheduledRunnable2();
scheduledExecutorService.scheduleWithFixedDelay(scheduledRunnable2,1,2,TimeUnit.SECONDS);

效果如下:

项目启动 : Sat Sep 29 10:51:15 CST 2018
pool-1-thread-1 : 521 , Sat Sep 29 10:51:17 CST 2018
pool-1-thread-1 : 334 , Sat Sep 29 10:51:20 CST 2018
pool-1-thread-2 : 388 , Sat Sep 29 10:51:23 CST 2018
pool-1-thread-1 : 528 , Sat Sep 29 10:51:26 CST 2018
pool-1-thread-3 : 862 , Sat Sep 29 10:51:29 CST 2018
pool-1-thread-3 : 312 , Sat Sep 29 10:51:32 CST 2018
pool-1-thread-3 : 534 , Sat Sep 29 10:51:35 CST 2018
pool-1-thread-3 : 452 , Sat Sep 29 10:51:38 CST 2018

启动后,延迟一秒,再sleep一秒,然后打印第一次调用;之后等待上一次结束后以2秒周期间隔再执行。

注意,后面间隔都是三秒,充分说明了第二次调用是在第一次调用结束后(因此每次执行都会sleep 1 秒)!


测试五 ,使用scheduleAtFixedRate调用scheduledRunnable2:

测试代码如下:

ScheduledRunnable2 scheduledRunnable2 = new ScheduledRunnable2();
scheduledExecutorService.scheduleAtFixedRate(scheduledRunnable2,1,2,TimeUnit.SECONDS);

效果如下:

项目启动 : Sat Sep 29 10:57:27 CST 2018
pool-1-thread-1 : 500 , Sat Sep 29 10:57:29 CST 2018
pool-1-thread-1 : 991 , Sat Sep 29 10:57:31 CST 2018
pool-1-thread-2 : 1 , Sat Sep 29 10:57:33 CST 2018
pool-1-thread-1 : 241 , Sat Sep 29 10:57:35 CST 2018
pool-1-thread-3 : 92 , Sat Sep 29 10:57:37 CST 2018
pool-1-thread-3 : 517 , Sat Sep 29 10:57:39 CST 2018

项目启动后,延迟一秒,又sleep 1 秒,然后打印第一次调用;之后以2秒间隔周期进行线程调用!!

由此可见FixedDelay与FixedRate区别:

  • FixedDelay 上一次结束后,等待X秒,执行下一次;
  • FixedRate 两次调用间隔X秒。

参考博文:SpringBoot中定时任务使用。


【4】Fork/Join 框架

Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join 汇总。

示意图如下:

多线程并发之线程池Executor与Fork/Join框架_线程池_02

实例代码模拟如下:

package com.jane.controller;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
* Created by Janus on 2018/9/29.
*/
public class TestForkJoinPool {

public static void main(String[] args){
//开始时间
Instant start = Instant.now();

//forkJoinTask执行离不开ForkJoinPool
ForkJoinPool pool = new ForkJoinPool();

// ForkJoinTask<Long> task = new ForkJoinSumCalculate(0, 1000000000L);
ForkJoinTask<Long> task = new ForkJoinSumCalculate(0, 50000000000L);

Long sum = pool.invoke(task);

System.out.println(sum);

//使用JDK1.8新特性--日期--结束时间
Instant end = Instant.now();

long millis = Duration.between(start, end).toMillis();//842-33044
System.out.println("耗费时间为 : "+millis);

}

}

class ForkJoinSumCalculate extends RecursiveTask<Long>{

private long start;
private long end;
//拆分临界值
private static final long THURSHOLD = 10000L;

public ForkJoinSumCalculate(long start,long end){
this.start = start;
this.end = end;
}

//The main computation performed by this task.
@Override
protected Long compute() {
long length = end-start;
//小于临界值,直接求和
if (length<THURSHOLD){
long sum = 0L;
for(long i=start;i<=end;i++){
sum+=i;
}
return sum;
}else{
//否则,进行递归拆分
long middle = (start+end)/2;
ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle);
left.fork();//进行拆分,同时压入线程队列

ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle+1, end);
right.fork();

return left.join()+right.join();
}
}
}

分别使用for循环和java8新特性测试如下:

// for 循环
@Test
public void testFor(){

Instant start = Instant.now();

long sum = 0L;
// for(long i = 0;i<=1000000000L;i++){
for(long i = 0;i<=50000000000L;i++){
sum+=i;
}
System.out.println(sum);
Instant end = Instant.now();
long millis = Duration.between(start, end).toMillis();//2526--22117
System.out.println("耗费时间为 : "+millis);
}

// java8新特性--并行流
@Test
public void test2(){
Instant start = Instant.now();
// long sum = LongStream.rangeClosed(0L, 1000000000L).parallel().reduce(0L, Long::sum);
long sum = LongStream.rangeClosed(0L, 50000000000L).parallel().reduce(0L, Long::sum);
System.out.println(sum);
Instant end = Instant.now();
long millis = Duration.between(start, end).toMillis();//1699--18909
System.out.println("耗费时间为 : "+millis);
}

总结如下:

  • 第一求和数据比较小的时候无需考虑哪种方式;
  • 第二,求和数据比较大的时候java8新特性是优异于for循环的;
  • 第三,求和数据比较大的时候使用ForkJoin需要考虑临界值的设置,否则可能效率不如for循环和java8新特性。

【5】Fork/Join 框架与线程池的区别

① Fork/Join采用“工作窃取”模式(work-stealing)

当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。

② Fork/Join优势

相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上。

在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。

③ ForkJoinPool继承图

如下图所示ForkJoinPool是AbstractExecutorService子类实现了ExecutorService接口(其是Executor接口子类)。
多线程并发之线程池Executor与Fork/Join框架_Fork/Join_03


【6】ThreadPoolExecutor

前面提到的Executors的几个静态方法的实现,其实就用到了ThreadPoolExecutor,只是根据不同的场景传入了不同的参数。完整的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;
}
  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:当线程数大于corePoolSize时,这是多余的空闲线程在终止前等待新任务的最长时间
  • unit:keepAliveTime 存活时间的单位
  • workQueue:工作任务队列,用于保存任务并将其传递给工作线程的队列。
  • threadFactory:构造线程池中线程的工厂
  • handler:由于达到线程边界和队列容量而阻止执行时要使用的处理程序

解释如下:

  • 提交任务的时候,判断当前线程池中的存活线程数量是否小于corePoolSize
  • 如果小于corePoolSize,则不管是否有线程处于空闲状态,都会新建一个线程。
  • 如果线程数量已经达到corePoolSize,则将任务扔进队列workQueue
  • 随着任务越来越多,队列可能已经满了,则需要看当前线程是否已经达到了maximumPoolSize,如果没有达到,则创建新的线程,并用它执行该任务。
  • 最坏情况,任务实在太多了,队列已经满了,而线程数量已经达到maximumPoolSize,还有新的任务来,说白了,就是已经满负荷了,任然还有任务需要执行,这个时候就会handler来处理该任务了。

另外三个参数keepAliveTime、unit 和factory 说明如下:

  • keepAliveTime。如我们所知,使用线程池的目的就是为了减少线程的创建,因为创建线程本身是比较耗资源的。由于线程本身需要占用资源,有一种情况就是,某个时候线程数量比较多,但是任务没有多少,就会出现有的线程没有活干,所以我们就可以考虑释放掉其资源,但是呢,我们又无法预知未来的任务量,所以我们就准许其空闲一段时间,如果过了这段时间都还是空闲的,那么就会释放掉其资源,这个参数就是用指定这段空闲时间的。默认情况下是有超过corePoolSize个线程时,就会用到该值, 但是也可以指定corePoolSize数量之内的线程空闲时是否释放资源(allowCoreThreadTimeout)。
  • unit 这个参数很好理解,就是单位,就是前面keepAliveTime这个我们准许空闲的时间的单位
  • factory .其类型为ThreadFactory,顾名思义,就是一个创建Thread的Factory. 该接口只有一个方法,产生一个Thread。通常情况下,我们都只需要使用默认的factory就可以了,但是为了定位问题方便,我们可以为线程池创建的线程设置一个名字,这样看日志的时候就比较方便了。

【7】RejectedExecutionHandler

采用Executors静态方法时,并没有让传入该参数,默认值如下:

private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();

实际上jdk本身提供了四种策略分别是:

  • AbortPolicy:会抛出异常
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
  • CallerRunnerPolicy:在调用execute的方法中执行被拒绝的任务
public static class CallerRunsPolicy implements RejectedExecutionHandler {

public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
  • DiscardOldestPolicy:丢掉队列中最老的任务,然后重试
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
  • DiscardPolicy:直接丢掉该任务
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }

/**
* Does nothing, which has the effect of discarding task r.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}

另外还有ThreadPoolExecutor还提供了一些hook方法,如有需要可以使用

  • beforeExecute() 任务执行之前调用
  • afterExecute() 任务执行之后调用

多线程并发项目代码下载地址:​​GitHub-JUC​​。

参考博文:JDK1.8新特性之Stream