手把手教你手动建立线程池

1、为何要手动建立线程池?

咱们之因此要手动建立线程池,是由于 JDK 自带的工具类所建立的线程池存在必定的弊端,那究竟存在怎么样的弊端呢?首先来回顾一下 JDK 中线程池框架的继承关系: java

Java线程池框架继承结构.png

JDK 线程池框架继承关系图

咱们最经常使用的线程池实现类是ThreadPoolExecutor(红框里的那个),首先咱们来看一下它最通用的构造方法:数据库

/**
 * 各参数含义
 * corePoolSize    : 线程池中常驻的线程数量。核心线程数,默认状况下核心线程会一直存活,即便处于闲置状态也不会 
 *                   受存活时间 keepAliveTime 的限制,除非将 allowCoreThreadTimeOut 设置为 true。
 * maximumPoolSize : 线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的
 *                           LinkedBlockingQueue时,这个值无效。
 * keepAliveTime   : 当线程数量多于 corePoolSize 时,空闲线程的存活时长,超过这个时间就会被回收
 * unit            : keepAliveTime 的时间单位
 * workQueue       : 存放待处理任务的队列
 * threadFactory   : 线程工厂
 * handler         : 拒绝策略,拒绝没法接收添加的任务
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) { ... ... }

使用 JDK 自带的 Executors工具类 (图中蓝色框中的那个,这是独立于线程池继承关系图的工具类,相似于 Collections 和 Arrays) 能够直接建立如下种类的线程池:编程

  1. 线程数量固定的线程池,此方法返回 ThreadPoolExecutor缓存

    public static ExecutorService newFixedThreadPool(int nThreads) {... ...}
  2. 单线程线程池,此方法返回 ThreadPoolExecutor网络

    public static ExecutorService newSingleThreadExecutor() {... ...}
  3. 可缓存线程的线程池,此方法返回 ThreadPoolExecutor多线程

    public static ExecutorService newCachedThreadPool() {... ...}
  4. 执行定时任务的线程池,此方法返回 ScheduledThreadPoolExecutor并发

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {... ...}
  5. 能够拆分执行子任务的线程池,此方法返回 ForkJoinPool框架

    public static ExecutorService newWorkStealingPool() {... ...}

JDK 自带工具类建立的线程池存在的问题

直接使用这些线程池虽然很方便,可是存在两个比较大的问题:ide

  1. 有的线程池能够无限添加任务或线程,容易致使 OOM;工具

    就拿咱们最经常使用FixedThreadPoolCachedThreadPool来讲,前者的详细建立方法以下:

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

    可见其任务队列用的是LinkedBlockingQueue,且没有指定容量,至关于无界队列,这种状况下就能够添加大量的任务,甚至达到Integer.MAX_VALUE的数量级,若是任务大量堆积,可能会致使 OOM。

    CachedThreadPool的建立方法以下:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
    }

    这个虽然使用了有界队列SynchronousQueue,可是最大线程数设置成了Integer.MAX_VALUE,这就意味着能够建立大量的线程,也有可能致使 OOM。

  2. 还有一个问题就是这些线程池的线程都是使用 JDK 自带的线程工厂 (ThreadFactory)建立的,线程名称都是相似pool-1-thread-1的形式,第一个数字是线程池编号,第二个数字是线程编号,这样很不利于系统异常时排查问题。

若是你安装了“阿里编码规约”的插件,在使用Executors建立线程池时会出现如下警告信息:

Alibaba Java Coding Guidelines.png

阿里编码规约的警告信息

为避免这些问题,咱们最好仍是手动建立线程池。

2、 如何手动建立线程池

2.1 定制线程数量

首先要说明一点,定制线程池的线程数并非多么高深的学问,也不是说一旦线程数设定不合理,你的程序就没法运行,而是要尽可能避免如下两种极端条件:

  1. 线程数量过大

    这会致使过多的线程竞争稀缺的 CPU 和内存资源。CPU 核心的数量和计算能力是有限的,在分配不到 CPU 执行时间的状况下,线程只能处于空闲状态。而在JVM 中,线程自己也是对象,也会占用内存,过多的空闲线程天然会浪费宝贵的内存空间。

  2. 线程数量太小

    线程池存在的意义,或者说并发编程的意义就是为了“压榨”计算机的运算能力,说白了就是别让 CPU 闲着。若是线程数量比 CPU 核心数量还小的话,那么一定有 CPU 核心将处于空闲状态,这是极大的浪费。

因此在实际开发中咱们须要根据实际的业务场景合理设定线程池的线程数量,那又如何分析业务场景呢?咱们的业务场景大体能够分为如下两大类:

  1. CPU (计算)密集型

    这种场景须要大量的 CPU 计算,好比加密、计算 hash 等,最佳线程数为 (CPU 核心数 + 1)。好比8核 CPU,能够把线程数设置为 9,这样就足够了,由于在 CPU 密集型的场景中,每一个线程都会在比较大的负荷下工做,不多出现空闲的状况,正好每一个线程对应一个 CPU 核心,而后不停地工做,这样就实现了最优利用率。多出的一个线程起到了备胎的做用,在其余线程意外中断时顶替上去,确保 CPU 不中断工做。其实也大可没必要这么死板,线程数量设置为 CPU 核心数的 1 到 2 倍都是能够接受的。

  2. I/O 密集型

    好比读写数据库,读写文件或者网络读写等场景。各类 I/O 设备 (好比磁盘)的速度是远低于 CPU 执行速度的,因此在 I/O 密集型的场景下,线程大部分时间都在等待资源而非 CPU 时间片,这样的话一个 CPU 核心就能够应付不少线程了,也就能够把线程数量设置大一点。线程具体数量的计算方法能够参考 Brain Goetz 的建议:

    假设有如下变量:

    • Nthreads = 线程数量
    • Ncpu = CPU 核心数
    • Ucpu = 指望的CPU 的使用率 ,由于 CPU 可能还要执行其余任务
    • W = 线程的平均等待资源时间
    • C = 线程平均使用 CPU 的计算时间
    • W / C = 线程等待时间与计算时间的比率

这样为了让 CPU 达到指望的使用率,最优的线程数量计算公式以下:

Nthreads = Ncpu Ucpu ( 1 + W / C )

CPU 核心数能够经过如下方法获取:

int N_CPUS = Runtime.getRuntime().availableProcessors();

固然,除了 CPU,线程数量还会受到不少其余因素的影响,好比内存和数据库链接等,须要具体问题具体分析。

2.2 使用可自定义线程名称的线程工厂

这个就简单多了,能够借助大名鼎鼎的谷歌开源工具库 Guava,首先引入以下依赖:

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>28.2-jre</version>
</dependency>

而后我就可使用其提供的ThreadFactoryBuilder类来建立线程工厂了,Demo 以下:

public class ThreadPoolDemo {

    // 线程数
    public static final int THREAD_POOL_SIZE = 16;

    public static void main(String[] args) throws InterruptedException {
        // 使用 ThreadFactoryBuilder 建立自定义线程名称的 ThreadFactory
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("hyn-demo-pool-%d").build();
        
        // 建立线程池,其中任务队列须要结合实际状况设置合理的容量
        ThreadPoolExecutor executor = new ThreadPoolExecutor(THREAD_POOL_SIZE,
                THREAD_POOL_SIZE,
                0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(1024),
                namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());
        
        // 新建 1000 个任务,每一个任务是打印当前线程名称
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> System.out.println(Thread.currentThread().getName()));
        }
        // 优雅关闭线程池
        executor.shutdown();
        executor.awaitTermination(1000L, TimeUnit.SECONDS);
        // 任务执行完毕后打印"Done"
        System.out.println("Done");
    }
}

控制台打印结果以下:

... ...
hyn-demo-pool-2
hyn-demo-pool-6
hyn-demo-pool-13
hyn-demo-pool-12
hyn-demo-pool-15
Done

可见这样的线程名称相比pool-1-thread-1更有辨识度,能够为不一样用途的线程池设定不一样的名称,便于系统出故障时排查问题。

3、总结

本文为你们介绍了手动建立线程池的详细方法,不过这些都是理论性的内容,而多线程编程是很是注重实践的一门学问,在实际生产环境中要综合考虑各类因素并不断尝试,才能实现最佳实践。

相关文章
相关标签/搜索