美文网首页
线程池的使用入门

线程池的使用入门

作者: 文景大大 | 来源:发表于2020-04-16 19:33 被阅读0次

    在上一篇文章中,我们总结了三种创建线程的方式:《Java多线程基础——三种创建线程的方式》,然而在真实的开发中很少直接使用这些方式创建线程,更多地是使用线程池。

    线程池是一种“池”技术,就像连接池一样,提前准备好特定数量的资源,所有任务都使用其中的资源运行,从而结局了线程频繁创建和销毁带来的资源损耗。同时,线程池还提供了线程的创建、运行、等待、关闭等操作,大大解放了开发人员的工作,简化了多线程使用的门槛。

    一、入门实例

    我们先来实际感受下使用线程池的例子,其中的某些参数不懂的可以先跳过,后面会详细介绍。

    @Slf4j
    public class FruitThread implements Runnable {
        private String fruitName;
    
        public FruitThread(String fruitName){
            this.fruitName = fruitName;
        }
    
        @Override
        public void run() {
            log.info("get fruit: {}", fruitName);
        }
    }
    
    public class ThreadPoolTest {
        public static void main(String[] args) {
            Runnable apple = new FruitThread("apple");
            Runnable orange = new FruitThread("orange");
    
            // 创建线程工厂,线程池中的线程都由该工厂创建
            ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
            // 创建一个线程池,参数先不用管,后续详细介绍
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 0L,
                    TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
            // 将任务交给线程池去执行
            threadPoolExecutor.execute(apple);
            threadPoolExecutor.execute(orange);
            // 关闭线程池
            threadPoolExecutor.shutdown();
        }
    }
    

    我们可以看到,与先前自己创建线程,自己负责执行不同,使用线程池的时候,我们不用管理线程的创建和执行,非常地方便。

    二、线程池参数

    我们看下创建线程池时的构造函数:

        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler)
    
    • corePoolSize,线程池中最少保有线程的数量,即使它们都是空闲状态;
    • maximumPoolSize,线程池中最多保有的线程数量;
    • keepAliveTime,当线程池中的线程数量超过corePoolSize时,这些多出来的线程如果空闲的话,等待多久时间被销毁;
    • unit,超时时间单位;
    • workQueue,当线程池中没有空闲状态线程时,新的任务会被存放到这个任务队列中,等待空闲线程来执行;
      • SynchronousQueue,直连队列,提交的任务直接交给空闲线程进行处理,队列本身不存储任务,当没有空闲线程时,提交任务的线程就会阻塞,直至有空闲线程,容易阻塞后面任务的提交与执行;
      • LinkedBlockingQueue,无界队列,可以存储无限量的任务(Integer.MAX_VALUE),此时线程池中只会维持corePoolSize个线程,不会超过这个值,即maximumPoolSize无效,会造成任务队列过大,资源浪费;
      • ArrayBlockingQueue,有界队列,比较常用,保存固定数量的任务,均衡前两者的优缺点;
    • threadFactory,创建线程池中线程的工厂;
    • handler,当线程池无法执行该线程或者任务队列无法再保存新来的任务时,用来处理这种异常情况的策略;
      • AbortPolicy,这是默认策略,抛出RejectedExecutionException异常;
      • CallerRunsPolicy,用当前执行execute方法的线程来运行当前被拒绝的任务,如果当前该线程已经结束,则该任务被丢弃;
      • DiscardOldestPolicy,丢弃任务队列中最早提交的任务,并重试execute方法将当前任务提交到队列中;
      • DiscardPolicy,静默丢弃当前被拒绝的任务;

    三、常用线程池

    除了使用上面new新的线程池对象来创建线程池之外,还可以使用如下四种方式来创建线程池。

    • Executors.newCachedThreadPool,可缓存线程池。

      如下是其源码,最少0线程,最大无界,采用直连队列。

      优点是任务执行迅速,不用经过任务队列,空闲时不用保留空闲线程,节约资源;缺点是最大线程数量无界,容易造成线程资源过度消耗;适用于大量短耗时任务,且对响应要求高的场景。

          public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
              return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                            60L, TimeUnit.SECONDS,
                                            new SynchronousQueue<Runnable>(),
                                            threadFactory);
          }
      
    • Executors.newFixedThreadPool,定长线程池。

      如下是其源码,线程池中的线程数量是从0逐步增加到nThreads的,一旦达到nThreads,往后最小、最大线程数量都是nThreads,空闲和忙碌时都保持这个数量;超时时间为0,因为它没有意义,使用无界队列。

      优点是线程资源消耗可控;缺点是任务骤增时无法弹性增长线程数量,可能导致大量任务积压延迟,且无界队列可能会导致任务资源过度消耗;适用于任务峰值量不会很高,且对响应时间要求不高的场景。

          public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
              return new ThreadPoolExecutor(nThreads, nThreads,
                                            0L, TimeUnit.MILLISECONDS,
                                            new LinkedBlockingQueue<Runnable>(),
                                            threadFactory);
          }
      
    • Executors.newScheduledThreadPool,延时线程池。

      如下是其源码,最少corePoolSize个线程,最大无界,一旦有线程空闲会被立即销毁,采用了延时队列,该队列中的任务只有等到其对应的延时条件满足时才会从任务队列中弹出被线程执行。

      适用于需要延时执行和定时执行的场景。

          public ScheduledThreadPoolExecutor(int corePoolSize,
                                             ThreadFactory threadFactory) {
              super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                    new DelayedWorkQueue(), threadFactory);
          }
      

      需要注意的是,其它线程池提交任务直接使用execute即可,延时线程池也可以使用execute,但是不会有延时效果,而是使用如下的方法来达到延时效果。

      • schedule(task, time,unit),task将在指定时间后提交到队列;
      • schdeuleAtFixedRate(task, initDelay, period, unit),task一开始延迟initDelay执行,无论task需要耗时多久,然后每隔period开始执行;
      • scheduleWithFixedDelay(task, initDelay, delay, unit),task一开始延迟initDelay执行,等到执行完毕后,再延迟delay开始下一次执行;
    • Executors.newSingleThreadExecutor,单线程线程池。

      如下是其源码,任何时间只有一个线程,采用了无界队列,可以保证所有任务按照提交顺序得到执行。

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

    根据阿里巴巴的Java代码开发规约,我们在实际开发中,尽量使用new方式自己创建线程池,而不是使用Executors来创建以上的四种线程池,理由是:

    new的方式,更容易让开发者明确线程池的运行规则,比如一眼就能直到线程池的各项参数,更容易思考和规避资源消耗问题。

    四、线程池的监控

    • getTaskCount,获取总的任务数量,包含已经完成的、执行中的、等待执行的;
    • getCompletedTaskCount,获取已经完成的任务数量;
    • getLargestPoolSize,获取曾经创建过的最大线程数量,用来判断是否达到过maximumPoolSize;
    • getPoolSize,获取当前的线程数量;
    • getActiveCount,获取当前正在执行任务的忙碌状态的线程数量;

    五、关闭线程池

    • shutdown,等待已经提交的任务执行完毕,不再接收新的任务,然后关闭线程池;
    • shutdownNow,中断正在执行中的线程任务,已经提交还未执行的任务全部废弃,不再接收新的任务,立即关闭线程池;

    相关文章

      网友评论

          本文标题:线程池的使用入门

          本文链接:https://www.haomeiwen.com/subject/yiwdvhtx.html