线程池解决的问题:频繁创建线程所造成的系统开销;统筹管理系统内存和CPU;统一管理资源。
    **
    线程池复用的原理

    1. 实现线程复用的逻辑主要在一个不停循环的 while 循环体中。
    2. 通过取 Worker firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务。
    3. 直接调用 task run 方法来执行具体的任务(而不是新建线程)。

    0)如何创建线程池?

    Executors的创建线程池的方法,创建出来的线程池都实现了ExecutorService接口。常用方法有以下几个:

    a、newFiexedThreadPool(int Threads):创建固定数目线程的线程池。 核心线程数 = 最大线程数
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    通过往构造函数中传参,创建了一个核心线程数和最大线程数相等的线程池,它们的数量也就是我们传入的参数,这里的重点是使用的队列是容量没有上限的 LinkedBlockingQueue,如果我们对任务的处理速度比较慢,那么随着请求的增多,队列中堆积的任务也会越来越多,最终大量堆积的任务会占用大量内存,并发生 OOM ,也就是OutOfMemoryError,这几乎会影响到整个程序,会造成很严重的后果。
    
    b、newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    这里的 CachedThreadPool使用的是 SynchronousQueue,SynchronousQueue 本身并不存储任务,而是对任务直接进行转发,这本身是没有问题的,但你会发现构造函数的第二个参数被设置成了 Integer.MAX_VALUE,这个参数的含义是最大线程数,所以由于 CachedThreadPool 并不限制线程的数量,当任务数量特别多的时候,就可能会导致创建非常多的线程,最终超过了操作系统的上限而无法创建新线程,或者导致内存不足。
    
    c、newSingleThreadExecutor()创建一个单线程化的Executor。
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    newSingleThreadExecutor 和 newFixedThreadPool 的原理是一样的,只不过把核心线程数和最大线程数都直接设置成了 1,但是任务队列仍是无界的 LinkedBlockingQueue,所以也会导致同样的问题,也就是当任务堆积时,可能会占用大量的内存并导致 OOM。
    
    
    d、newScheduledThreadPool(int corePoolSize)创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
    public static ScheduledExecutorService public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
    }
    采用的任务队列是 DelayedWorkQueue,这是一个延迟队列,同时也是一个无界队列,所以和 LinkedBlockingQueue 一样,如果队列中存放过多的任务,就可能导致 OOM。
    

    image.png
    ThreadPoolExecutor->自己指定线程池参数、配置队列类型。

    private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
            60L, TimeUnit.SECONDS,
            new ArrayBlockingQueue(10));
    

    Java线程池的基本配置

        1、corePoolSize:核心线程数
            * 核心线程会一直存活,及时没有任务需要执行
            * 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
            * 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
    
        2、queueCapacity:任务队列容量(阻塞队列)
            * 当核心线程数达到最大时,新任务会放在队列中排队等待执行
    
        3、maxPoolSize:最大线程数
            * 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
            * 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
    
        4、 keepAliveTime:空闲线程的存活时间
            * 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
            * 如果allowCoreThreadTimeout=true,则会直到线程数量=0
    
        5、allowCoreThreadTimeout:允许核心线程超时
    
        6、rejectedExecutionHandler:任务拒绝处理器
            * 两种情况会拒绝处理任务:
                - 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
                - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
            * 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
            * ThreadPoolExecutor类有几个内部实现类来处理这类情况:
                - AbortPolicy 丢弃任务,抛运行时异常
                - CallerRunsPolicy 交给提交任务的线程执行任务
                - DiscardPolicy 丢弃当前任务
                - DiscardOldestPolicy 从任务队列中踢出最先进入队列的任务
            * 实现RejectedExecutionHandler接口,可自定义处理器
    

    1)线程池如何运转?
    image.jpeg

    线程池按以下行为执行任务
        1. 当线程数小于核心线程数时,创建线程。
        2. 当线程数等于核心线程数,且任务队列未满时,将任务放入任务队列。
        3. 当线程数等于核心线程数,且任务队列已满
            - 若线程数小于最大线程数,创建线程
            - 若线程数等于最大线程数,执行拒绝策略
    

    2)如何合理配置线程池?

    任务的性质:CPU 密集型任务,IO 密集型任务和混合型任务。
    CPU 密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的线程数,实际上并不会起到很好的效果。
    我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。
    
    耗时 IO 型,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。对于这种任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。
    
    线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)
    线程的平均工作时间所占比例越高,就需要越少的线程;
    线程的平均等待时间所占比例越高,就需要越多的线程;
    

    3)多线程提交任务execute()方法和submit()方法的区别

    1.对返回值的处理不同
    execute方法不关心返回值。
    submit方法有返回值,Future。
    
    2.对异常的处理不同
    excute方法会抛出异常。
    sumbit方法不会抛出异常。除非你调用Future.get()。
    

    4)如何正确的关闭线程池?

    void shutdown();
    shutdown(),它可以安全地关闭一个线程池,调用 shutdown() 方法之后线程池并不是立刻就被关闭,因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。但这并不代表 shutdown() 操作是没有任何效果的,调用 shutdown() 方法后如果还有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务。
    
    boolean isShutdown();
     isShutdown(),它可以返回 true 或者 false 来判断线程池是否已经开始了关闭工作,也就是是否执行了 shutdown 或者 shutdownNow 方法。这里需要注意,如果调用 isShutdown() 方法的返回的结果为 true 并不代表线程池此时已经彻底关闭了,这仅仅代表线程池开始了关闭的流程,也就是说,此时可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务。
    
    boolean isTerminated();
    isTerminated(),这个方法可以检测线程池是否真正“终结”了,这不仅代表线程池已关闭,同时代表线程池中的所有任务都已经都执行完毕了,因为我们刚才说过,调用 shutdown 方法之后,线程池会继续执行里面未完成的任务,不仅包括线程正在执行的任务,还包括正在任务队列中等待的任务。直到所有任务都执行完毕了,调用 isTerminated() 方法才会返回 true,这表示线程池已关闭并且线程池内部是空的,所有剩余的任务都执行完毕了。
    
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
    awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回 true,否则超时返回 fasle。
    我们则可以根据 awaitTermination() 返回的布尔值来判断下一步应该执行的操作。
    
    List<Runnable> shutdownNow();
    在执行 shutdownNow 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作,例如记录在案并在后期重试。
    

    5)FutureTask 获取异步结果

    在 Executors 框架体系中,FutureTask 用来表示可获取结果的异步任务。FutureTask 实现了 Future 接口,FutureTask 提供了启动和取消异步任务,查询异步任务是否计算结束以及获取最终的异步任务的结果的一些常用的方法。通过get()方法来获取异步任务的结果,但是会阻塞当前线程直至异步任务执行结束。一旦任务执行结束,任务不能重新启动或取消,除非调用runAndReset()方法。

    get 方法

    当 FutureTask 处于未启动或已启动状态时,执行 FutureTask.get()方法将导致调用线程阻塞。如果 FutureTask 处于已完成状态,调用 FutureTask.get()方法将导致调用线程立即返回结果或者抛出异常

    cancel 方法

    当 FutureTask 处于未启动状态时,执行 FutureTask.cancel()方法将此任务永远不会执行;
    当 FutureTask 处于已启动状态时,执行 FutureTask.cancel(true)方法将以中断线程的方式来阻止任务继续进行,如果执行 FutureTask.cancel(false)将不会对正在执行任务的线程有任何影响;
    当FutureTask处于已完成状态时,执行 FutureTask.cancel(…)方法将返回 false。
    image.jpeg