java并发编程【二】Executor框架与线程池

文章目录

  • 一、Executor框架
    • 1.1、 this引用逃逸
    • 1.2、Executor框架
  • 二、线程池ThreadPoolExecutor
    • 2.1、线程池优点
    • 2.2、线程池方法
    • 2.3、线程池创建
    • 2.4、线程池参数
    • 2.5、运行原理
    • 2.6、线程池拒绝策略
      • 2.6.1、AbortPolicy
      • 2.6.2、CallerRunsPolicy
      • 2.6.3、DiscardOldestPolicy
      • 2.6.4、DiscardPolicy
  • 三、自定义线程池ThreadPoolExecutor
    • 3.1、线程池的使用
    • 3.2、设置参数
    • 3.3、自定义线程池工厂类
      • 3.3.1、自定义工厂类
      • 3.3.2、应用自定义工厂类
    • 3.4、扩展ThreadPoolExecutor
    • 3.5、拒绝策略扩展
      • 3.5.1、dubbo中拒绝策略
      • 3.5.2、Netty中拒绝策略
      • 3.5.3、ActiveMQ中拒绝策略
      • 3.5.4、pinpoint中拒绝策略
      • 3.5.4、自定义拒绝策略
  • 四、线程池ForkJoinPool
    • 4.1、分治算法
    • 4.2、Fork/Join框架
    • 4.3、Fork/Join使用
      • 4.3.1、ForkJoinPool
      • 4.3.2、ForkJoinTask
    • 4.4、ForkJoinPool案例
  • 五、Callable、Futrue与 FutureTask
    • 5.1、 Runnable与 Callable
    • 5.2、 Future与 FutureTask
      • 5.2.1、Future应用
      • 5.2.2、FutureTask 应用
    • 5.3、CompletionService
      • 5.3.1、 源码
      • 5.3.2、对比
        • (1)Futrue
        • (2)CompletionService
    • 5.4、CompletableFuture
      • (1)案例1
    • 5.5、小结
  • 六、常见问题
      • 6.1、线程饥饿死锁


一、Executor框架

知识体系如下:

java并发编程【二】Executor框架与线程池_第1张图片

1.1、 this引用逃逸

this指针逃逸是指在构造函数返回之前,其他线程就已经持有了该对象的引用(doSomething),产生的结果难以预期。

public class ThisEscape{
    public ThisEscape() {
        new Thread(new EscapeRunnable()).start();
    }
    private class EscapeRunnable implements Runnable{
        @Override
        public void run() {
            //这里引用未初始化完毕的对象【此处省略了this】
            doSomething();
        }
    }

    private void doSomething(){
        System.out.println("此方法属于ThisEscape");
    }
}

改进方法:将线程启动或者监听器移到init方法中

public class ThisEscape{
   private Thread t;
    public ThisEscape() {
        t = new Thread(new EscapeRunnable());
    }
    public void init(){
        t.start();
   }
    private class EscapeRunnable implements Runnable{
        @Override
        public void run() {
             //这里引用的对象已经初始化完毕
             doSomething();
        }
    }

	private void doSomething(){
        System.out.println("此方法属于ThisEscape");
    }
}

1.2、Executor框架

为避免this逃逸问题,Eexecutor作为灵活且强大的异步执行框架,基于生产者-消费者模式,提供了一个标准,可以将任务的提交过程执行过程解耦,其提交任务的线程相当于生产者,执行任务的线程相当于消费者,并用Runnable来表示任务。
其内部使用了线程池机制,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。

java并发编程【二】Executor框架与线程池_第2张图片

Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

接口或类 描述
Executor接口 定义了一个接收Runnable对象的方法executor
ExecutorService接口 提供了生命周期管理的方法,返回 Future 对象,一般用该接口来实现和管理多线程。
ThreadPoolExecutor 实现了ExecutorService,主要作用是提供有参构造方法创建线程池。
Executors工厂类 主要用来创建不同类型的线程池,返回的线程池都实现了ExecutorService接口。

二、线程池ThreadPoolExecutor

线程池其实是池化技术的应用一种,常见的池化技术还有很多,例如数据库的连接池、Java中的内存池、常量池等等。

池化技术的核心思想其实就是实现资源的一个复用,避免资源的重复创建和销毁带来的性能开销。在线程池中,线程池可以管理一堆线程,让线程执行完任务之后不会进行销毁,而是继续去处理其它线程已经提交的任务。

2.1、线程池优点

降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控;
提高系统响应速度,因为池中有现成的资源,不用重新去创建;

2.2、线程池方法

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

方法 描述
void execute(Runnable command) 执行无返回值的任务,一般用来执行Runnable
Future submit(Callable task) 执行有返回值的任务,一般来执行Callable
shutdownNow 立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
shutdown 平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
isTerminated 当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

方法 描述
Executors.newCachedThreadPool() 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n) 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n) 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行

2.3、线程池创建

Executors.newCachedThreadPool()

