Java并发编程(三)-- 线程池及其常用实现

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u011519624/article/details/79836228

Java里的线程池在平日的工作用的不算少,特别是有大量数据需要多线程处理的情形,那么大家在用之前是不是该刨根问底呢?
我们知道,线程池不光是Java里有涉及,各大开源框架、中间件、数据库等都会设计不同类型的线程池,比如Dubbo、Tomcat、MySQL等等等,因为使用线程池的好处如下:

  1. 通过重复利用已经创建的线程降低线程创建和销毁带来的性能损耗;
  2. 提高任务的响应速度,任务不需要等待线程创建就能执行,因为利用池中已有的线程;
  3. 增加了线程的可控性,线程如果无限创建,将严重消耗系统资源,那么可以使用线程池对线程统一分配和监控,使线程数量在健康的状态下。

线程池的实现原理

向线程池提交一个任务后,线程池的处理逻辑如下:
线程池

  1. 任务提交,执行execute方法后,检查当前运行的线程数据量是否达到核心线程池的大小,如果未达到,则创建新的线程去处理任务,否则进行第2步;
  2. 核心线程池满了后,任务将放置在同步队列中,等待核心线程的处理,即第4步。如果同步队列中存储的任务已经满了,将进行第3步;
  3. 同步队列满了后,则创建新的线程来处理任务,直到线程数量达到最大线程数量时,则进行第5步;
  4. 核心线程反复的从同步队列中拉取任务处理;
  5. 当前运行的线程数超过maximumPoolSize,任务将被拒绝,并执行拒绝策略(RejectedExecutionHandler)。

线程池的创建

我们可用通过ThreadPoolExecutor来创建定制化的线程池,如下:

new  ThreadPoolExecutor(corePoolSize,maximumPoolSize,KeepAliveTime,milliseconds,runnableTaskQueue,handler);

参数的含义如下:

  • 1、corePoolSize:核心线程数,任务进入线程池时,不管有无空闲的线程都会建立新的线程处理,直到线程数达到核心线程数。可通过方法直接使线程池建立核心线程数的线程(预热)。
  • 2、maximumPoolSize:最大线程数,线程池的任务队列满时,可继续创建线程数至最大线程数。任务队列无边界时,此参数无意义。
  • 3、keepAliveTime:除核心线程外的空闲线程保持存活时间,如果任务很多,并且任务执行的时间较短,则可以适当的调大线程的存活时间,提高线程的利用率。
  • 5、milliseconds:线程保持存活的时间单位,这里以毫秒为单位。
  • 6、runnableTaskQueue:任务队列,用来保存等待执行的任务的阻塞队列。可用以下阻塞队列实现:

    1)ArrayBlockingQueue
        数组结构的有界阻塞队列,FIFO特性
    2)LinkedBlockingQueue
        链表结构的阻塞队列,FIFO特性。
        Executors.newFixedThreadPool()(固定大小的线程池)使用了此队列。
    3)SynchronousQueue
        不存储元素的阻塞队列,每个插入操作必须等到另一个线程执行移除操作,否则插入操作一直处于阻塞状态。
        Executors.newCachedThreadPool()(缓存线程池)使用了此队列。
    4)PriorityBlockingQueue
        有优先级的无限阻塞队列。
    
  • 7、ThreadFactory:创建线程的工厂实例。

  • 8、RejectedExecutionHandler:拒绝执行策略,当线程池满了时,不能接收新的任务,而执行的拒绝策略,有以下几种策略:

    1)AbortPolicy:直接抛出异常。   
    2)CallerRunsPolicy:只用调用者的线程来执行任务。
    3)DiscardOldestPolicy:丢弃队列里最近的一个任务,并且执行当前任务。 
    4)DiscardPolicy:直接丢弃。
    5)自定义拒绝策略
    

常用实现

上面所说的线程池,Executor框架针对不同的用处,创建了三种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。

1、SingleThreadExecutor

