线程池的内部实现

上一篇文章讲了线程池的基本用法和一些使用技巧,这篇文章就继续深入的了解线程池,看看常用的线程池的内部实现,话不多说,开始:

  对于几个核心的线程池,无论是newFixedThreadPool(int nThreads),newSingleThreadExecutor()和newCacheedThreadPool(),虽然看起来创建的线程有着完全不同的功能特点,但是其实内部实现都使用了 ThreadPoolExecutor 实现,下面看看这三个线程的实现:

 1 public static ExecutorService newFixedThreadPool(int nThreads) {
 2         return new ThreadPoolExecutor(nThreads, nThreads,
 3                                       0L, TimeUnit.MILLISECONDS,
 4                                       new LinkedBlockingQueue<Runnable>());
 5     }
 6 public static ExecutorService newSingleThreadExecutor() {
 7         return new FinalizableDelegatedExecutorService
 8             (new ThreadPoolExecutor(1, 1,
 9                                     0L, TimeUnit.MILLISECONDS,
10                                     new LinkedBlockingQueue<Runnable>()));
11     }
12 public static ExecutorService newCachedThreadPool() {
13         return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
14                                       60L, TimeUnit.SECONDS,
15                                       new SynchronousQueue<Runnable>());
16     }

 由以上线程池的实现代码可以看出,它们都只是 ThreadPoolExecutor 类的封装。所以我们了解了ThreadPoolExecutor ,就了解了线程池,我们就从它最重要的构造函数了解开始:

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

函数的参数含义:

  ❤ corePoolSize:指定了线程池中的线程数量,默认情况下,线程池中的线程数量为0,当有任务来时才去创建线程;

  ❤ maximumPoolSize:指定了线程池中的最大线程数量;

  ❤ keepAliveTime:当线程池线程数量超过corePoolSize时,多于的空闲线程的存活时间,即,超过corePoolSize的空闲时间,在多长时间内,会被销毁;

  ❤ unit:keepAliveTime的单位;

  ❤ workQueue:任务队列,被提交但尚未被执行的任务;

  ❤ threadFactory:线程工厂,用于创建线程,一般默认的就行;

  ❤ handler:拒接策略。当任务太多来不及处理,如何拒绝任务。

corePoolSize和maximumPoolSize的理解

这里特别罗列几条规则解释这两个参数:

  ①线程池中的线程数量小于corePoolSize,新的任务都不排队而是直接新增线程执行;

  ②线程池的线程数量大于等于corePoolSize,而workQueue未满,新的任务加入workQueue而不是新增线程;

  ③线程池的线程数量大于等于corePoolSize,并且workQueue已满,但是线程数量小于maximumPoolSize,新增新的线程来处理任务;

  ④线程池的线程数量大于等于corePoolSize,并且workQueue已满,而且线程数量大于等于maximumPoolSize,新的任务被拒绝,使用handler处理被拒绝的任务;

workQueue和handler的详细说明

workQueue:

   workQueue指的是被提交但是未被执行的任务队列,它是一个BlockingQueue 接口的对象,仅用于存放Runnable对象。

  根据队列功能来分类,在 ThreadPoolExecutor 的构造函数中可使用下面几种BlockingQueue:

    ❤ 直接提交的队列:该功能由SynchronousQueue 对象提供,SynchronousQueue是一个特殊的BlockingQueue。SynchronousQueue 没有容量,每一个插入操作都要等待相应的删除操作,反之,每一个删除操作都要等待对应的插入操作。如果使用SynchronousQueue,提交的任务不会被真实的保存,而总是将新的任务提交给线程执行,如果没有空闲的线程,则会尝试创建线程,如果线程数量已经达到最大值,则执行拒绝策略。因此,使用SynchronousQueue队列,通常需要设置很大的maximumPoolSize值,否则很容易执行拒绝策略;

    ❤ 有界的任务队列:有界的任务队列可使用ArrayBlockingQueue实现,ArrayBlockingQueue的构造函数必须带一个容量参数,表示该队列的最大容量  public ArrayBlockingQueue(int capacity),当时用有界的任务队列时,若有新的任务需要执行,如果线程池中线程数量小于corePoolSize,则会优先创建新的线程,若大于等于corePoolSize,则会把新的任务加入到等待队列中,若队列已满,无法加入,则在总线程数量不大于maximumPoolSize的前提下,创建新的线程执行。若大于等于maximumPoolSize,则执行拒绝策略。可见,有界队列仅当在任务队列装满时,才可能将线程数提到corePoolSize之上,换言之,除非系统非常繁忙,否则核心线程数维持在corePoolSize。

    ❤ 无界的任务队列:无界的任务队列可以通过LinkedBlockingQueue 类实现。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,线程池的线程数量小于corePoolSize时,线程池会生成新的线程执行任务,当线程池的线程数量到达corePoolSize时,线程数量就不会增加。若后续有新的任务提交,而又没有空闲的线程,则任务就会直接进入无界的队列等待。若任务的创建和处理的速度差异很大,无界队列就会保持快速增长,直到系统资源耗尽。

    ❤ 优先任务队列:优先任务队列是带有执行优先级的队列。它通过PriorityBlockingQueue 实现,可以控制任务的执行先后顺序,它是一个特殊的无界队列。无论是有界队列ArrayBlockingQueue,还是未指定大小的无界队列LinkedBlockingQueue 都是按照先进先出算法处理的。而PriorityBlockingQueue 则可以根据任务自身的优先级顺序先后执行,在确保系统性能的同时,同时也有很好的质量保证(总是确保高优先级的任务先执行)。