创建可缓存无限制数量的线程池,如果线程中没有空闲线程池的话此时再来任务会新建线程,如果超过60秒此线程无用,那么就会将此线程销毁。简单来说就是忙不来的时候无限制创建临时线程,闲下来再回收

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

Executors.newFixedThreadPool()

创建固定大小的线程池,可控制线程最大的并发数,超出的线程会在队列中等待。简单来说就是忙不来的时候会将任务放到无限长度的队列里。

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

Executors.newSingleThreadExecutor()

创建线程池中线程数量为1的线程池,用唯一线程来执行任务,保证任务是按照指定顺序执行

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

Executors.newScheduledThreadPool()

创建固定大小的线程池,支持定时及周期性的任务执行

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

不建议使用Executors创建线程池

java并发编程【二】Executor框架与线程池_第3张图片

FixedThreadPool和SingleThreadExecutor:这两个线程池的实现方式,我们可以看到它设置的工作队列都是LinkedBlockingQueue,我们知道此队列是一个链表形式的队列,此队列是没有长度限制的,是一个无界队列,那么此时如果有大量请求,就有可能造成OOM。

CachedThreadPool和ScheduledThreadPool:这两个线程池的实现方式,我们可以看到它设置的最大线程数都是Integer.MAX_VALUE,那么就相当于允许创建的线程数量为Integer.MAX_VALUE。此时如果有大量请求来的时候也有可能造成OOM。

2.4、线程池参数

public ThreadPoolExecutor(    int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
参数 描述
corePoolSize 线程池中核心线程数的数量
maximumPoolSize 在线程池中允许存在的最大线程数
keepAliveTime 当存在的线程数大于corePoolSize,那么会找到空闲线程去销毁,此参数是设置空闲多久的线程才被销毁。
unit 时间单位
workQueue 工作队列,线程池中的当前线程数大于核心线程的话,那么接下来的任务会放入到队列中
threadFactory 在创建线程的时候,通过工厂模式来生产线程。可以设置我们自定义的线程创建工厂
handler 如果超过了最大线程数,那么就会执行我们设置的拒绝策略

2.5、运行原理

execute方法源码

public void execute(Runnable command) {

    if (command == null)
        throw new NullPointerException();
    // ctl是一个32位的整形值,前3为代表线程池的状态,后29位代表当前线程池中线程的数量
    int c = ctl.get();
    // workerCountOf(c)这个方法是通过位运算计算当前线程池线程数量;当线程数量小于核心线程数,那么执行添加工作线程的操作
    if (workerCountOf(c) < corePoolSize) {
    // 创建新的工作线程;如果添加成功,直接返回;
        if (addWorker(command, true))
            return;
        // 添加工作线程不成功,有可能是当前核心线程数已经满了;
        //在多线程执行任务的时候,有可能判断的时候确实没有达到核心线程数,但是当真正添加的时候,前面已经有任务添加达到了核心线程数了
        c = ctl.get();
    }
  
    // 如果线程池还在运行状态,那么就把这个任务添加到队列中去
    if (isRunning(c) && workQueue.offer(command)) {
         // 重新获取线程池状态
        int recheck = ctl.get();

         // 如果线程池此刻没有处于运行状态,那么就把之前队列中的任务推出来
        if (! isRunning(recheck) && remove(command))
            // 把推出来的任务执行拒绝策略
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 上面如果添加到队列中也失败了,那么就创建非核心线程
    else if (!addWorker(command, false))
        // 如果添加非核心线程失败,那么执行拒绝策略
        reject(command);
}

线程池工作原理如下:

java并发编程【二】Executor框架与线程池_第4张图片

1、线程数< corePoolSize时,来一个任务就创建一个线程
2、当前线程池的线程数大于了corePoolSize时,新来的任务会放入 【workQueue】队列中
3、workQueue也满时,再来任务就会新建临时线程,
4、系统会进行线程活性检查,一旦超时会销毁临时线程,超时时间【keepAliveTime】【unit】
5、当前线程大于了maximumPoolSize最大线程数,会执行【handler】拒绝策略

2.6、线程池拒绝策略

参数 描述
AbortPolicy 丢弃任务,抛出运行时异常
CallerRunsPolicy 由提交任务的线程来执行任务
DiscardPolicy 丢弃这个任务,但是不抛异常
DiscardOldestPolicy 从队列中剔除最先进入队列的任务,然后再次提交任务

2.6.1、AbortPolicy

public static class AbortPolicy implements RejectedExecutionHandler {
 
    public AbortPolicy() { }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

AbortPolicy: 拒绝策略,直接抛异常 RejectedExecutionException
如果不指定拒绝策略就默认是AbortPolicy策略。当然,你也可以自己实现RejectedExecutionHandler接口,比如将任务存在数据库或者缓存中,这样就数据库或者缓存中获取到被拒绝掉的任务了。

2.6.2、CallerRunsPolicy

public static class CallerRunsPolicy implements RejectedExecutionHandler {

    public CallerRunsPolicy() { }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

CallerRunsPolicy:将此任务交给调用者直接执行,调用run方法

2.6.3、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);
            }
        }
    }

