Java:线程池

创建和销毁线程非常损耗性能,那有没有可能复用一些已经被创建好的线程呢?答案是肯定的,那就是线程池。

另外,线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间,在线程销毁时需要回收这些系统资源,频繁地创建销毁线程会浪费大量资源,而通过复用已有线程可以更好地管理和协调线程的工作。

线程池主要解决两个问题:
一、 当执行大量异步任务时线程池能够提供很好的性能。
二、 线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程等。
——《Java并发编程之美》

线程池体系

在这里插入图片描述
解释说明:

  • Executor 是线程池最顶层的接口,在 Executor 中只有一个 execute 方法,用于执行任务。至于线程的创建、调度等细节由子类实现。
  • ExecutorService 继承并拓展了 Executor,在 ExecutorService 内部提供了更全面的任务提交机制以及线程池关闭方法。
  • ThreadPoolExecutor 是 ExecutorService 的默认实现,所谓的线程池机制也大多封装在此类当中。
  • ScheduledExecutorService 继承自 ExecutorService,增加了定时任务相关方法。
  • ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor,并实现了 ScheduledExecutorService 接口。
  • ForkJoinPool 是一种支持任务分解的线程池,一般要配合可分解任务接口 ForkJoinTask 来使用。

创建线程池

为了开发者可以更方便地使用线程池,JDK 中给我们提供了一个线程池的工厂类—Executors。在 Executors 中定义了多个静态方法,用来创建不同配置的线程池。常见有以下几种。

本文涉及到的源码来源于Java JDK15.0.2

newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按先进先出的顺序执行。

源码

 /**
     * Creates an Executor that uses a single worker thread operating
     * off an unbounded queue. (Note however that if this single
     * thread terminates due to a failure during execution prior to
     * shutdown, a new one will take its place if needed to execute
     * subsequent tasks.)  Tasks are guaranteed to execute
     * sequentially, and no more than one task will be active at any
     * given time. Unlike the otherwise equivalent
     * {@code newFixedThreadPool(1)} the returned executor is
     * guaranteed not to be reconfigurable to use additional threads.
     *
     * @return the newly created single-threaded Executor
     */
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

示例

public class TestnewSingleThreadExecutor {

    public static void main(String[] args) {

        //newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按先进先出的顺序执行。
        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

        int len = 5;
        for (int i = 0; i < len; i++) {
            final int taskID = i;

            // 向线程池中提交任务
            newSingleThreadExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    PrintlnUtils.println("线程: "+Thread.currentThread().getName()+" 正在执行任务 taskID:  "+taskID);
                }
            });

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 执行上述代码结果如下,可以看出所有的 task 始终是在同一个线程中被执行的。
        //线程: pool-1-thread-1 正在执行任务 taskID:  0
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  1
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  2
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  3
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  4
    }
}

newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

源码

/**
     * Creates a thread pool that creates new threads as needed, but
     * will reuse previously constructed threads when they are
     * available.  These pools will typically improve the performance
     * of programs that execute many short-lived asynchronous tasks.
     * Calls to {@code execute} will reuse previously constructed
     * threads if available. If no existing thread is available, a new
     * thread will be created and added to the pool. Threads that have
     * not been used for sixty seconds are terminated and removed from
     * the cache. Thus, a pool that remains idle for long enough will
     * not consume any resources. Note that pools with similar
     * properties but different details (for example, timeout parameters)
     * may be created using {@link ThreadPoolExecutor} constructors.
     *
     * @return the newly created thread pool
     */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

示例

public class TestnewCachedThreadPool {

    public static void main(String[] args) {

        //newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

        int len = 5;
        for (int i = 1; i <= len; i++) {
            final int taskID = i;
		/*
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
         */

            // 向线程池中提交任务
            newCachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        PrintlnUtils.println("线程: " + Thread.currentThread().getName() + " 正在执行任务 taskID:  " + taskID);
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

        }

        newCachedThreadPool.shutdown();

        //线程: pool-1-thread-5 正在执行任务 taskID:  5
        //
        //线程: pool-1-thread-2 正在执行任务 taskID:  2
        //
        //线程: pool-1-thread-3 正在执行任务 taskID:  3
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  1
        //
        //线程: pool-1-thread-4 正在执行任务 taskID:  4

        //从上面日志中可以看出,缓存线程池会创建新的线程来执行任务。但是如果将代码修改一下,在提交任务之前休眠 1 秒钟,如下:
        //再次执行则打印日志同 SingleThreadPool 一模一样,原因是提交的任务只需要 500 毫秒即可执行完毕,
        // 休眠 1 秒导致在新的任务提交之前,线程 “pool-1-thread-1” 已经处于空闲状态,可以被复用执行任务。

        //线程: pool-1-thread-1 正在执行任务 taskID:  1
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  2
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  3
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  4
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  5



    }
}

newFixedThreadPool

创建一个固定数目的、可重用的线程池。

