线程池的7种建立方式,强烈推荐你用它...

根据摩尔定律所说:集成电路上可容纳的晶体管数量每 18 个月翻一番,所以 CPU 上的晶体管数量会愈来愈多。html

但随着时间的推移,集成电路上可容纳的晶体管数量已趋向饱和,摩尔定律也渐渐失效,所以多核 CPU 逐渐变为主流,与之相对应的多线程编程也开始变得普及和流行起来,这固然也是好久以前的事了,对于如今而言多线程编程已经成为程序员必备的职业技能了,那接下来咱们就来盘一盘“线程池”这个多线程编程中最重要的话题。java

什么是线程池?

线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时能够避免从新建立和销毁线程所带来性能开销,只须要从“池子”内取出相应的线程执行对应的任务便可。程序员

池化思想在计算机的应用也比较普遍,好比如下这些:数据库

  • 内存池(Memory Pooling):预先申请内存,提高申请内存速度,减小内存碎片。
  • 链接池(Connection Pooling):预先申请数据库链接,提高申请链接的速度,下降系统的开销。
  • 实例池(Object Pooling):循环使用对象,减小资源在初始化和释放时的昂贵损耗。

线程池的优点主要体如今如下 4 点:编程

  1. 下降资源消耗:经过池化技术重复利用已建立的线程,下降线程建立和销毁形成的损耗。
  2. 提升响应速度:任务到达时,无需等待线程建立便可当即执行。
  3. 提升线程的可管理性:线程是稀缺资源,若是无限制建立,不只会消耗系统资源,还会由于线程的不合理分布致使资源调度失衡,下降系统的稳定性。使用线程池能够进行统一的分配、调优和监控。
  4. 提供更多更强大的功能:线程池具有可拓展性,容许开发人员向其中增长更多的功能。好比延时定时线程池ScheduledThreadPoolExecutor,就容许任务延期执行或按期执行。

同时阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须经过线程池提供,不容许在应用中自行显式建立线程数组

说明:线程池的好处是减小在建立和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。
若是不使用线程池,有可能形成系统建立大量同类线程而致使消耗完内存或者“过分切换”的问题。缓存

知道了什么是线程池以及为什要用线程池以后,咱们再来看怎么用线程池。安全

线程池使用

线程池的建立方法总共有 7 种,但整体来讲可分为 2 类:多线程

  • 一类是经过 ThreadPoolExecutor 建立的线程池;
  • 另外一个类是经过 Executors 建立的线程池。

image.png

线程池的建立方式总共包含如下 7 种(其中 6 种是经过 Executors 建立的,1 种是经过 ThreadPoolExecutor 建立的):并发

  1. Executors.newFixedThreadPool:建立一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
  2. Executors.newCachedThreadPool:建立一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
  3. Executors.newSingleThreadExecutor:建立单个线程数的线程池,它能够保证先进先出的执行顺序;
  4. Executors.newScheduledThreadPool:建立一个能够执行延迟任务的线程池;
  5. Executors.newSingleThreadScheduledExecutor:建立一个单线程的能够执行延迟任务的线程池;
  6. Executors.newWorkStealingPool:建立一个抢占式执行的线程池(任务执行顺序不肯定)【JDK 1.8 添加】。
  7. ThreadPoolExecutor:最原始的建立线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。

单线程池的意义
从以上代码能够看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 建立的都是单线程池,那么单线程池的意义是什么呢?
答:虽然是单线程池,但提供了工做队列,生命周期管理,工做线程维护等功能。

那接下来咱们来看每种线程池建立的具体使用。

1.FixedThreadPool

建立一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。

使用示例以下:

public static void fixedThreadPool() {
    // 建立 2 个数据级的线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(2);

    // 建立任务
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
        }
    };

    // 线程池执行任务(一次添加 4 个任务)
    // 执行任务的方法有两种:submit 和 execute
    threadPool.submit(runnable);  // 执行方式 1:submit
    threadPool.execute(runnable); // 执行方式 2:execute
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}

执行结果以下:
image.png