DiscardOldestPolicy:丢弃队列中最老的任务,然后再执行。

2.6.4、DiscardPolicy

public static class DiscardPolicy implements RejectedExecutionHandler {

    public DiscardPolicy() { }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}

DiscardPolicy:直接丢弃了当前任务不执行。


三、自定义线程池ThreadPoolExecutor

3.1、线程池的使用

public class ThreadPoolExecutorDemo {
 
    public static void main(String[] args) {
        //设置核心池大小
        int corePoolSize = 5;
        //设置线程池最大能接受多少线程
        int maximumPoolSize = 10;
        //当前线程数大于corePoolSize、小于maximumPoolSize时,超出corePoolSize的线程数的生命周期
        long keepAliveTime = 100;
        //设置时间单位是秒
        TimeUnit unit = TimeUnit.SECONDS;
        //设置线程池缓存队列的排队策略为FIFO,并且指定缓存队列大小为5
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
        // 设置线程工厂
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 设置线程饱和策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
 
        //创建ThreadPoolExecutor线程池对象,并初始化该对象的各种参数
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
 
 
        //往线程池中循环提交线程
        for (int i = 0; i < 15; i++) {
            //创建线程任务对象
            ThreadTask threadTask = new ThreadTask(i);
            //开启线程
            threadPoolExecutor.execute(threadTask);
            //获取线程池中线程的相应参数
            System.out.println("线程池中线程数目:" + threadPoolExecutor.getPoolSize()
                    + ",队列中等待执行的任务数目:"+ threadPoolExecutor.getQueue().size()
                    + ",已执行完的任务数目:"+ threadPoolExecutor.getCompletedTaskCount());
        }
        //待线程池以及缓存队列中所有的线程任务完成后关闭线程池。
        threadPoolExecutor.shutdown();
    }
 
 
    public static class ThreadTask implements Runnable{
        private int num;
 
        public ThreadTask(int num) {
            this.num = num;
        }
 
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "==正在执行task " + num);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "==正在完毕 " + num);
        }
    }
}

3.2、设置参数

CPU密集型任务:
比如有N个CPU,那么就配置线程池的容量大小为N+1,这样能获得最优的利用率。因为CPU密集型的线程恰好在某时因为发生一个页错误或者因为其他的原因而暂停,刚好有一个额外的线程,可以确保在这种情况下CPU周期不会中断工作。

IO密集任务:
CPU大部分时间都是在等待IO的阻塞操作,那么此时就可以将线程池的容量大小配置的大一些
N:CPU的数量
U:目标CPU的使用率,0<=U<=1
W/C:等待时间与计算时间的比率
大小就是NU(1+W/C)

《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法:线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)。通过这个公式,我们可以计算出一个合理的线程数量,如果任务的平均等待时间长,线程数就随之增加,而如果平均工作时间长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。


3.3、自定义线程池工厂类

每个线程自定义名称

3.3.1、自定义工厂类

class CustomerThreadFactory implements ThreadFactory{

    private String name;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    
    CustomerThreadFactory(String name){
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r,name+threadNumber.getAndIncrement());
        return thread;
    }
}

3.3.2、应用自定义工厂类

public static void customerThread(){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0,Integer.MAX_VALUE,1, TimeUnit.SECONDS,new SynchronousQueue<>(),
                new CustomerThreadFactory("customerThread"));

        for (int i = 0; i < 10; i++) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.printf(Thread.currentThread().getName());
                    System.out.printf("n");
                }
            });
        }
    }

3.4、扩展ThreadPoolExecutor

ThreadPoolExecutor源码可以发现源码中有三个方法都是protected, 被protected修饰的成员对于本包和其子类可见,
我们可以通过继承来覆写这些方法,那么就可以进行我们独有的扩展了。

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }

执行任务的线程会调用beforeExecute和afterExecute方法,可以通过它们添加日志、时序、监视器或者同级信息收集的功能。

线程池完成关闭时调用terminated,也就是在所有任务都已经完成并且所有工作者线程也已经关闭后,terminated可以用来释放Executor在其生命周期里分配的各种资源,此外还可以执行发送通知、记录日志或者手机finalize统计等操作。

注意:

无论任务是正常从run中返回,还是抛出一个异常,afterExecute都会被调用(如果任务完成后抛出一个Error,则afterExecute不会被调用)。
如果beforeExecute抛出一个RuntimeException,任务将不会被执行,afterExecute也不会被调用。

3.5、拒绝策略扩展

通过自定义的拒绝策略,做好监控,可以帮助我们快速的定位问题,及时的人工修复问题。

3.5.1、dubbo中拒绝策略

public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
 
    protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);
 
    private final String threadName;
 
    private final URL url;
 
    private static volatile long lastPrintTime = 0;
 
    private static Semaphore guard = new Semaphore(1);
 
    public AbortPolicyWithReport(String threadName, URL url) {
        this.threadName = threadName;
        this.url = url;
    }
 
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        String msg = String.format("Thread pool is EXHAUSTED!" +
                        " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
                        " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
                threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
                e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
                url.getProtocol(), url.getIp(), url.getPort());
        logger.warn(msg);
        dumpJStack();
        throw new RejectedExecutionException(msg);
    }
 
    private void dumpJStack() {
       //省略实现
    }
}

