如何优雅的使用线程池

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

线程池的基本思想还是一种对象池的思想,开辟内存空间,事先创建好线程,这些创建好的线程执行调度由池管理器来统一管理。当有线程任务时,从池中取一个线程来执行线程任务,线程执行完任务后,将线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

博文回顾:不聊接腿的事,就聊Java多线程编程

    本文主要从以下几个方面阐述线程池。

    一、线程池的优点

    使用线程池的好处很多,归纳如下:

    1.减少资源的开销 

    2.提高响应速度 ,每次请求到来时,由于线程的创建已经完成,故可以直接执行任务,因此提高了响应速度。

    3.提高线程的可管理性 ,线程是一种稀缺资源,若不加以限制,不仅会占用大量资源,而且会影响系统的稳定性。因此,线程池可以对线程的创建与停止、线程数量等等因素加以控制,使得线程在一种可控的范围内运行,不仅能保证系统稳定运行,而且方便性能调优。

    二、线程池的原理

   线程池的基本思想还是一种对象池的思想,开辟内存空间,事先创建好线程,这些创建好的线程执行调度由池管理器来统一管理。当有线程任务时,从池中取一个线程来执行线程任务,线程执行完任务后,将线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

    三、线程池的实现方式

    线程池在Java 中又是如何实现的呢?

    在 JDK 1.5 之后推出了相关的 api,常见的创建线程池方式有以下几种:

  • Executors.newCachedThreadPool():无限线程池。

  • Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。

  • Executors.newSingleThreadExecutor():创建单个线程的线程池。

  • Executors.newScheduledThreadPool():创建周期性执行某个任务的线程池 。

   具体demo见:创建线程池的4种方式

   然而,早期的jdk中创建的方式,在阿里代码规范中给出红色警告:    

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:1)FixedThreadPool和SingleThreadPool:  允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。2)CachedThreadPool:  允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

    ThreadPoolExecutor又是何许人也?

    研究下它的族谱:

ScheduledThreadPoolExecutor继承ThreadPoolExecutor类和实现ScheduledExecutorService接口,主要实现任务的计划执行和周期执行,这里介绍线程池以ThreadPoolExecutor为主。

 可以看到ThreadPoolExecutor的四个构造函数,将参数最全的一个构造函数介绍下:    

    public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler) {        if (corePoolSize < 0 ||            maximumPoolSize <= 0 ||            maximumPoolSize < corePoolSize ||            keepAliveTime < 0)            throw new IllegalArgumentException();        if (workQueue == null || threadFactory == null || handler == null)            throw new NullPointerException();        this.acc = System.getSecurityManager() == null ?                null :                AccessController.getContext();        this.corePoolSize = corePoolSize;        this.maximumPoolSize = maximumPoolSize;        this.workQueue = workQueue;        this.keepAliveTime = unit.toNanos(keepAliveTime);        this.threadFactory = threadFactory;        this.handler = handler;    }

该类构造方法参数列表如下:

int corePoolSize

核心线程:线程池新建线程的时候,如果当前线程总数小于 corePoolSize ,则新建的是核心线程;如果超过corePoolSize,则新建的是非核心线程。核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。如果指定ThreadPoolExecutor的 allowCoreThreadTimeOut 这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间( keepAliveTime),就会被销毁掉。

int maximumPoolSize

该线程池中线程总数的最大值,线程总数计算公式 = 核心线程数 + 非核心线程数。

long keepAliveTime, TimeUnit unit

 该线程池中非核心线程闲置超时时长

注意:一个非核心线程,如果不干活(闲置状态)的时长,超过这个参数所设定的时长,就会被销毁掉。但是,如果设置了  allowCoreThreadTimeOut = true,则会作用于核心线程。

BlockingQueue<Runnable> workQueue

该线程池中的任务队列:维护着等待执行的Runnable对象。当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。

ThreadFactory threadFactory

创建线程的方式,这是一个接口,new它的时候需要实现他的Thread newThread(Runnable r)方法。

RejectedExecutionHandler handler 

 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。

四、线程池的工作顺序

corePoolSize -> 任务队列-> maximumPoolSize -> 拒绝策略

拿装修公司的比喻:某装修公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。

线程池就是程序中的“装修公司”,代劳各种脏活累活。上面的过程对应到线程池上:

  int corePoolSize:正式工数量

  int maximumPoolSize:工人数量上限,包括正式工和临时工

  long keepAliveTime, TimeUnit unit: 临时工游手好闲的最长时间,超过此时间将被解雇

  BlockingQueue<Runnable> workQueue:排期队列

  ThreadFactory threadFactory:招人渠道

  RejectedExecutionHandler handler :拒单方式

五、任务提交方式

线程池任务提交方式有如下三种:

1.Future<T> submit(Callable<T> task) 

2.void execute(Runnable command)

3.Future<?> submit(Runnable task) 

六、正确使用线程池

正确使用线程池需注意:

1.避免使用无界队列

不要使用Executors.newXXXThreadPool()快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度。

2.明确拒绝任务时的行为

任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,接口定义如下:

public interface RejectedExecutionHandler {    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);}AbortPolicy 抛出RejectedExecutionException
DiscardPolicy 什么也不做,直接忽略
DiscardOldestPolicy 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置
CallerRunsPolicy 直接由提交任务者执行这个任务
3.获取处理结果和异常
线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionException,submit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:ExecutorService executorService = Executors.newFixedThreadPool(4);Future<Object> future = executorService.submit(new Callable<Object>() {    @Override    public Object call() throws Exception {        throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者    }});try {    Object result = future.get();} catch (InterruptedException e) {    // interrupt} catch (ExecutionException e) {    // exception in Callable.call()    e.printStackTrace();}

4.正确配置线程池参数

corePoolSize和maximumPoolSize设置不当会影响效率,甚至耗尽线程;

workQueue设置不当容易导致OOM;

handler设置不当会导致提交任务时抛出异常。

a.先看下机器的CPU核数,然后在设定具体参数:

即CPU核数 = Runtime.getRuntime().availableProcessors()

b.分析下线程池处理的程序是CPU密集型,还是IO密集型

CPU密集型:核心线程数 = CPU核数 + 1

IO密集型:核心线程数 = CPU核数 * 2

5.循环调用线程池

  任务产生速度不要大于线程池任务处理速度

6.线程池线程任务尽量避免使用死循环调用方式

   防止无法即时提取终止任务(当然,这个可以在任务中添加标志位控制线程循环任务的执行)。

                                                                

猜你喜欢

转载自blog.csdn.net/conconbenben/article/details/102529038