Skip to content

Latest commit

 

History

History
320 lines (282 loc) · 15.3 KB

02_线程池的问题.md

File metadata and controls

320 lines (282 loc) · 15.3 KB

并发包的问题

目录


1. 什么是线程池?线程池的工作原理?
2. 使用线程池的好处?
3. 线程池的注意事项。
4. 简单线程池的设计。

     一个线程池管理了一组工作线程,同时它还包括了一个用于放置等待执行 任务的任务队列(阻塞队列)。
     默认情况下,在创建了线程池后,线程池中的线程数为 0。当任务提交给 线程池之后的处理策略如下:
     1. 如果此时线程池中的数量小于 corePoolSize(核心池的大小),即 使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务(也 就是每来一个任务,就要创建一个线程来执行任务)。
     2. 如果此时线程池中的数量大于等于 corePoolSize,但是缓冲队列 workQueue 未满,那么任务被放入缓冲队列,则该任务会等待空闲线程将其 取出去执行。
     3. 如果此时线程池中的数量大于等于 corePoolSize, 缓冲队列 workQueue 满,并且线程池中的数量小于 maximumPoolSize(线程池 最大线程数),建新的线程来处理被添加的任务。
     4. 如果此时线程池中的数量大于等于 corePoolSize , 缓冲队列 workQueue 满,并且线程池中的数量等于 maximumPoolSize,那么通 过 RejectedExecutionHandler 所指定的策略(任务拒绝策略)来处理此任务。 也就是处理任务的优先级为: 核 心 线 程 corePoolSize 、 任 务 队 列 workQueue、最大线程 maximumPoolSize,如果三者都满了,使用 handler 处理被拒绝的任务。
     5. 特别注意,在 corePoolSize 和 maximumPoolSize 之间的线程 数会被自动释放。当线程池中线程数量大于 corePoolSize 时,如果某线程 空闲时间超过 keepAliveTime,线程将被终止,直至线程池中的线程数目不 大于 corePoolSize。这样,线程池可以动态的调整池中的线程数。
02_1_1

使用线程池的好处:

     1.通过重复利用已创建的线程,减少在创建和销毁线程上所花的时间以及系 统资源的开销。
     2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就可以立即 执行。
     3.提高线程的可管理性。使用线程池可以对线程进行统一的分配和监控。
     4.如果不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内 存。

对于原理,有几个接口和类值得我们关注:

     Executor 接口
     Executors 类
     ExecutorService 接口
     AbstractExecutorService 抽象类
     ThreadPoolExecutor 类

     Executor 是一个顶层接口,在它里面只声明了一个方法 execute(Runnable),返回 值为 void,参数为 Runnable 类型,从字面意思可以理解,就是用来执行传进去的任务的;
     然后 ExecutorService 接口继承了 Executor 接口,并声明了一些方法:submit、 invokeAll、invokeAny 以及 shutDown 等;
     抽象类 AbstractExecutorService 实现了 ExecutorService 接口,基本实现了 ExecutorService 中声明的所有方法;
     然后 ThreadPoolExecutor 继承了类 AbstractExecutorService。 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
     创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代 Timer 类。
02_1_2

Executor 接口

public interface Executor { void execute(Runnable command); }      Executor 接口只有一个方法 execute(),并且需要传入一个 Runnable 类型的参数。那么它的作用自然是 具体的执行参数传入的任务。

ExecutorService 接口
public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
    throws InterruptedException;
    ......
}
Executors 类:

它主要用来创建线程池。

Executors.newSingleThreadExecutor(); //创建容量为 1 的缓冲池 Executors.newFixedThreadPool(int n); //创建固定容量大小的缓冲池 Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池最大容量为Integer.MAX_VALUE(无界线程池)

下面是这三个静态方法的具体实现:

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

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

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

     1. newSingleThreadExecutor
     创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串 行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。 此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
     2.newFixedThreadPool
     创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的 最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结 束,那么线程池会补充一个新线程。
     3. newCachedThreadPool
     创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就 会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添 加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系 统(或者说 JVM)能够创建的最大线程大小。

ThreadPoolExecutor 类

02_1_3

在 ThreadPoolExecutor 类中提供了四个构造方法:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
    BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

     不过在 java doc 中,并不提倡我们直接使用 ThreadPoolExecutor 创 建线程池,而是使用 Executors 类中提供的几个静态方法来创建线程池。