可以看到,当dubbo的工作线程触发了线程拒绝后,主要做了三个事情,记录触发线程拒绝策略的真实原因。

1)输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在

2)输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。

3)继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性。

3.5.2、Netty中拒绝策略

private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
        NewThreadRunsPolicy() {
            super();
        }
 
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                final Thread t = new Thread(r, "Temporary task executor");
                t.start();
            } catch (Throwable e) {
                throw new RejectedExecutionException(
                        "Failed to start a new thread", e);
            }
        }
    }

Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的。

所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常

3.5.3、ActiveMQ中拒绝策略

new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
        try {
            executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker");
        }

        throw new RejectedExecutionException("Timed Out while attempting to enqueue Task.");
    }
});

activeMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常

3.5.4、pinpoint中拒绝策略

public class RejectedExecutionHandlerChain implements RejectedExecutionHandler {
    private final RejectedExecutionHandler[] handlerChain;
 
    public static RejectedExecutionHandler build(List<RejectedExecutionHandler> chain) {
        Objects.requireNonNull(chain, "handlerChain must not be null");
        RejectedExecutionHandler[] handlerChain = chain.toArray(new RejectedExecutionHandler[0]);
        return new RejectedExecutionHandlerChain(handlerChain);
    }
 
    private RejectedExecutionHandlerChain(RejectedExecutionHandler[] handlerChain) {
        this.handlerChain = Objects.requireNonNull(handlerChain, "handlerChain must not be null");
    }
 
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        for (RejectedExecutionHandler rejectedExecutionHandler : handlerChain) {
            rejectedExecutionHandler.rejectedExecution(r, executor);
        }
    }
}

pinpoint的拒绝策略实现很有特点,和其他的实现都不同。他定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的rejectedExecution依次执行一遍。

3.5.4、自定义拒绝策略

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义拒绝策略的逻辑
    }
});

或者

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setRejectedExecutionHandler((r, executor1) -> {
    // 自定义拒绝策略的逻辑
});

四、线程池ForkJoinPool

传统线程池ThreadPoolExecutor有两个明显的缺点:

  1. 无法对大任务进行拆分,对于某个任务只能由单线程执行;
  2. 工作线程从队列中获取任务时存在竞争情况。

这两个缺点都会 影响任务的执行效率。为了解决传统线程池的缺陷,Java7中引入Fork/Join框架,并在Java8中 得到广泛应用。

4.1、分治算法

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
分治算法的步骤如下:

  1. 分解:将要解决的问题划分成若干规模较小的同类问题;
  2. 求解:当子问题划分得足够小时,用较简单的方法解决;
  3. 合并:按原问题的要求,将子问题的解逐层合并构成原问题的解。

java并发编程【二】Executor框架与线程池_第5张图片
分治思想在很多领域都有广泛的应用,例如算法领域有分治算法(归并排序、快速排序都 属于分治算法,二分法查找也是一种分治算法);Java 并发包 里也提供了一种叫做 Fork/Join 的并行计算框架。

4.2、Fork/Join框架

Fork/Join框架的核心是ForkJoinPool类,它是对 AbstractExecutorService类的扩展。ForkJoinPool允许其他线程向它提交任务,并根据设定将这些任务拆分为粒度更细的子任务,这些子任务将由ForkJoinPool内部的工作线程来并行执行,并且工作线程之间可以窃取彼此之间的任务。
java并发编程【二】Executor框架与线程池_第6张图片
ForkJoinPool最适合计算密集型任务,而且最好是非阻塞任务ForkJoinPool是ThreadPoolExecutor线程池的一种补充,是对计算密集型场景的加强。
根据经验和实验,任务总数单任务执行耗时 以及 并行数 都会影响到Fork/Join的性能。所以,当你使用Fork/Join框架时,你需要谨慎评估这三个指标,最好能通过模拟对比评估,不要凭感觉冒然在生产环境使用。

4.3、Fork/Join使用

Fork/Join 计算框架主要包含两部分,一部分是分治任务的线程池 ForkJoinPool,另一部分是分治任务 ForkJoinTask。


4.3.1、ForkJoinPool

ForkJoinPool 是用于执行 ForkJoinTask 任务的执行池,不再是传统执行池 Worker+Queue 的组合式,而是维护了一个队列数组 WorkQueue(WorkQueue[]),这样在提交任务和线程任务的时候大幅度减少碰撞。

提交任务三种方式

