2、深入理解线程池

为何使用线程池

    在并发情况下,线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁的创建线程和销毁线程需要时间,从而大大的降低系统的效率

线程池讲解

    ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor之间的关系

  • Executor是一个顶层接口,它里面只声明了一个方法execute(Runnable)
  • ExecutorService接口集成了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny、shutDown
  • 抽象类AbstractExecutorService实现了ExecutorService接口,
  • ThreadPoolExecutor继承了AbstractExecutorService抽象类

    一、ThreadPoolExecutor类

        1、java.util.concurrent.ThreadPoolExecutor 类是线程池中最核心的一个类

        2、ThreadPoolExecutor构造函数

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

      构造函数各个参数含义:

  • corePoolSize:核心池的大小
  • maximumPoolSize:线程池最大线程数,它表示在线程池中能创建多少个线程
  • keepAliveTime:表示线程在没有任务执行时最多保持多久时间会终止
  • 参数keepAliveTime的时间单位,
TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒
  • workQueue:一个阻塞队列,用来存储等待执行的任务

          阻塞队列有一下几种:

ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue

ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与  BlockingQueue有关

  • threadFactory:线程工厂,主要用来创建线程
  • handler:表示当拒绝处理任务时的策略
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

     3、ThreadPoolExecutor类有几个重要的方法

  • execute():向线程池提交一个任务,交由线程池去处理
  • submit():向线程池提交一个任务,它能够返回任务执行的结果,内部还是调用了execute方法,利用管理future来获取线程的执行结果
  • shutdown、shutdownNow:关闭线程

线程池具体实现原理

    1.线程池状态

    ThreadPoolExecutor类中定义了 一些变量

volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

    runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;

    当创建线程池后,初始化时,线程池处于running状态;

    如果调用shutdown方法,线程池处于shutdown状态,此时线程池不能接受新任务,它会等待所有线程执行完毕;

    如果调用shutdownNow方法,线程池处于stop状态,此时线程池不能接收新的任务,并且会去终止正在执行的任务;

   当线程池处于shutdown、stop状态,并且所有线程、已经销毁,任务缓存队列已经清空或执行结束,线程池被设置为terminated状态

    2.任务的执行

    ThreadPoolExecutor类中重要的成员变量

private final BlockingQueue<Runnable> workQueue;              //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock();   //线程池的主要状态锁,对线程池状态(比如线程池大小
                                                              //、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>();  //用来存放工作集
private volatile long  keepAliveTime;    //线程存货时间   
private volatile boolean allowCoreThreadTimeOut;   //是否允许为核心线程设置存活时间
private volatile int   corePoolSize;     //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int   maximumPoolSize;   //线程池最大能容忍的线程数
private volatile int   poolSize;       //线程池中当前的线程数
private volatile RejectedExecutionHandler handler; //任务拒绝策略
private volatile ThreadFactory threadFactory;   //线程工厂,用来创建线程
private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount;   //用来记录已经执行完毕的任务个数

    corePoolSize就是线程池大小,maxmumPoolSize就是线程池的一种补救措施,当任务量过大的时候,线程池能创建的maxmumPoolSize最大线程数。

    largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目。

    任务提交给线程池之后的处理策略:

  • 当线程池中的线程数小于corePoolSize,则每接收一个任务就会创建一个线程去处理这个任务
  • 当线程池中的线程数大于等于corePoolSize,则接收的任务,会将其放入 任务缓存队列中,如果添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(任务缓存队列已满),则会创建新的线程去执行此任务
  • 当线程池中的线程数达到了maxmunPoolSize,则会采取任务拒绝策略去处理此任务
  • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止

    3.线程池中的线程初始化

    默认情况下,线程池创建后没有线程,需要提交任务后才会创建线程。

    如果需要线程池创建后就立即创建线程,可以调用prestartCoreThread、prestartAllCoreThread方法创建线程

    4.任务缓存队列及排队策略

    任务缓存队列,即workQueue,用来存放等待执行的任务

    通常采用的三种类型

  • ArrayBlockingQueue 基于数组的先进先出队列,此队列创建时必须指定大小
  • LinkedBlockingQueue 基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE
  • synchronousQueue 这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务

 5.任务拒绝策略

    当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

