玩转线程池

293 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担。线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory。即便没有这样的情况,大量的线程回收也会给GC带来很大的压力。为了避免重复的创建线程,线程池的出现可以让线程进行复用。通俗点讲,当有工作来,就会向线程池拿一个线程,当工作完成后,并不是直接关闭线程,而是将这个线程归还给线程池供其他任务使用。

线程池整体结构

图片

Executor是线程池最上层的接口,这个接口有一个核心方法execute(Runnable command),具体是由ThreadPoolExecutor类实现,用于执行任务,ExecutorService接口继承Executor,提供了shutdown(),shutdownNew(),submit()等用来关闭和执行线程的方法。

ExecutorService最终的默认实现类ThreadPoolExecutor。

ThreadPoolExecutor分析

首先通过构造器来一步一步分析。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

  • corePoolSize:线程池的核心池大小,在创建线程池之后,线程池默认没有任何线程,创建之后,默认线程池线程数量为0,当任务过来时就会创建一个新的线程,直到达到corePoolSize之后,会将线程放入workQueue中

  • maximumPoolSize:最大线程数量

  • workQueue一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列。通过workQueue,线程池实现了阻塞功能

  • threadFactory:线程工厂,用来创建线程

  • handler:表示当拒绝处理任务时的策略

  • keepAliveTime:线程池维护线程锁允许的空闲时间,当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程之外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime

    这些参数是怎么再具体执行中发挥作用的呢,我们来看下

execute的执行流程图

![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8a3814f21d114d18b070492fe904360d~tplv-k3u1fbpfcp-zoom-1.image)  

  

当任务过来时就会创建一个新的线程,直到达到corePoolSize之后,会将线程放入workQueue中, 如果运行线程小于corePoolSize,即使有空闲线程,当任务过来时也会创建新的线程 如果线程池数量大于corePoolSize但小于maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务

 如果运行线程数量大于等于maximumPoolSize时,这时如果workQueue也已经满了,则通过handler所指定的策略去处理,如果除核心线程之外的线程没有处理任务,且超过keepAliveTime,就会被回收。

四种常用线程池

newCachedThreadPool

   public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。

  • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

  • 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

newFixedThreadPool

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

  • 创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到线程池池队列中。

  • FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

 

newSingleThreadExecutor

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

一个单线程的线程池,只有一个线程在工作(如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。)能够保证所有任务的执行顺序按照任务的提交顺序执行,同一时段只有一个任务在运行。

newScheduleThreadPool

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

返回值是ScheduledExecutorService

创建一个定长的线程池,而且支持定时的以及周期性的任务执行。
可定时运行(初始延时),运行频率(每隔多长时间运行,还是运行成功一次之后再隔多长时间再运行)的线程池
适合定时以及周期性执行任务的场合。

这里有个执行方法需要注意的。

  private void threadPoolExecutorTest6() throws Exception {
        ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
        // 周期性执行某一个任务,线程池提供了两种调度方式,这里单独演示一下。测试场景一样。
        // 测试场景:提交的任务需要3秒才能执行完毕。看两种不同调度方式的区别
        // 效果1: 提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,完毕后立刻执行)。
        // 也就是说这个代码中是,3秒钟执行一次(计算方式:每次执行三秒,间隔时间1秒,执行结束后马上开始下一次执行,无需等待)
        threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任务-1 被执行,现在时间:" + System.currentTimeMillis());
            }
        }, 20001000, TimeUnit.MILLISECONDS);

        // 效果2:提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,等上一次执行完毕后再开始计时,等待1秒)。
        // 也就是说这个代码钟的效果看到的是:4秒执行一次。 (计算方式:每次执行3秒,间隔时间1秒,执行完以后再等待1秒,所以是 3+1)
        threadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任务-2 被执行,现在时间:" + System.currentTimeMillis());
            }
        }, 20001000, TimeUnit.MILLISECONDS);
    }

阻塞队列

在线程池中,如果任务数量超过了核心线程数,就会把任务放入阻塞队列等待运行,在线程池中主要有三种阻塞队列

  • ArrayBlockingQueue :基于数组的有界队列。

  • LinkedBlockingQueue:基于链表的先进先出队列,是无界的。

  • SynchronousQueue:无缓冲等待队列,它将任务直接交给线程处理而不保持它们。如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中。

四种拒绝策略

  • AbortPolicy 丢弃任务,并抛出RejectedExecutionException 异常

  • CallerRunsPolicy:该任务被线程池拒绝,由调用 execute方法的线程执行该任务。

  • DiscardOldestPolicy :抛弃队列最前面的任务,然后重新尝试执行任务。

  • DiscardPolicy:丢弃任务,不过也不抛出异常。