java并发编程【二】Executor框架与线程池_第7张图片

  1. execute类型的方法在提交任务后,不会返回结果。ForkJoinPool不仅允许提交ForkJoinTask类型任务,还允许提交Runnable任务,将会转换为ForkJoinTask类型。由于任务是不可切分的,所以这类任务无法获得任务拆分这方面的效益,不过仍然可以获得任务窃取带来的好处和性能提升。
  2. invoke方法接受ForkJoinTask类型的任务,并在任务执行结束后,返回泛型结果。如果提交的任务是null,将抛出空指针异常。
  3. submit方法支持三种类型的任务提交:ForkJoinTask类型、Callable类型和Runnable类型。在提交任务后,将返回ForkJoinTask类型的结果。如果提交的任务是null,将抛出空指针异常,并且当任务不能按计划执行的话,将抛出任务拒绝异常

4.3.2、ForkJoinTask

ForkJoinTask是ForkJoinPool的核心之一,它是任务的实际载体,定义了任务执行时的具体逻辑和拆分逻辑。ForkJoinTask继承了Future接口,所以也可以将其看作是轻量级的Future。
ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,承载着主要的任务协调作用,一个用于任务提交,一个用于结果获取。

  • fork()——提交任务
    fork方法用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。

  • join()——获取任务执行结果
    join方法用于获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果。

ForkJoinTask使用限制:

ForkJoinTask最适合用于纯粹的计算任务,也就是纯函数计算,计算过程中的对象都是独立的,对外部没有依赖。提交到ForkJoinPool中的任务应避免执行阻塞I/O

4.4、ForkJoinPool案例

计算从1到1亿之间所有数字之和:

package com.javakk;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;

/**
 * ForkJoinPool求和
 * @author 老K
 */
public class ForkJoinPoolTest {

    private static ForkJoinPool forkJoinPool;

    /**
     * 求和任务类继承RecursiveTask
     * ForkJoinTask一共有3个实现:
     * RecursiveTask:有返回值
     * RecursiveAction:无返回值
     * CountedCompleter:无返回值任务,完成任务后可以触发回调
     */
    private static class SumTask extends RecursiveTask<Long> {
        private long[] numbers;
        private int from;
        private int to;

        public SumTask(long[] numbers, int from, int to) {
            this.numbers = numbers;
            this.from = from;
            this.to = to;
        }

        /**
         * ForkJoin执行任务的核心方法
         * @return
         */
        @Override
        protected Long compute() {
            if (to - from < 10) { // 设置拆分的最细粒度,即阈值,如果满足条件就不再拆分,执行计算任务
                long total = 0;
                for (int i = from; i <= to; i++) {
                    total += numbers[i];
                }
                return total;
            } else { // 否则继续拆分,递归调用
                int middle = (from + to) / 2;
                SumTask taskLeft = new SumTask(numbers, from, middle);
                SumTask taskRight = new SumTask(numbers, middle + 1, to);
                taskLeft.fork();
                taskRight.fork();
                return taskLeft.join() + taskRight.join();
            }
        }
    }

    public static void main(String[] args) {
        // 也可以jdk8提供的通用线程池ForkJoinPool.commonPool
        // 可以在构造函数内指定线程数
        forkJoinPool = new ForkJoinPool();
        long[] numbers = LongStream.rangeClosed(1, 100000000).toArray();
        // 这里可以调用submit方法返回的future,通过future.get获取结果
        Long result = forkJoinPool.invoke(new SumTask(numbers, 0, numbers.length - 1));
        forkJoinPool.shutdown();
        System.out.println("最终结果:"+result);
        System.out.println("活跃线程数:"+forkJoinPool.getActiveThreadCount());
        System.out.println("窃取任务数:"+forkJoinPool.getStealCount());
    }
}

最终结果:5000000050000000
活跃线程数:4
窃取任务数:12

上例中在compute方法里拆分的最小粒度是10个元素,大家可以改成其他的值试下,会发现执行的效率差别很大,所以要注意拆分粒度对性能的影响。

并行流【并行流底层内部使用的线程池也是ForkJoinPool】

     //采用并行流(JDK8以后的推荐做法)
    public static void main(String[] args) {

        Instant start = Instant.now();
        long result = LongStream.rangeClosed(0, 10000000L).parallel().reduce(0, Long::sum);
        Instant end = Instant.now();
        System.out.println("耗时:" + Duration.between(start, end).toMillis() + "ms");

        System.out.println("结果为:" + result); // 打印结果500500

    }
输出:
耗时:130ms
结果为:50000005000000


五、Callable、Futrue与 FutureTask

5.1、 Runnable与 Callable

Executor框架使用Runnable作为其基本的任务表示形式。但是 Runnable不能返回一个值或抛出一个受检查的异常。

public interface Runnable {
    public abstract void run();
}

由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。所以引入了Callable接口,它会返回一个值,并可能抛出一个异常。

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。

5.2、 Future与 FutureTask

Future

Executor执行的任务有4个生命周期阶段:创建、提交、开始和完成。由于有些任务执行过长,我们希望取消这些任务。在Executor框架中,已提交但尚未开始的任务可以取消,对于已经开始执行的任务,只有当它们响应中断时才能取消。

Future是一种设计模式,用来获取异步任务的结果,提供了方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。

