• Java 中线程池的 7 种创建方式!


    在 Java 语言中,并发编程都是通过创建线程池来实现的,而线程池的创建方式也有很多种,每种线程池的创建方式都对应了不同的使用场景,总体来说线程池的创建可以分为以下两类:

    • 通过 ThreadPoolExecutor 手动创建线程池。
    • 通过 Executors 执行器自动创建线程池。


    而以上两类创建线程池的方式,又有 7 种具体实现方法,这 7 种实现方法分别是:

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

    接下来我们分别来看这 7 种线程池的具体使用。

    1.FixedThreadPool

    创建一个固定大小的线程池,可控制并发线程数。
    使用 FixedThreadPool 创建 2 个固定大小的线程池,具体实现代码如下:

    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

    创建一个可缓存的线程池,若线程数超过任务所需,那么多余的线程会被缓存一段时间后才被回收,若线程数不够,则会新建线程。
    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 个线程来执行相应的任务。

    使用场景

    CachedThreadPool 是根据短时间的任务量来决定创建的线程数量的,所以它适合短时间内有突发大量任务的处理场景。

    3.SingleThreadExecutor

    创建单个线程的线程池,它可以保证先进先出的执行顺序。
    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

    单个线程的线程池有什么意义?

    单个线程的线程池相比于线程来说,它的优点有以下 2 个:

    • 可以复用线程:即使是单个线程池,也可以复用线程。
    • 提供了任务管理功能:单个线程池也拥有任务队列,在任务队列可以存储多个任务,这是线程无法实现的,并且当任务队列满了之后,可以执行拒绝策略,这些都是线程不具备的。

    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 秒之后被执行了,实现了延迟 1s 再执行任务。

    5.SingleThreadScheduledExecutor

    创建一个单线程的可以执行延迟任务的线程池,此线程池可以看作是 ScheduledThreadPool 的单线程池版本。
    它的使用示例如下:

    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 版本新增的,因此只有在 JDK 1.8 以上的程序中才能使用。
    newWorkStealingPool 使用示例如下:

    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

    ThreadPoolExecutor 是最原始、也是最推荐的手动创建线程池的方式,它在创建时最多提供 7 个参数可供设置。
    ThreadPoolExecutor 使用示例如下:

    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 相比于其他创建线程池的优势在于,它可以通过参数来控制最大任务数和拒绝策略,让线程池的执行更加透明和可控,所以在阿里巴巴《Java开发手册》是这样规定的:

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

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

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

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

    总结

    线程池的创建方式总共有以下 7 种:

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

    线程池的创建推荐使用最后一种 ThreadPoolExecutor 的方式来创建,因为使用它可以明确线程池的运行规则,规避资源耗尽的风险

    是非审之于己,毁誉听之于人,得失安之于数。

    公众号:Java面试真题解析

    面试合集:https://gitee.com/mydb/interview

  • 相关阅读:
    面试(转)
    Expression Blend实战开发技巧
    Twelve Principles of Agile Software
    Test Software Engineer
    Web开发工程师必读的15个设计博客
    麻省理工的C/C++的课程
    Orchard:处理1对多的关系
    DotNetNuke Switches to C# !!
    我的那个他
    2011 微软MVP全球大会
  • 原文地址:https://www.cnblogs.com/vipstone/p/15974852.html
Copyright © 2020-2023  润新知