Java 进阶——多线程优化之线程池 ThreadPoolExecutor的使用(三)

版权声明:本文为CrazyMo_原创,转载请在显著位置注明原文链接 https://blog.csdn.net/CrazyMo_/article/details/80679888

引言

前面花了很多时间把线程池的核心容器和主要核心流程源码大概的分析了一遍,如果有认真看了的话相信,一定对于线程池有了较深的理解,ThreadPoolExecutor是线程池框架的一个核心类,通过对ThreadPoolExecutor的分析,可以知道其对资源进行了复用,并非无限制的创建线程,可以有效的减少线程创建和切换的开销,使用起来也不在话下,这篇就简单应用下线程池。

一、线程池的意义

合理利用线程池不仅能让我们的编码风格更规范更优雅,而且还能提高我们的应用性能,提高用户体验。具体体现在:

  • 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  • 提供高效的定时执行、定期执行、单线程、并发数控制等功能

二、四种系统线程池的简单使用

通过Executor框架的工厂类Executors,可以通过Executors的静态方法来创建四种类型的线程池,一般来说采取默认的饱和策略就可以了,如果需要传入自定义的饱和策略,可以调用对应的重载构造方法通过ThreadFactory参数传入。本质上说就是不同的类型采用了不同的阻塞队列,线程池的使用很简单,主要通过四步:

  • 通过Executors创建对应的线程池
  • 创建自己的工作线程Runnable或者Callable
  • 通过sumbmit或者execute提交
  • 根据需要关闭线程池

1、通过Executors.newFixedThreadPool() 创建可重用固定线程数的线程池

初始化一个指定线程数的线程池,其中核心线程池大小等于线程池大小,采用LinkedBlockingQueue且容量为Integer。MAX_VALUE,实际现场数量永远维持在mThreads,因此核心线程池大小和线程池大小相当于是无效的,虽然keepAliveTime为0,但当线程池没有可执行任务时,也不会释放线程即keepAliveTime无效。

	/**
	* corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。当线程池中的线程数大于corePoolSize时,
	* keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。keepAliveTime设置为0L,意味着多余的空闲线程会被立即终止。
    */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());//容量为Integer.MAX_VALUE,太奢侈了吧
    }
这里写代码片

2、通过Executors.newSingleThreadExecutor() 单个worker线程的线程池

初始化的线程池中有且只有一个工作线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行,其实使用装饰模式增强了ScheduledExecutorService的功能,不仅确保只有一个线程顺序执行任务,也保证线程意外终止后会重新创建一个线程继续执行任务。

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

3、通过Executors.newCachedThreadPool() 根据需要创建新线程的线程池

内部采用SynchronousQueue存储等待的任务,这个阻塞队列不存储工作线程,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程,比较适合处理执行时间比较小的任务

 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());//不存储的阻塞队列
    }

CachedThreadPool的corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是“无界“”的。keepAliveTime设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。FixedThreadPool和SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列。CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。

4、通过Executors.newScheduledThreadPool() 延迟运行任务或者定期执行任务的线程池 (调度池)

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,内部使用DelayQueue作为容器。它主要用来在给定的延迟之后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor的功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。一般Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。在实际的业务场景中可以使用该线程池定期的同步数据。

public class Executors {
		public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
		        return new ScheduledThreadPoolExecutor(corePoolSize);
	    }
	    ...
   }

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {
	public ScheduledThreadPoolExecutor(int corePoolSize) {
	   super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
	}
    ...
}

调度池内部使用的DelayQueue是一个无界队列,所以ThreadPoolExecutor的maximumPoolSize在ScheduledThreadPoolExecutor中没有什么意义(设置maximumPoolSize的大小没有什么效果)。

三、自定义线程池

在自定义线程池之前再简单总结下线程的创建的一般规则: ThreadPoolExecutor对象初始化时,不创建任何执行线程,当有新任务进来时,才会创建执行线程。构造ThreadPoolExecutor对象时,需要配置该对象的核心线程池大小和最大线程池大小

  • 当目前执行线程的总数小于核心线程大小时,所有新加入的任务,都在新线程中处理。
  • 当目前执行线程的总数大于或等于核心线程时,所有新加入的任务,都放入任务缓存队列中。
  • 当目前执行线程的总数大于或等于核心线程,并且缓存队列已满,同时此时线程总数小于线程池的最大大小,那么创建新线程,加入线程池中,协助处理新的任务。
  • 当所有线程都在执行,线程池大小已经达到上限,并且缓存队列已满时,就rejectHandler拒绝新的任务。

为了更加明确线程池的运行规则和规避耗尽资源的风险,尽量不要用Executors 去创建线程池。

    static int NUMBER_OF_CORES=Runtime.getRuntime().availableProcessors();
    static int KEEP_ALIVE_TIME=1;
    static TimeUnit KEEP_ALIVE__UNIT=TimeUnit.SECONDS;
    static BlockingQueue<Runnable> taskQueue=new LinkedBlockingDeque<>();
    ExecutorService mExecutorService=new ThreadPoolExecutor(NUMBER_OF_CORES,NUMBER_OF_CORES*2,KEEP_ALIVE_TIME,KEEP_ALIVE__UNIT,
            taskQueue,new BackgroundThreadFactory(),new DefaultRejectExecutionHandler())

四、线程池的合理配置

要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。

  • 任务的性质——CPU密集型任务、IO密集型任务和混合型任务。
  • 任务的优先级——高、中和低。
  • 任务的执行时间——长、中和短。
  • 任务的依赖性——是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过
Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。

注意:如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。有一次,我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然,我们的系统所有的任务是用单独的服务器部署的,我们使用不同规模的线程池完成不同类型的任务,但是出现这样问题时也会影响到其他任务。

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/80679888