public interface Future<V> {
    //试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败。
    //当调用 cancel 时,如果调用成功,而此任务尚未启动, 则此任务将永不运行。
    //如果任务已经启动,则mayInterruptIfRunning 参数确定是否应该以试图停止任务的方式来中断执行此任务的线程。
    boolean cancel(boolean mayInterruptIfRunning);
 
    //如果在任务正常完成前将其取消,则返回 true。 
    boolean isCancelled();
 
   //如果任务已完成,则返回 true。可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。
    boolean isDone();
 
   //等待线程结果返回,会阻塞
    V get() throws InterruptedException, ExecutionException;
 
   //设置超时时间
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask

java并发编程【二】Executor框架与线程池_第8张图片

FutureTask类是Future 的一个实现,并实现了Runnable,所以可通过Excutor(线程池) 来执行,也可传递给Thread对象执行。如果在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。 link

Executor框架利用FutureTask来完成异步任务,并可以用来进行任何潜在的耗时的计算。一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。

FutureTask类既可以使用new Thread(Runnable r)放到一个新线程中跑,也可以使用ExecutorService.submit(Runnable r)放到线程池中跑,而且两种方式都可以获取返回结果,但实质是一样的,即如果要有返回结果那么构造函数一定要注入一个Callable对象。

5.2.1、Future应用

package com.func.axc.futuretask;
 
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
/**
 * 功能概要:
 * 
 * @author linbingwen
 * @since  2016年6月8日 
 */
public class FutureTest {
 
	/**
	 * @author linbingwen
	 * @since  2016年6月8日 
	 * @param args    
	 */
	public static void main(String[] args) {
		   System.out.println("main Thread begin at:"+ System.nanoTime());
		    ExecutorService executor = Executors.newCachedThreadPool();
		    HandleCallable task1 = new HandleCallable("1");
		    HandleCallable task2 = new HandleCallable("2");
		    HandleCallable task3 = new HandleCallable("3");
	        Future<Integer> result1 = executor.submit(task1);
	        Future<Integer> result2 = executor.submit(task2);
	        Future<Integer> result3 = executor.submit(task3);
	        executor.shutdown();
	        try {
	            Thread.sleep(1000);
	        } catch (InterruptedException e1) {
	            e1.printStackTrace();
	        }
	        try {
	            System.out.println("task1运行结果:"+result1.get());
	            System.out.println("task2运行结果:"+result2.get());
	            System.out.println("task3运行结果:"+result3.get());
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        } catch (ExecutionException e) {
	            e.printStackTrace();
	        }
	        System.out.println("main Thread finish at:"+ System.nanoTime());
	}
 
}
 
class HandleCallable implements Callable<Integer>{
	private String name;
	public HandleCallable(String name) {
		this.name = name;
	}
	
    @Override
    public Integer call() throws Exception {
		System.out.println("task"+ name + "开始进行计算");
		Thread.sleep(3000);
		int sum = new Random().nextInt(300);
		int result = 0;
		for (int i = 0; i < sum; i++)
			result += i;
		return result;
    }
}

执行结果:
java并发编程【二】Executor框架与线程池_第9张图片

5.2.2、FutureTask 应用

package com.func.axc.futuretask;
 
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
import org.springframework.scheduling.config.Task;
 
/**
 * 功能概要:
 * 
 * @author linbingwen
 * @since 2016年5月31日
 */
public class FutrueTaskTest {
 
	public static void main(String[] args) {
		//采用直接启动线程的方法
		System.out.println("main Thread begin at:"+ System.nanoTime());
		MyTask task1 = new MyTask("1");
        FutureTask<Integer> result1 = new FutureTask<Integer>(task1);
        Thread thread1 = new Thread(result1);
        thread1.start();
        
		MyTask task2 = new MyTask("2");
        FutureTask<Integer> result2 = new FutureTask<Integer>(task2);
        Thread thread2 = new Thread(result2);
        thread2.start();
 
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}
		
		try {
			System.out.println("task1返回结果:"  + result1.get());
			System.out.println("task2返回结果:"  + result2.get());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
 
		System.out.println("main Thread finish at:"+ System.nanoTime());
		
	}
}
 
class MyTask implements Callable<Integer> {
	private String name;
	
	public MyTask(String name) {
		this.name = name;
	}
	
