• 面试突击32:为什么创建线程池一定要用ThreadPoolExecutor?


    在 Java 语言中,并发编程都是依靠线程池完成的,而线程池的创建方式又有很多,但从大的分类来说,线程池的创建总共分为两大类:手动方式使用 ThreadPoolExecutor 创建线程池和使用 Executors 执行器自动创建线程池。
    那究竟要使用哪种方式来创建线程池呢?我们今天就来详细的聊一聊。

    先说结论

    在 Java 语言中,一定要使用 ThreadPoolExecutor 手动的方式来创建线程池,因为这种方式可以通过参数来控制最大任务数和拒绝策略,让线程池的执行更加透明和可控,并且可以规避资源耗尽的风险。

    OOM风险演示

    假如我们使用了 Executors 执行器自动创建线程池的方式来创建线程池,那么就会存现线程溢出的风险,以 CachedThreadPool 为例我们来演示一下:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ThreadPoolExecutorExample {
        static class OOMClass {
            // 创建 1MB 大小的变量(1M = 1024KB = 1024*1024Byte)
            private byte[] data_byte = new byte[1 * 1024 * 1024];
        }
        public static void main(String[] args) throws InterruptedException {
            // 使用执行器自动创建线程池
            ExecutorService threadPool = Executors.newCachedThreadPool();
            List<Object> list = new ArrayList<>();
            // 添加任务
            for (int i = 0; i < 10; i++) {
                int finalI = i;
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        // 定时添加
                        try {
                            Thread.sleep(finalI * 200);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 将 1M 对象添加到集合
                        OOMClass oomClass = new OOMClass();
                        list.add(oomClass);
                        System.out.println("执行任务:" + finalI);
                    }
                });
            }
        }
    }
    

    第 2 步将 Idea 中 JVM 最大运行内存设置为 10M(设置此值主要是为了方便演示),如下图所示:
    image.png
    以上程序的执行结果如下图所示:
    image.png
    从上述结果可以看出,当线程执行了 7 次之后就开始出现 OutOfMemoryError 内存溢出的异常了。

    内存溢出原因分析

    想要了解内存溢出的原因,我们需要查看 CachedThreadPool 实现的细节,它的源码如下图所示:
    image.png
    构造函数的第 2 个参数被设置成了 Integer.MAX_VALUE,这个参数的含义是最大线程数,所以由于 CachedThreadPool 并不限制线程的数量,当任务数量特别多的时候,就会创建非常多的线程。而上面的 OOM 示例,每个线程至少要消耗 1M 大小的内存,加上 JDK 系统类的加载也要占用一部分的内存,所以当总的运行内存大于 10M 的时候,就出现内存溢出的问题了。

    使用ThreadPoolExecutor来改进

    接下来我们使用 ThreadPoolExecutor 来改进一下 OOM 的问题,我们使用 ThreadPoolExecutor 手动创建线程池的方式,创建一个最大线程数为 2,最多可存储 2 个任务的线程池,并且设置线程池的拒绝策略为忽略新任务,这样就能保证线程池的运行内存大小不会超过 10M 了,实现代码如下:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.*;
    
    /**
     * ThreadPoolExecutor 演示示例
     */
    public class ThreadPoolExecutorExample {
        static class OOMClass {
            // 创建 1MB 大小的变量(1M = 1024KB = 1024*1024Byte)
            private byte[] data_byte = new byte[1 * 1024 * 1024];
        }
    
        public static void main(String[] args) throws InterruptedException {
            // 手动创建线程池,最大线程数 2,最多存储 2 个任务,其他任务会被忽略
            ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 2,
                    0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
                    new ThreadPoolExecutor.DiscardPolicy()); // 拒绝策略:忽略任务
            List<Object> list = new ArrayList<>();
            // 添加任务
            for (int i = 0; i < 10; i++) {
                int finalI = i;
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        // 定时添加
                        try {
                            Thread.sleep(finalI * 200);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 将 1m 对象添加到集合
                        OOMClass oomClass = new OOMClass();
                        list.add(oomClass);
                        System.out.println("执行任务:" + finalI);
                    }
                });
            }
            // 关闭线程池
            threadPool.shutdown();
            // 检测线程池的任务执行完
            while (!threadPool.awaitTermination(3, TimeUnit.SECONDS)) {
                System.out.println("线程池中还有任务在处理");
            }
        }
    }
    

    以上程序的执行结果如下图所示:
    image.png
    从上述结果可以看出,线程池从开始执行到执行结束都没有出现 OOM 的异常,这就是手动创建线程池的优势。

    其他创建线程池的问题

    除了 CachedThreadPool 线程池之外,其他使用 Executors 自动创建线程池的方式,也存在着其他一些问题,比如 FixedThreadPool 它的实现源码如下:
    image.png
    而默认情况下任务队列 LinkedBlockingQueue 的存储容量是 Integer.MAX_VALUE,也是趋向于无限大,如下图所示:
    image.png
    这样就也会造成,因为线程池的任务过多而导致的内存溢出问题。其他几个使用 Executors 自动创建线程池的方式也存在此问题,这里就不一一演示了。

    总结

    线程池的创建方式总共分为两大类:手动使用 ThreadPoolExecutor 创建线程池和自动使用 Executors 执行器创建线程池的方式。其中使用 Executors 自动创建线程的方式,因为线程个数或者任务个数不可控,可能会导致内存溢出的风险,所以在创建线程池时,建议使用 ThreadPoolExecutor 的方式来创建

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

    公众号:Java面试真题解析

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

  • 相关阅读:
    Spring 基于构造函数的依赖注入
    SpringMVC后台接受前台传值的方法
    Spring--jar包
    Ubuntu扩展磁盘空间
    在VScode中运行C/C++
    一个好用的C语言操作
    Python下载超快
    Python多线程
    C语言回调函数
    VScode中运行python
  • 原文地址:https://www.cnblogs.com/vipstone/p/16033301.html
Copyright © 2020-2023  润新知