【科普解读】线程池为何是聪明开发者的最爱?

56 阅读8分钟

关注公众号 程序员小胖 每日技术干货,第一时间送达!

引言

在面试过程中为什么面试官一般都会问到线程池这个知识点?其实在日常工作中针对复杂场景的处理线程池算是离我们很近的解决方案之一,既然是我们经常接触的技术解决方案,那面试官肯定不会放过这个好机会去了解候选人对于技术的追求是不是嘴上说的对技术有热情 热爱编程工作等等花言巧语。接下来我们了解下池化技术一个重要的实现场景 线程池。

为什么要用线程池

  • 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
  • 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。 综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。

什么是线程池?

”池化资源”技术产生的原因是 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。

线程池

是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

线程池的实现原理

主要基于池化技术的核心思想(对于池化技术还不是很明白的朋友可以看我上一篇《独家揭秘!池化技术的秘密用途!》),即实现资源的复用,避免资源的重复创建和销毁带来的性能开销。

线程池的工作原理可以概括为以下几点:

  • 核心线程数:线程池在创建时会设置一个核心线程数,当线程池中的线程数量小于核心线程数时,即使线程处于空闲状态,也会新建线程以满足核心线程数的要求。
  • 任务队列:当线程池中的线程数量等于核心线程数,且队列未满时,新任务会被添加到队列中,而不是新建线程来处理任务。如果队列已满,且线程数量小于最大线程数,线程池会继续新建线程来处理任务。
  • 最大线程数:线程池在创建时会设置一个最大线程数,当线程数量大于核心线程数,且队列已满,且线程数量等于最大线程数时,如果还有任务需要执行,线程池会通过拒绝策略来拒绝这些任务。
  • 空闲线程终止:如果线程池中的空闲线程数量超过一定时间,线程会被终止,这样可以动态调整线程池中的线程数量,节省资源。
  • 线程池的构造参数:线程池的构造需要传入一系列参数,包括核心线程数、最大线程数、任务队列等,这些参数在线程池中起到了关键作用。

总结来说,线程池通过池化技术实现了线程的复用,通过核心线程数、任务队列、最大线程数等参数的设置,实现了线程数量的动态调整,提高了资源利用率,降低了系统开销。

线程池的创建方式

  1. newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
 private static ExecutorService threadpool = Executors.newSingleThreadExecutor();
    @Test
    public void newSingleThreadExecutor() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            final int index = i;
            Thread.sleep(3000);
            threadpool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "  start ===" + index);
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "  end ===" + index);

                }
            });
        }
        Thread.sleep(200000);
    }

可以看到它的线程池长度固定为1,同一时刻只有一个线程在执行,不存在多线程资源争夺的问题。

  1. newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
private static ExecutorService threadpool = Executors.newFixedThreadPool(3);
    @Test
    public void newFixedThreadPool() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            final int index = i;
            Thread.sleep(1000);
            threadpool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "  start ===" + index);
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "  end ===" + index);

                }
            });
        }
        Thread.sleep(200000);
    }

可以看到它的线程池是固定长度的,当占用数量达到线程池长度的时候,后面的线程需要等待线程池资源的释放,才能获取执行机会。

  1. newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
private static ExecutorService threadpool = Executors.newCachedThreadPool();
    @Test
    public void newCachedThreadPool() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            final int index = i;
            Thread.sleep(1000);
            threadpool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "  start ===" + index);
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "  end ===" + index);

                }
            });
        }

        Thread.sleep(200000);
    }

可以看到它的线程池中线程数量并不固定,如果有新任务进来,它会自动扩充线程池大小并启动,如果任务结束,它的池大小则会缩减。

  1. newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
  • 定长线程池特性
private static ExecutorService threadPool = Executors.newScheduledThreadPool(2);
    @Test
    public void newScheduledThreadPool() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            final int index = i;
            Thread.sleep(1000);
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "  start ===" + index);
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "  end ===" + index);

                }
            });
        }
        Thread.sleep(200000);
    }

执行结果,可以看到它定长线程池的特性,具有newFixedThreadPool 的特性。

  • 延迟特性
private static ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
    @Test
    public void newScheduledThreadPool() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            final int index = i;
            threadPool.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " "+DateUtils.parseDate(new Date())+" begin ===" + index);
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "  end ===" + index);

                }
            },5, TimeUnit.SECONDS);
        }
        Thread.sleep(200000);
    }

可以看到线程池在延迟5秒后启动执行

  • 延迟+周期特性

private static ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);

    @Test
    public void newScheduledThreadPool() throws InterruptedException {
            threadPool.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " "+DateUtils.parseDate(new Date())+" start ===");
                }
            },8,5, TimeUnit.SECONDS);
        Thread.sleep(200000);
    }

行可以看到,线程延迟8秒开始第一次执行,之后以每5秒执行一次

submit()和execute()的区别?

submit()和execute()的区别主要从三方面区分:

  1. 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。
  2. 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
  3. 异常处理:submit()方便Exception处理

Executors和ThreaPoolExecutor的区别

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 Executors 各个方法的弊端:

  • newFixedThreadPool 和 newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
  • newCachedThreadPool 和 newScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定