	@Override
	public Integer call() throws Exception {
		System.out.println("task"+ name + "开始进行计算");
		Thread.sleep(3000);
		int sum = new Random().nextInt(300);
		int result = 0;
		for (int i = 0; i < sum; i++)
			result += i;
		return result;
	}
 
}

执行结果:
java并发编程【二】Executor框架与线程池_第10张图片

5.3、CompletionService

CompletionService就是一个将线程池执行结果放入到一个Blockqueueing的类。
如果在线程池中我们使用Future或FutureTask来取得返回结果,比如。我们开了100条线程。但是这些线程的执行时间是未知的。但是我们又需要返回结果。每执行一条线程就根据结果做一次相应的操作。

如果是Future或FutureTask。我们只能通过一个循环,不断的遍历线程池里的线程。取得其执行状态。然后再来取结果。这样效率就太低了,有可能发生一条线程执行完毕了,但我们不能立刻知道它处理完成了。还得通过一个循环来判断。基本上面的这种问题,所以出了CompletionService。

CompletionService原理不是很难,它就是将一组线程的执行结果放入一个BlockQueueing当中。这里线程的执行结果放入到Blockqueue的顺序只和这个线程的执行时间有关。和它们的启动顺序无关。并且你无需自己在去写很多判断哪个线程是否执行完成,它里面会去帮你处理

CompletionService主要解决了 Future 阻塞的问题。
CompletionService主要应用场景: 同时执行多个Callable任务,并且需对任务的返回结果进行处理。若想优先处理先执行完的任务结果,使用它尤其方便。

5.3.1、 源码

CompletionService接口

package java.util.concurrent;
 
public interface CompletionService<V> {
    //提交线程任务
    Future<V> submit(Callable<V> task);
    //提交线程任务
    Future<V> submit(Runnable task, V result);
   //阻塞等待
    Future<V> take() throws InterruptedException;
   //非阻塞等待
    Future<V> poll();
   //带时间的非阻塞等待
    Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}

ExecutorCompletionService实现类

package java.util.concurrent;
 
public class ExecutorCompletionService<V> implements CompletionService<V> {
    private final Executor executor;//线程池类
    private final AbstractExecutorService aes;
    private final BlockingQueue<Future<V>> completionQueue;//存放线程执行结果的阻塞队列
 
    //内部封装的一个用来执线程的FutureTask
    private class QueueingFuture extends FutureTask<Void> {
        QueueingFuture(RunnableFuture<V> task) {
            super(task, null);
            this.task = task;
        }
        protected void done() { completionQueue.add(task); }//线程执行完成后调用此函数将结果放入阻塞队列
        private final Future<V> task;
    }
 
    private RunnableFuture<V> newTaskFor(Callable<V> task) {
        if (aes == null)
            return new FutureTask<V>(task);
        else
            return aes.newTaskFor(task);
    }
 
    private RunnableFuture<V> newTaskFor(Runnable task, V result) {
        if (aes == null)
            return new FutureTask<V>(task, result);
        else
            return aes.newTaskFor(task, result);
    }
 
     //构造函数,这里一般传入一个线程池对象executor的实现类
    public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();//默认的是链表阻塞队列
    }
 
    //构造函数,可以自己设定阻塞队列
    public ExecutorCompletionService(Executor executor,
                                     BlockingQueue<Future<V>> completionQueue) {
        if (executor == null || completionQueue == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = completionQueue;
    }
    //提交线程任务,其实最终还是executor去提交
    public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task);
        executor.execute(new QueueingFuture(f));
        return f;
    }
    //提交线程任务,其实最终还是executor去提交
    public Future<V> submit(Runnable task, V result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task, result);
        executor.execute(new QueueingFuture(f));
        return f;
    }
 
    public Future<V> take() throws InterruptedException {
        return completionQueue.take();
    }
 
    public Future<V> poll() {
        return completionQueue.poll();
    }
 
    public Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException {
        return completionQueue.poll(timeout, unit);
    }
 
}

从源码中可以知道。最终还是线程还是提交到Executor当中去运行,所以构造函数中需要Executor参数来实例化。而每次有线程执行完成后往阻塞队列添加一个Future。

5.3.2、对比

class HandleFuture<Integer> implements Callable<Integer> {
	
	private Integer num;
	
	public HandleFuture(Integer num) {
		this.num = num;
	}
 
	@Override
	public Integer call() throws Exception {
		Thread.sleep(3*100);
		System.out.println(Thread.currentThread().getName());
		return num;
	}
	
}

(1)Futrue

	public static void FutureTest() throws InterruptedException, ExecutionException {
		System.out.println("main Thread begin:");
		ExecutorService executor = Executors.newCachedThreadPool();
		List<Future<Integer>> result = new ArrayList<Future<Integer>>();
		for (int i = 0;i<10;i++) {
			Future<Integer> submit = executor.submit(new HandleFuture(i));
			result.add(submit);
		}
		executor.shutdown();
		for (int i = 0;i<10;i++) {//一个一个等待返回结果
			System.out.println("返回结果:"+result.get(i).get());
		}
		System.out.println("main Thread end:");
	}

执行结果:
java并发编程【二】Executor框架与线程池_第11张图片
从输出结果可以看出,我们只能一个一个阻塞的取出。这中间肯定会浪费一定的时间在等待上。如7返回了。但是前面1-6都没有返回。那么7就得等1-6输出才能输出。

(2)CompletionService

	public static void CompleTest() throws InterruptedException, ExecutionException {
		System.out.println("main Thread begin:");
		ExecutorService executor = Executors.newCachedThreadPool();
		// 构建完成服务
		CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executor);
		for (int i = 0;i<10;i++) {
			completionService.submit(new HandleFuture(i));
		}
		for (int i = 0;i<10;i++) {//一个一个等待返回结果
			System.out.println("返回结果:"+completionService.take().get());
		}
		System.out.println("main Thread end:");
	}