如何合理配置线程池的大小

  1、多少线程合适

        线程池中的最大线程数量应该被限制,才不会导致系统资源耗尽。这些系统资源被包括了内存(堆栈)、打开的文件句柄,打开的TCP连接、打开的数据库连接以及其它有限资源。相反的,如果线程执行的是CPU密集型任务而不是IO密集型任务,服务器的物理内核数就应该被视为是有限的资源,这样创建的线程数就不应该超过系统的内核数。        系统应创建多少线程取决于这个应用执行的任务。开发人员应使用现实的请求来对系统进行负载测试,测试不同的线程池大小配置对系统的影响。每次测试都增加线程池的大小,直到系统达到崩溃的临界点。这个方法使你可以发现线程池线程数量的上限。超过这个上限,系统的资源将耗尽。在某些情况下,可以谨慎地增加系统的资源,例如分配更多的RAM空间给JVM,或者调整操作系统使其支持同时打开更多的文件句柄。然而,在某些情况下创建的线程数量会达到我们测试出的理论上限,这非常值得我们注意。稍后还会看到这方面的内容

        利特尔法则

        

    

利特尔法则解释了这三种变量的关系:L—系统里的请求数量、λ—请求到达的速率和W—每个请求的处理时间。例如,如果每秒10个请求到达,处理一个请求需要1秒,那么系统在每个时刻都有10个请求在处理。如果处理每个请求的时间翻倍,那么系统每时刻需要处理的请求数也翻倍为20,因此需要20个线程。

任务的执行时间对于系统中正在处理的请求数量有着很大的影响,一些后端资源的迟延,例如数据库,通常会使得请求的处理时间延长,从而导致线程池中的线程被迅速用尽。因此,理论上测出的线程数上限对于这种情况就不是很合适,这个上限值还应该考虑到线程的执行时间,并结合理论上的上限值。

例如,假设JVM最多能同时处理的请求数为1000。如果我们预计每个请求需要耗费的时间不超过30秒,那么,在最坏的情况下我们每秒能同时处理的请求数不会超过33 ⅓个。但是,如果一切都很顺利,每个请求只需使用500ms就可以完成,那么通过1000个线程应用每秒就可以处理2000个请求。当系统突然出现短暂的任务执行迟延的问题时,通过使用一个队列来减缓这一问题是可行的。

   2、为什么线程数配置不当会带来麻烦?

     如果线程池的线程数量过少,我们就无法充分利用系统资源,这使得用户需要花费很长时间来等待请求的响应。但是,如果允许创建过多的线程,系统的资源又会被耗尽,这会对系统造成更大的破坏。

不仅仅是本地的资源被耗尽,其它一些应用也会受到影响。例如,许多应用都使用同一个后端数据库进行查询等操作。数据库有并发连接数量的限制。如果一个应用不加限制地占用了所有数据库连接,其它获取数据库连接的应用都将被阻塞。这将导致许多应用运行中断。

    更糟的是,资源耗尽还会引发一些连锁故障。设想这样一个场景,一个应用有许多个实例,这些实例都运行在一个负载均衡器之后。如果一个实例因为过多的请求而占用了过多内存,JVM就需要花更多的时间进行垃圾收集工作,那么JVM处理请求的时间就减少了。这样一来,这个应用实例处理请求的能力降低了,系统中的其它实例就必须处理更多的请求。其它的实例也会因为请求数过多以及线程池大小没有限制的原因产生资源枯竭等问题。这些实例用尽了内存资源,导致虚拟机进行频繁地内存收集操作。这样的恶性循环会在这些实例中产生,直到整个系统奔溃。

    使用多个线程池还有一个好处,就是它能帮助避免出现死锁问题。如果每个空闲线程都因为一个尚未处理完毕的请求阻塞,就会发生死锁,没有一个线程可以继续往下执行。如果使用多个线程池,理解好每个线程池应负责的工作,那么死锁的问题就能在一定程度上避免。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值