明白了几种任务队列,我们再来回顾上述三种核心的线程池的实现,加深对常用核心的线程池的理解:

  ⑴newFixedThreadPool(int nThreads)方法:这个方法返回了一个corePoolSize和maximumPoolSize大小一样的,并且使用了LInkedBlockingQueue 任务队列的线程池。因为对于固定大小的线程池来说,不存在线程数量的动态变化,因此corePoolSize和maximumPoolSize可以相等,同时,它使用无界队列来存放无法立即执行的任务,当任务提交非常频繁的时候,该队列可能迅速膨胀,从而耗尽系统资源。

  ⑵newSingleThreadExecutor()方法:该方法返回的是一个单线程线程池,是newFixedThreadPool的一种退化,只是简单的将线程数量设置为1而已。

  ⑶newCachedThreadPool()方法:该方法返回corePoolSize为0,maximumPoolSize为Integer的最大值。这就意味着在没有任务时,该线程池内无线程,当任务被提交时,该线程池会使用空闲的线程执行任务,若无空闲线程,则将加入SynchronousQueue队列,而SynchronousQueue 队列是一种直接提交的队列,它总是会迫使线程池增加新的线程执行任务,当任务执行完毕后,由于corePoolSize为0,因此空闲线程又会在指定时间内(60S)被回收。对于newCachedThreadPool(),如果有大量的任务被提交,而任务执行又不那么快时,那么系统便会开启等量的线程处理,这样的做法可能会很快耗尽系统资源。

  下面看看ThreadPoolExecutor线程池的核心调度代码:

 1 public void execute(Runnable command) {
 2         if (command == null)
 3             throw new NullPointerException();
 4         int c = ctl.get();
 5         if (workerCountOf(c) < corePoolSize) {
 6             if (addWorker(command, true))
 7                 return;
 8             c = ctl.get();
 9         }
10         if (isRunning(c) && workQueue.offer(command)) {
11             int recheck = ctl.get();
12             if (! isRunning(recheck) && remove(command))
13                 reject(command);
14             else if (workerCountOf(recheck) == 0)
15                 addWorker(null, false);
16         }
17         else if (!addWorker(command, false))
18             reject(command);
19     }

上述代码的第5行workerCountOf()方法取得了当前线程池的线程总数。当线程总数小于corePoolSize核心线程数时,会将任务通过addWorker()方法直接调度执行,否则在第10行代码处(workQueue.offer())进入等待队列,如果进入队列失败(比如有界队列到达了上限,或者使用了SynchronousQueue),则会执行第17行,将任务直接提交给线程池。如果当前线程数量已经达到了maximumPoolSize,则提交失败,执行第18行的拒绝策略。

handler():

  JDK内置提供了四种拒绝策略:

  ⒈ AbortPolicy策略:该策略直接抛出异常,阻止系统正常工作;

  ⒉ CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降;

  ⒊ DiscardOledestPolicy策略:该策略丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

  ⒋ DiscardPolicy策略:该策略默默丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这就是最好的一种方案。

 上面四种内置策略均实现了 RejectedExecutionHandler 接口,若上面的拒绝策略无法满足实际需求,你也可以自己扩展 RejectedExecutionHandler 接口,RejectExecutionHandler 的定义如下:

1 public interface RejectedExecutionHandler{
2     void rejectedExecution(Runnable r,ThreadPoolExecutor executor);
3 }

其中r为请求的任务,executor 为当前的线程池。

下面演示下拒绝策略的使用:

 1 public class RejectThreadPoolDemo {
 2 
 3     public static class MyTask implements Runnable{
 4         @Override
 5         public void run() {
 6             System.out.println(System.currentTimeMillis() + " : Thread ID:" + Thread.currentThread().getId());
 7             try {
 8                 Thread.sleep(100);
 9             } catch (InterruptedException e) {
10                 e.printStackTrace();
11             }
12         }
13     }
14     //测试
15     public static void main(String[] args) throws InterruptedException {
16         MyTask myTask = new MyTask();
17         ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10),
18                 new RejectedExecutionHandler() {
19                     @Override
20                     public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
21                         System.out.println(r.toString() + " is discard!");
22                     }
23                 });
24         //循环提交任务
25         for (int i = 0; i < Integer.MAX_VALUE;i++){
26             es.submit(myTask);
27             Thread.sleep(10);
28         }
29         es.shutdown();
30     }
31 }

输出结果:

......
1538097339848 : Thread ID:12
1538097339858 : Thread ID:13
1538097339868 : Thread ID:14
1538097339878 : Thread ID:15
java.util.concurrent.FutureTask@330bedb4 is discard!
java.util.concurrent.FutureTask@2503dbd3 is discard!
java.util.concurrent.FutureTask@4b67cf4d is discard!
java.util.concurrent.FutureTask@7ea987ac is discard!
java.util.concurrent.FutureTask@12a3a380 is discard!
1538097339938 : Thread ID:11
1538097339948 : Thread ID:12
1538097339958 : Thread ID:13
......

上述代码在16到23行,自定义了一个线程池,该线程池拥有5个常驻线程,并且最大线程池数量也是5个,这和固定大小的线程池是一样的。但是它却只拥有一个容量只有10的任务队列;然后我们自定义了拒绝策略,打印出了被拒绝的任务;从输出结果来看,在执行了一些任务后,拒绝策略就生效了。

 

参考:《Java高并发程序设计》 葛一鸣 郭超 编著:

posted on 2018-09-28 09:25  AoTuDeMan  阅读(548)  评论(0编辑  收藏  举报

导航