源码

 /**
     * Creates a thread pool that reuses a fixed number of threads
     * operating off a shared unbounded queue.  At any point, at most
     * {@code nThreads} threads will be active processing tasks.
     * If additional tasks are submitted when all threads are active,
     * they will wait in the queue until a thread is available.
     * If any thread terminates due to a failure during execution
     * prior to shutdown, a new one will take its place if needed to
     * execute subsequent tasks.  The threads in the pool will exist
     * until it is explicitly {@link ExecutorService#shutdown shutdown}.
     *
     * @param nThreads the number of threads in the pool
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code nThreads <= 0}
     */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

示例

public class TestnewFixedThreadPool {

    public static void main(String[] args) {

        //newFixedThreadPool 创建一个固定数目的、可重用的线程池。
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);

        int len = 10;
        for (int i = 1; i <= len; i++) {
            final int taskID = i;


            // 向线程池中提交任务
            newFixedThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        PrintlnUtils.println("线程: " + Thread.currentThread().getName() + " 正在执行任务 taskID:  " + taskID);
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        //上述代码创建了一个固定数量 3 的线程池,因此虽然向线程池提交了 10 个任务,但是这 10 个任务只会被 3 个线程分配执行,执行效果如下:
        //线程: pool-1-thread-1 正在执行任务 taskID:  1
        //
        //线程: pool-1-thread-3 正在执行任务 taskID:  3
        //
        //线程: pool-1-thread-2 正在执行任务 taskID:  2
        //
        //线程: pool-1-thread-3 正在执行任务 taskID:  4
        //
        //线程: pool-1-thread-2 正在执行任务 taskID:  6
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  5
        //
        //线程: pool-1-thread-3 正在执行任务 taskID:  7
        //
        //线程: pool-1-thread-2 正在执行任务 taskID:  8
        //
        //线程: pool-1-thread-1 正在执行任务 taskID:  9
        //
        //线程: pool-1-thread-2 正在执行任务 taskID:  10
    }
}

newScheduledThreadPool

创建一个定时线程池,支持定时及周期性任务执行。

源码

 /**
     * Creates a thread pool that can schedule commands to run after a
     * given delay, or to execute periodically.
     * @param corePoolSize the number of threads to keep in the pool,
     * even if they are idle
     * @return the newly created scheduled thread pool
     * @throws IllegalArgumentException if {@code corePoolSize < 0}
     */
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

示例

public class TestnewScheduledThreadPool {

    public static void main(String[] args) {

        //newScheduledThreadPool 创建一个定时线程池,支持定时及周期性任务执行。
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        //延迟1秒执行,每隔一秒执行一次
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                Date date = new Date();
                PrintlnUtils.println("线程 "+Thread.currentThread().getName()+" 报时:"+date);


            }
        },500,500, TimeUnit.MILLISECONDS);

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 使用shutdown关闭定时任务
        scheduledExecutorService.shutdown();

        //线程 pool-1-thread-1 报时:Sat Jan 09 09:00:42 CST 2021
        //
        //线程 pool-1-thread-1 报时:Sat Jan 09 09:00:43 CST 2021
        //
        //线程 pool-1-thread-1 报时:Sat Jan 09 09:00:43 CST 2021
        //
        //线程 pool-1-thread-1 报时:Sat Jan 09 09:00:44 CST 2021
        //
        //线程 pool-1-thread-1 报时:Sat Jan 09 09:00:44 CST 2021
        //
        //线程 pool-1-thread-1 报时:Sat Jan 09 09:00:45 CST 2021
        //
        //线程 pool-1-thread-1 报时:Sat Jan 09 09:00:45 CST 2021
        //
        //线程 pool-1-thread-2 报时:Sat Jan 09 09:00:46 CST 2021
        //
        //线程 pool-1-thread-1 报时:Sat Jan 09 09:00:46 CST 2021
        //
        //线程 pool-1-thread-1 报时:Sat Jan 09 09:00:47 CST 2021



    }
}

上面这几种就是常用到的线程池使用方式,但是,在 阿里Java开发手册 中已经严禁使用 Executors 来创建线程池,这是为什么呢?
看一下newSingleThreadExecutor 和 newFixedThreadPool() 的具体实现,如下:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

可以看到传入的是一个无界的阻塞队列,理论上可以无限添加任务到线程池。当核心线程执行时间很长(比如 sleep10s),则新提交的任务还在不断地插入到阻塞队列中,最终造成
OOM。

看一下 newCachedThreadPool 的实现:
在这里插入图片描述
可以看到,缓存线程池的最大线程数为 Integer 最大值。当核心线程耗时很久,线程池会尝试创建新的线程来执行提交的任务,当内存不足时就会报无法创建线程的错误。

以上两种情况如果发生在生产环境将会是致命的,从阿里手册中严禁使用 Executors 的态度上也能看出,阿里也是经历过血淋淋的教训。

总结
线程池是一把双刃剑,使用得当会使代码如虎添翼;但是使用不当将会造成重大性灾难。而剑柄是握在开发者手中,只有理解线程池的运行原理,熟知它的工作机制与使用场景,才会使这把双刃剑发挥更好的作用。

欢迎关注我的公众号,不定期推送优质的文章,
微信扫一扫下方二维码即可关注。
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值