下面解释一下构造器中各个参数的含义:
     corePoolSize:核心池的大小。默认情况下,在创建了线程池后,线程池中 的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中 的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。
     maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多 少个线程。
     keepAliveTime:默认情况下,只有当线程池中的线程数大于 corePoolSize 时, keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize, 即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。
     unit:参数 keepAliveTime 的时间单位。      workQueue:一个阻塞队列,任务缓存队列,即 workQueue,它用来存 放等待执行的任务。
     workQueue 的类型为 BlockingQueue,通常可以取下面三种类型:
     1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
     2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列 大小,则默认为 Integer.MAX_VALUE;
     3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接 新建一个线程来执行新来的任务。在某次添加元素后必须等待其他线程取走后才能继续添 加。
     ThreadFactory:线程工厂,主要用来创建线程。
     handler:表示当拒绝处理任务时的策略,有以下四种取值:
     1.AbortPolicy:直接抛出异常(默认的)
     2.DiscardPolicy:直接丢弃任务
     3.DiscardOldestPolicy:丢弃队列中最旧(队头)的任务,并执行当前 任务
     4.CallerRunsPolicy:不用线程池中的线程执行,用调用者所在线程执 行。
     在 ThreadPoolExecutor 类中有几个非常重要的方法:
     execute()
     submit()
     shutdown()
     shutdownNow()

execute 和 submit 区别:

     submit 有返回值,execute 没有返回值。 所以说可以根据任务有无返回 值选择对应的方法。
     *** submit 方便异常的处理。 如果任务可能会抛出异常,而且希望外面的调 用者能够感知这些异常,那么就需要调用 submit 方法,通过捕获 Future.get 抛出的异常。***

shutdown()和 shutdownNow()的区别:

     shutdown()和 shutdownNow()是用来关闭线程池的。
     shutdown 方法:此方法执行后不得向线程池再提交任务,如果有空闲线 程则销毁空闲线程,等待所有正在执行的任务及位于阻塞队列中的任务执行结 束,然后销毁所有线程。
     shutdownNow 方法:此方法执行后不得向线程池再提交任务,如果有空 闲线程则销毁空闲线程,取消所有位于阻塞队列中的任务,并将其放入 List容器,作为返回值。取消正在执行的线程(实际上仅仅是设 置正在执行线程的中断标志位,调用线程的 interrupt 方法来中断线程)。

线程池的注意事项

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。
     (1)线程池的大小。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及 应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与 CPU 数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可 采用自适应算法来动态调整线程池的大小,以提高 CPU 的有效利用率和系统的整体性能。
     (2)并发错误。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避 免死锁现象的发生。
     (3)线程泄漏。这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中 就会发生线程泄漏现象。

简单线程池的设计

一个典型的线程池,应该包括如下几个部分:
1、线程池管理器(ThreadPool),用于启动、停用,管理线程池
2、工作线程(WorkThread),线程池中的线程
3、请求接口(WorkRequest),创建请求对象,以供工作线程调度任务的执行
4、请求队列(RequestQueue),用于存放和提取请求
5、结果队列(ResultQueue),用于存储请求执行后返回的结果

     线程池管理器,通过添加请求的方法(putRequest)向请求队列(RequestQueue) 添加请求,这些请求事先需要实现请求接口,即传递工作函数、参数、结果处理函数、以 及异常处理函数。之后初始化一定数量的工作线程,这些线程通过轮询的方式不断查看请 求队列(RequestQueue),只要有请求存在,则会提取出请求,进行执行。然后,线程池 管理器调用方法(poll)查看结果队列(resultQueue)是否有值,如果有值,则取出,调 用结果处理函数执行。通过以上讲述,不难发现,这个系统的核心资源在于请求队列和结 果队列,工作线程通过轮询 requestQueue 获得任务,主线程通过查看结果队列,获得执 行结果。因此,对这个队列的设计,要实现线程同步,以及一定阻塞和超时机制的设计, 以防止因为不断轮询而导致的过多 cpu 开销。
02_1_4

问:

     如何合理的配置 java 线程池?如 CPU 密集型的任务,基本线程池 应该配置多大?IO 密集型的任务,基本线程池应该配置多大?用有界 队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取 最好的吞吐量?

答:

     1)配置线程池时,CPU 密集型任务可以少配置线程数,大概和机器的 cpu 核数相 当,可以使得每个线程都在执行任务。
     2)IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽 可能多的线程,2*cpu 核数。
     3)有界队列和无界队列的配置需区分业务场景,一般情况下配置有界队列,在一些 可能会有爆发性增长的情况下使用无界队列。
     4)任务非常多时,使用非阻塞队列使用 CAS 操作替代锁可以获得好的吞吐量。 synchronousQueue 吞吐率最高。


搬运工信息