若是以为以上方法比较繁琐,还用更简单的使用方法,以下代码所示:

public static void fixedThreadPool() {
    // 建立线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    // 执行任务
    threadPool.execute(() -> {
        System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
    });
}

2.CachedThreadPool

建立一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。

使用示例以下:

public static void cachedThreadPool() {
    // 建立线程池
    ExecutorService threadPool = Executors.newCachedThreadPool();
    // 执行任务
    for (int i = 0; i < 10; i++) {
        threadPool.execute(() -> {
            System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });
    }
}

执行结果以下:
image.png
从上述结果能够看出,线程池建立了 10 个线程来执行相应的任务。

3.SingleThreadExecutor

建立单个线程数的线程池,它能够保证先进先出的执行顺序。

使用示例以下:

public static void singleThreadExecutor() {
    // 建立线程池
    ExecutorService threadPool = Executors.newSingleThreadExecutor();
    // 执行任务
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + ":任务被执行");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });
    }
}

执行结果以下:
image.png

4.ScheduledThreadPool

建立一个能够执行延迟任务的线程池。

使用示例以下:

public static void scheduledThreadPool() {
    // 建立线程池
    ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
    // 添加定时执行任务(1s 后执行)
    System.out.println("添加任务,时间:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任务被执行,时间:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }, 1, TimeUnit.SECONDS);
}

执行结果以下:
image.png
从上述结果能够看出,任务在 1 秒以后被执行了,符合咱们的预期。

5.SingleThreadScheduledExecutor

建立一个单线程的能够执行延迟任务的线程池。

使用示例以下:

public static void SingleThreadScheduledExecutor() {
    // 建立线程池
    ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
    // 添加定时执行任务(2s 后执行)
    System.out.println("添加任务,时间:" + new Date());
    threadPool.schedule(() -> {
        System.out.println("任务被执行,时间:" + new Date());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }, 2, TimeUnit.SECONDS);
}

执行结果以下:
image.png
从上述结果能够看出,任务在 2 秒以后被执行了,符合咱们的预期。

6.newWorkStealingPool

建立一个抢占式执行的线程池(任务执行顺序不肯定),注意此方法只有在 JDK 1.8+ 版本中才能使用。

使用示例以下:

public static void workStealingPool() {
    // 建立线程池
    ExecutorService threadPool = Executors.newWorkStealingPool();
    // 执行任务
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
        });
    }
    // 确保任务执行完成
    while (!threadPool.isTerminated()) {
    }
}

执行结果以下:
image.png
从上述结果能够看出,任务的执行顺序是不肯定的,由于它是抢占式执行的。

7.ThreadPoolExecutor

最原始的建立线程池的方式,它包含了 7 个参数可供设置。

使用示例以下:

public static void myThreadPoolExecutor() {
    // 建立线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
    // 执行任务
    for (int i = 0; i < 10; i++) {
        final int index = i;
        threadPool.execute(() -> {
            System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

执行结果以下:
image.png

ThreadPoolExecutor 参数介绍

ThreadPoolExecutor 最多能够设置 7 个参数,以下代码所示:

public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue,
                           ThreadFactory threadFactory,
                           RejectedExecutionHandler handler) {
     // 省略...
 }

7 个参数表明的含义以下:

参数 1:corePoolSize

核心线程数,线程池中始终存活的线程数。

参数 2:maximumPoolSize

最大线程数,线程池中容许的最大线程数,当线程池的任务队列满了以后能够建立的最大线程数。

参数 3:keepAliveTime

最大线程数能够存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。

参数 4:unit:

单位是和参数 3 存活时间配合使用的,合在一块儿用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有如下 7 种可选:

  • TimeUnit.DAYS:天
  • TimeUnit.HOURS:小时
  • TimeUnit.MINUTES:分
  • TimeUnit.SECONDS:秒
  • TimeUnit.MILLISECONDS:毫秒
  • TimeUnit.MICROSECONDS:微妙
  • TimeUnit.NANOSECONDS:纳秒

参数 5:workQueue

一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含如下 7 种类型:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue相似,还含有非阻塞方法。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

较经常使用的是 LinkedBlockingQueueSynchronous,线程池的排队策略与 BlockingQueue 有关。

参数 6:threadFactory

线程工厂,主要用来建立线程,默认为正常优先级、非守护线程。

参数 7:handler

拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:

  • AbortPolicy:拒绝并抛出异常。
  • CallerRunsPolicy:使用当前调用的线程来执行此任务。
  • DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
  • DiscardPolicy:忽略并抛弃当前任务。

默认策略为 AbortPolicy

线程池的执行流程

ThreadPoolExecutor 关键节点的执行流程以下:

  • 当线程数小于核心线程数时,建立线程。
  • 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
  • 当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,建立线程;若线程数等于最大线程数,抛出异常,拒绝任务。

线程池的执行流程以下图所示:
image.png

线程拒绝策略

咱们来演示一下 ThreadPoolExecutor 的拒绝策略的触发,咱们使用 DiscardPolicy  的拒绝策略,它会忽略并抛弃当前任务的策略,实现代码以下:

public static void main(String[] args) {
    // 任务的具体方法
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("当前任务被执行,执行时间:" + new Date() +
                               " 执行线程:" + Thread.currentThread().getName());
            try {
                // 等待 1s
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    // 建立线程,线程的任务队列的长度为 1
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
                                                           100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
                                                           new ThreadPoolExecutor.DiscardPolicy());
    // 添加并执行 4 个任务
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}

咱们建立了一个核心线程数和最大线程数都为 1 的线程池,而且给线程池的任务队列设置为 1,这样当咱们有 2 个以上的任务时就会触发拒绝策略,执行的结果以下图所示:
image.png
从上述结果能够看出只有两个任务被正确执行了,其余多余的任务就被舍弃并忽略了。其余拒绝策略的使用相似,这里就不一一赘述了。

自定义拒绝策略

除了 Java 自身提供的 4 种拒绝策略以外,咱们也能够自定义拒绝策略,示例代码以下:

public static void main(String[] args) {
    // 任务的具体方法
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("当前任务被执行,执行时间:" + new Date() +
                               " 执行线程:" + Thread.currentThread().getName());
            try {
                // 等待 1s
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    // 建立线程,线程的任务队列的长度为 1
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
                                                           100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
                                                           new RejectedExecutionHandler() {
                                                               @Override
                                                               public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                                                   // 执行自定义拒绝策略的相关操做
                                                                   System.out.println("我是自定义拒绝策略~");
                                                               }
                                                           });
    // 添加并执行 4 个任务
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}

程序的执行结果以下:
image.png

究竟选用哪一种线程池?

通过以上的学习咱们对整个线程池也有了必定的认识了,那究竟该如何选择线程池呢?

咱们来看下阿里巴巴《Java开发手册》给咱们的答案:

【强制要求】线程池不容许使用 Executors 去建立,而是经过 ThreadPoolExecutor 的方式,这样的处理方式让写的同窗更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端以下:

1) FixedThreadPool 和 SingleThreadPool:容许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而致使 OOM。

2)CachedThreadPool:容许的建立线程数量为 Integer.MAX_VALUE,可能会建立大量的线程,从而致使 OOM。

因此综上状况所述,咱们推荐使用 ThreadPoolExecutor 的方式进行线程池的建立,由于这种建立方式更可控,而且更加明确了线程池的运行规则,能够规避一些未知的风险。

总结

本文咱们介绍了线程池的 7 种建立方式,其中最推荐使用的是 ThreadPoolExecutor 的方式进行线程池的建立,ThreadPoolExecutor 最多能够设置 7 个参数,固然设置 5 个参数也能够正常使用,ThreadPoolExecutor 当任务过多(处理不过来)时提供了 4 种拒绝策略,固然咱们也能够自定义拒绝策略,但愿本文的内容能帮助到你。原创不易,以为不错就点个赞再走吧!

参考 & 鸣谢

https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

http://www.javashuo.com/article/p-miwcjepu-ny.html

关注公众号「Java中文社群」发现更多干货。

相关文章
相关标签/搜索