创建单个线程的线程池,构造方法如下:

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

从构造方法的参数可以看出,核心线程数量和最大线程数量都设置为1,说明是单线程处理,同时也不存在线程数量大于核心线程数量的情况,所以keepAliveTime空闲线程存活时间不会生效。使用LinkedBlockingQueue链表阻塞队列,最大值是Integer.MAX_VALUE(2^31-1),近似无界,保证新任务都能够加入至同步队列中。除此以外还使用FinalizableDelegatedExecutorService类进行了包装。这个包装类的主要目的是为了屏蔽ThreadPoolExecutor中动态修改线程数量的功能,仅保留ExecutorService中提供的方法。虽然是单线程处理,一旦线程因为处理异常等原因终止的时候,ThreadPoolExecutor会自动创建一个新的线程继续进行工作。
SingleThreadExecutor适用于需要保证顺序地执行各个任务,并且确保在任何时间都不会有多线程处理的应用场景。缺点是当请求太多时,会导致工作队列存储的任务太多,导致内存问题。

2、FixedThreadPool

固定大小的线程池,构造方法如下:

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

从构造方法的参数可以看出,FixedThreadPool与SingleThreadExecutor最大的区别在于线程数的设置,其他参数基本一致。FixedThreadPool设置了固定大小的nThreads数量线程池,并且任务队列近似无界。
FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的场景,适用于负载比较重的服务器。缺点与SingleThreadExecutor一样,任务队列数量没有限制,在任务执行时间无限延长的这种极端情况下会造成内存问题。

3、CachedThreadPool

缓存的线程池,构造方法如下:

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

构造方法参数显示,核心线程数设置为0,最大线程数设置为Integer.MAX_VALUE,近似无限,表明所有任务都会进入至工作队列。工作队列使用SynchronousQueue实现,SynchronousQueue是一个只有1个元素的队列,入队的任务需要一直等待直到队列中的元素被移出。keepAliveTime设置为60秒,表示线程有60秒的空闲存活时间。
综上,CachedThreadPool的处理策略是提交的任务会立即分配一个线程进行执行,线程池中线程数量会随着任务数的变化自动扩张和缩减,在任务执行时间过长且请求过多时,会创建过多的线程而耗尽CPU资源。

java线程池中基于缓存和基于定长的两种线程池,当请求太多时分别是如何处理的?定长的用的是什么队列,如果队列也满了呢?交换进磁盘?基于缓存的线程池解决方法呢?

知道上面这些基础的线程池知识后,下面这道面试题是不是迎刃而解了呢?
回想构造方法:
FixedThreadPool:ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
基于定长(FixedThreadPool)的线程池使用的是近似无界的LinkedBlockingQueue链表阻塞队列(容量是Integer.MAX_VALUE)作为工作队列,所以请求太多时会堆积在工作队列中,任务过多时会造成内存问题,甚至OOM问题。如果说内存够大,工作队列里的任务数量达到Integer.MAX_VALUE都未出现内存问题,则会采用默认的拒绝策略抛出异常,我们可以对异常的任务做持久化后续处理,以防任务丢失。
回想构造方法:
CachedThreadPool:ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>())
基于缓存的线程池,从构造函数可看出,corePoolSize是0,maximumPoolSize是Integer.MAX_VALUE,说明最大线程数是近似无界的,且使用容量为1的SynchronousQueue作为工作队列,意味着,如果主线程提交任务的速度大于线程池线程处理任务的速度时,CachedThreadPool 就会不断创建新的线程去处理任务,在最恶劣的情况下,CachedThreadPool 会因为创建过多的线程而耗尽了CPU和内存的资源。就比如如上的问题下,请求太多时,基于缓存的线程池可能会耗尽了CPU和内存资源。该怎么解决这个问题呢?对线程池监控,线程数量达到预警值时,切换至任务磁盘持久化,磁盘里的任务另起线程异步处理。

展开阅读全文

没有更多推荐了,返回首页