输出结果:
java并发编程【二】Executor框架与线程池_第12张图片
可以看出,结果的输出和线程的放入顺序无关系。每一个线程执行成功后,立刻就输出。

5.4、CompletableFuture

Future模式虽然好用,但也有一个问题,那就是将任务提交给线程后,调用线程并不知道这个任务什么时候执行完,如果执行调用get()方法或者isDone()方法判断,可能会进行不必要的等待,那么系统的吞吐量很难提高。

为了解决这个问题,JDK对Future模式又进行了加强,创建了一个CompletableFuture,它可以理解为Future模式的升级版本,它最大的作用是提供了一个回调机制,可以在任务完成后,自动回调一些后续的处理,这样,整个程序可以把“结果等待”完全给移除了。参考案例

CompletableFuture默认依靠fork/join框架启动新的线程实现异步与并发的,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调函数的方式处理返回结果,并且提供了转换和组合CompletableFuture的方法。

主要是为了解决Future模式的缺点:

  1. Future虽然可以实现异步获取线程的执行结果,但是Future没有提供通知机制,调用方无法得知Future什么时候执行完的问题。
  2. 想要获取Future的结果,要么使用阻塞,在future.get()的地方等待Future返回结果,这时会变成同步操作。要么使用isDone()方法进行轮询,又会耗费无谓的CPU 资源。
  3. 可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

(1)案例1

@Test
    public void test2() throws Exception {
        ForkJoinPool pool=new ForkJoinPool();
        // 创建异步执行任务:
        CompletableFuture<Double> cf = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread()+"job1 start,time->"+System.currentTimeMillis());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            if(true){
                throw new RuntimeException("test");
            }else{
                System.out.println(Thread.currentThread()+"job1 exit,time->"+System.currentTimeMillis());
                return 1.2;
            }
        },pool);


        //cf执行异常时,将抛出的异常作为入参传递给回调方法
        CompletableFuture<Double> cf2= cf.exceptionally((param)->{
             System.out.println(Thread.currentThread()+" start,time->"+System.currentTimeMillis());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            System.out.println("error stack trace->");
            param.printStackTrace();
            System.out.println(Thread.currentThread()+" exit,time->"+System.currentTimeMillis());
             return -1.1;
        });



        //cf正常执行时执行的逻辑,如果执行异常则不调用此逻辑
        CompletableFuture cf3=cf.thenAccept((param)->{
            System.out.println(Thread.currentThread()+"job2 start,time->"+System.currentTimeMillis());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            System.out.println("param->"+param);
            System.out.println(Thread.currentThread()+"job2 exit,time->"+System.currentTimeMillis());
        });


        System.out.println("main thread start,time->"+System.currentTimeMillis());
        //等待子任务执行完成,此处无论是job2和job3都可以实现job2退出,主线程才退出,如果是cf,则主线程不会等待job2执行完成自动退出了
        //cf2.get时,没有异常,但是依然有返回值,就是cf的返回值
        System.out.println("run result->"+cf2.get());
        System.out.println("main thread exit,time->"+System.currentTimeMillis());
    }

5.5、小结

java并发编程【二】Executor框架与线程池_第13张图片


六、常见问题

6.1、线程饥饿死锁

在线程池中,如果一个任务将另一个任务提交到同一个Executor,那么通常会引发死锁。第二个线程停留在工作队列中等待第一个提交的任务执行完成,但是第一个任务又无法执行完成,因为它在等待第二个任务执行完成。

java并发编程【二】Executor框架与线程池_第14张图片

使用 SingleThreadExecutor模拟

public class AboutThread {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    public static void main(String[] args) {
        AboutThread aboutThread = new AboutThread();
        aboutThread.threadDeadLock();
    }

    public void threadDeadLock(){
        Future<string> taskOne  = executorService.submit(new TaskOne());
        try {
            System.out.printf(taskOne.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    public class TaskOne implements Callable{

        @Override
        public Object call() throws Exception {
            Future<string> taskTow = executorService.submit(new TaskTwo());
            return "TaskOne" + taskTow.get();
        }
    }

    public class TaskTwo implements Callable{

        @Override
        public Object call() throws Exception {
            return "TaskTwo";
        }
    }
}

总结:

尽量避免在同一个线程池中处理两种不同类型的任务

参考资料:

1、 如何优雅的使用线程池
2、Java 中的线程池是如何实现的?
3、线程池源码分析-execute()方法
4、并发编程十五:线程池任务类型和ForkJoin实战详解
5、线程池拒绝策略
6、Java线程池
7、java并发编程:Executor、Executors、ExecutorService
8、Java并发编程:Callable、Future和FutureTask原理解析
9、Java并发编程与技术内幕:Callable、Future、FutureTask、CompletionService
10、线程池ForkJoinPool简介
11、Java多线程之CompletableFuture
12、【Java多线程】JUC之线程池(五) CompletionService 、CompletableFuture

你可能感兴趣的:(java基础,java)