一文彻底搞定Java线程池,从此面试有底气

Java线程池学习

前言

线程池是一个很重要的知识点,我相信大家在面试中也很常见,今天我们就来详细了解一下线程池,并且重点介绍一下线程池常见参数的含义,如何配置线程池参数,并且文末附有线程池常见面试题,我相信通过本文的学习,大家以后对于线程池相关的面试题,肯定是能做到游刃有余的
PS:由于作者能力有限,文章内容可能存在描述不当、错误的地方,还请各位读者能够告知博主,博主将立即作出更正,文章部分内容是摘自其他优秀的博客,如有侵权,还请告知,在下将立即删除改正,相关文章引用在文末链接

线程池基本介绍

  • 线程池是什么

    线程池是一种管理线程的机制,它通过复用线程对象来减少线程的创建和销毁的开销。线程池在多线程编程中被广泛应用,它可以提高程序的性能和效率,并且可以更好地管理系统的资源。

    简而言之:线程池就是一种对线程进行池化的技术

  • 什么是线程

    线程是操作系统进行运算调度的最小单位,它是进程中的一个相对独立可执行的代码片段。Java中每个线程都有自己独立的虚拟机栈本地方法栈程序计数器,同时共享同一个进程的元空间直接内存

  • 什么是进程

    进程是操作系统进行资源调度的最小单位,它是一个正在运行的程序实例。

  • 什么是池化

    池化(Pooling)是一种资源管理的策略,用于减少资源的创建和销毁开销,提高资源的利用率和系统性能。

    池化这个策略在计算机中有很多常见的应用,比如:数据库连接池、线程池、常量池

  • 为什么需要线程池?(线程池的作用是什么)

    池化的本质是为了提高资源的复用率,假如我们不使用池化技术,每次用完一个线程就直接销毁,然后下一次使用有需要重新去创建一个新的线程,这就会涉及到频繁的内存分配和GC,这个过程是需要浪费资源的。

    这里就举一个简单的例子,每次都有亲戚来家里访问,我们每次都需要去摆盘,过完之后就把盘子收起来,这样来回弄很费时间,所以我们干脆直接就专门把盘子放在桌子上,每次客人拜访完,我们就只将盘子中的东西给填充玩,无需再去等到客人来了再继续摆设,这个过程节约了时间,在计算机里面时间也是一种资源。

    同样的,对于数据库连接,我们每次都建立连接都需呀先进行身份验证,这个过程也是耗时的,如果没有连接池,每次连接都需要进行一个身份验证,而有了连接池,只需要直接去连接池中拿一个验证好的连接即可。

    对于常量池也是一样的道理,如果没有池,每次一个字符串都需要重新去new一个对象,单独分配一个内存来存储这个数据,有了常量池只需要放到固定的位置,其它线程访问时,直接到固定位置获取即可。这个思想其实有一个设计模式与之对应,也就是享元模式

    • 降低资源消耗:线程的创建和销毁都是有成本的,通过使用线程池,可以复用线程对象,避免频繁地创建和销毁线程,从而降低资源消耗。
    • 提高响应速度:线程池中的线程可以立即响应任务,无需等待线程创建的时间,从而提高任务的响应速度。
    • 控制并发数:线程池可以限制可并发执行的线程数量,防止系统因过多的线程而导致资源耗尽。
    • 提高系统稳定性:通过合理配置线程池的大小,可以避免系统因过多的线程而导致资源耗尽和系统崩溃的问题。

    注意:不使用线程池(单线程)是串行执行的,使用线程池是并行执行的,不保障执行的顺序性,想要保障,需要使用其它的线程协调技术

  • 线程池的常见状态

    • Running(运行状态):线程池初始化后的初始状态,接受新任务并处理已添加的任务。
    • Shutdown(关闭状态):调用shutdown()方法后的状态。不再接受新任务,但会处理已添加到队列中的任务。
    • Stop(停止状态):调用shutdownNow()方法后的状态。不再接受新任务,并且尝试停止当前正在执行的任务。此时可能会中断正在运行的线程。
    • Tidying(清理状态):所有任务都已经终止,线程池正在进行最终的清理工作,比如关闭线程池。
    • Terminated(终止状态):线程池彻底终止的状态,表示线程池已经关闭。

    PS:线程是6种状态,线程池是5种状态

    image-20231006162154194

  • 什么是线程上下文

    线程上下文是指线程运行时的状态和环境,具体包括:线程状态、线程计数器、虚拟机栈以及本地方法栈等

  • 什么是线程上下文切换

    CPU的执行权从一个线程转移到另一个线程,这个过程称之为线程上下文切换,在进行线程上下文切换时,会暂存当前线程的上下文,便于后续的恢复

注意:频繁的线程上下文切换意味着会消耗大量的时间,所以一般尽量减少线程上下文切换的频率

  • 什么是CPU密集型

    CPU密集型(CPU-bound)指的是任务的执行主要依赖于CPU的处理能力,而对I/O操作的需求较少。这类任务通常涉及大量的计算、逻辑判断和数据处理,例如复杂的算法运算、大规模数据的排序或搜索等。在CPU密集型任务中,CPU的主要工作是进行复杂的计算,而不会长时间等待外部资源的响应。

    简而言之:CPU需要处理大量的计算操作

  • 什么是IO密集型

    IO密集型(I/O-bound)指的是任务执行主要依赖于I/O操作,而对CPU的处理能力需求较低。这类任务通常涉及大量的读写操作,例如从磁盘读取大量数据、网络通信、用户交互等。在IO密集型任务中,CPU的主要工作是等待I/O操作完成,并处理相关的事件。

    简而言之:CPU需要处理大量的读写操作

注意

  1. CPU密集型和IO密集型不是二选一的概念,而是针对任务的特点进行描述。在实际应用中,很多任务既涉及CPU的计算,又需要进行I/O操作,但其中一个方面可能占据主导地位。
  2. 对于CPU密集型任务,为了充分利用CPU的处理能力,可以采用多线程、并行计算或使用优化的算法来提高性能。
  3. 对于IO密集型任务,为了避免CPU长时间处于空闲等待的状态,可以采用异步IO、事件驱动或使用线程池等方式来提高效率。
  4. 理解任务的特性(CPU密集型还是IO密集型)有助于选择合适的编程模型或优化策略,从而提高系统的性能和并发处理能力。

Executor

Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Threadstart 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this 逃逸问题

img

Executor 框架的使用示意图

  • 什么是this逃逸问题

    this逃逸问题是指在对象还没有构造完成时,它的引用就被其他线程获取到了。这就导致了其他线程可能会在对象构造完成之前就开始对其进行操作,可能会引发一些意想不到的问题。

    this逃逸问题可能会导致线程安全性问题、内存可见性问题和数据一致性问题等,因此需要在编码过程中注意避免this引用在构造方法中逃逸。以下是一个简单是demo,用于展示 this 逃逸问题

    class ThisEscapeDemo {
        private int value;
    
        public ThisEscapeDemo() {
            new Thread(new InnerRunnable()).start();
            // 休眠 10ms ,用于模拟对象还未构造完成,this就被子线程引用了
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            this.value = 42;
        }
    
        private class InnerRunnable implements Runnable {
            @Override
            public void run() {
                // 此时ThisEscapeDemo对象还未构造完成就直接引用this了,输出结果为0
                System.out.println(ThisEscapeDemo.this.value);
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            new ThisEscapeDemo();
        }
    }
    

ThreadPoolExecutor

在前面的基本介绍中,主要是论述了线程池是什么,还有线程池框架的大体结构,主要还是一些比较抽象的东西,明白了线程池是一个池化技术,对于 Java 中线程池具体是个啥,我们还没有讲解。在 Java 中无非就是对象和类,线程池同样的,Java 官方将线程池封装成了ExecutorService接口,只需要实现这个接口就算一个线程池,而ThreadPoolExecutor是接口的一个具体实现。

也就是说,Java中线程池具体是一个类、接口,是他们的实例对象,到这我相信大家应该是能够理解了,Java通过将线程池技术进行封装变成了一个接口(规范),我们要使用线程池,只需要调用接口实现类的实例对象即可。

这个操作在 面对对象的 语言中,都是很常见的,将一个抽象的东西封装一个具体的对象

image-20231004145815103

常用方法介绍

这里也只简单介绍一下ThreadPoolExecutor的常用方法和常见成员变量,关于详情可以直接去看源码

  • ThreadPoolExecutor接口常见方法

    • submit(Runnable task)submit(Callable<T> task):用于提交一个任务到线程池,并返回一个表示任务执行结果的Future对象。
    • invokeAny(Collection<? extends Callable<T>> tasks)invokeAll(Collection<? extends Callable<T>> tasks):用于提交多个任务到线程池,invokeAny()方法返回其中一个完成的任务的结果,而invokeAll()方法返回所有任务的结果集合。
    • shutdown()shutdownNow():用于优雅地关闭线程池。shutdown()方法将不再接受新的任务,但会等待已经提交的任务执行完成;shutdownNow()方法会立即停止所有正在执行的任务,并返回等待执行的任务列表。
    • isShutdown()isTerminated():用于检查线程池是否已经关闭。

    以上方法都是实现ExecutorService接口的方法

ThreadPoolExecutor除了提供以上四个常用方法以外,还提供了四个不同的构造方法,这里只介绍其中一个最全的,其它三个都是直接引用这一个的:

    /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

线程参数详解

  • ThreadPoolExecutor类常见成员变量
    • corePoolSize(必选):核心线程数。 默认值为 0,线程池的核心线程数,即线程池空闲时保留的线程数量。
    • maximumPoolSize(必选):最大线程数。默认值为 Integer.MAX_VALUE,线程池中最大允许的线程数。
    • keepAliveTime(必选):临时线程空闲存活期。默认值为 0。,线程闲置超时时长,如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
    • unit(必选):临时线程空闲存活期时间单位。默认值为 TimeUnit.MILLISECONDS,指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
    • workQueue(必选):任务队列。默认为一个无界队列,也就是 LinkedBlockingQueue,用于保存待执行任务的阻塞队列。
    • threadFactory(可选):线程工厂。默认为一个普通的线程工厂,使用线程的默认设置,用于创建新线程的工厂。
    • rejectedExecutionHandler(可选):拒绝策略。默认为一个拒绝策略为抛出 RejectedExecutionException 的处理器,也就是 ThreadPoolExecutor.AbortPolicy,当线程池已满并且无法执行新的任务时,用于处理被拒绝的任务的策略。

除了以上参数我们还需要了解几个常见的概念

  1. 当任务队列满了,核心线程都被占用,最大线程数量还没达到,此时会创建临时线程(非核心线程)
任务队列
  • ThreadPoolExecutorworkQueue成员变量取值:

    该成员变量接受的是一个BlockingQueue类型的阻塞队列,Java 官方已经为我们提供了7中不同的阻塞队列,以下七种不同的阻塞队列都可以作为任务队列

    • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
    • LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE
    • PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供
    • Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
    • DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
    • SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
    • LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
    • LinkedTransferQueue: 它是 ConcurrentLinkedQueueLinkedBlockingQueueSynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。

关于这七种,我们一般而言用的最多的是 ArrayBlockingQueueLinkedBlockingQueue,这两个也是比较经典的有界和无界队列,如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。

线程工厂

只需要通过实现ThreadFactory接口,即可自定义线程工厂,通过采用工厂模式,可以让线程的创建变得更加友好。

默认的线程工厂,位于java.util.concurrent.Executors.DefaultThreadFactory

    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
任务拒绝策略
  • ThreadPoolExecutorrejectedExecutionHandler成员变量有以下取值:
    • ThreadPoolExecutor.AbortPolicy默认):拒绝抛异常。如果线程池队列已满,并且线程池中的线程数达到了最大线程数,则新提交的任务将立即被拒绝,并抛出RejectedExecutionException异常。
    • ThreadPoolExecutor.CallerRunsPolicy提交线程执行。如果线程池队列已满,并且线程池中的线程数达到了最大线程数,则线程池会将任务返回给提交任务的线程执行。也就是说,调用线程自己执行该任务。这种情况下不会抛出RejectedExecutionException异常。虽然这种策略能够保证任务一定能够被执行,但可能会影响主线程或调用线程的性能。
    • ThreadPoolExecutor.DiscardPolicy直接丢弃。如果线程池队列已满,并且线程池中的线程数达到了最大线程数,则新提交的任务将被直接丢弃,不会抛出异常,也不会执行任务。该策略下没有任何反馈机制,因此任务丢失了。
    • ThreadPoolExecutor.DiscardOldestPolicy丢弃并添加。如果线程池队列已满,并且线程池中的线程数达到了最大线程数,则线程池会将队列中最早(最先)提交但还未被执行的任务丢弃,然后将新提交的任务添加到队列中。

Executors

ThreadPoolExecutor是线程池最核心的类,但是它太灵活了,从上面也可以看出ThreadPoolExecutor有好多配置想,看着就头痛🤣,这一点 Java 官方也是考虑到了的,直接就为我们这群 API 调用师封装了许多不同类型的线程池,基本上囊括了各种常见的应用场景,不得不说这 Java 官方是真的六,基本上为我们开发者封装了好多好用的“轮子”,不仅大大降低了开发难度,同时也提供了一个代码学习的模板,所以作为一名 Java 开发者,学习源码是一个必不可少的过程🤭如果哪天你能够随手写出 Java 源码级别的代码,那么就预示着你不在是一个 API 调用师,而是一名真正的 Java 开发者(●’◡’●)

  • Executors提供的四种线程池

    • newFixedThreadPool(int nThreads):创建一个定长线程池,该线程池中的线程数量固定为指定的nThreads个。如果任务数量超过线程池的大小,多余的任务将等待执行。

      • 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的无界队列。

      • 应用场景:控制线程最大并发数。

    • newCachedThreadPool():创建一个可缓存线程池,该线程池中的线程数量会根据任务的数量自动调整。如果线程池中有空闲线程,则会直接使用空闲线程执行任务;如果所有线程都在工作,则会创建新的线程执行任务。当线程空闲一段时间后,会被回收。

      • 特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
      • 应用场景:执行大量、耗时少的任务。
    • newSingleThreadExecutor():创建一个单线程线程池,该线程池按顺序执行任务。如果任务在执行时出现异常,会创建一个新的线程继续执行后续的任务。

      • 特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
      • 应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
    • newScheduledThreadPool(int corePoolSize):创建一个定时线程池,该线程池支持定时和周期性任务执行。该线程池可用于按一定的延迟时间执行任务,或者按一定的时间间隔执行任务。

      • 特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列
      • 应用场景:执行定时或周期性的任务。
  • 四种方式的比较

    img

    • FixedThreadPoolSingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至出现 OOM(OutOfMemoryError,内存不足错误)。
    • CachedThreadPoolScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至出现 OOM。

    所以一般而言更加推荐使用ThreadPoolExecutor,而不是Executors,阿里巴巴 Java 开发手册中也有写

    image-20231004170745581

线程池常见计算

  • 线程池中的常见计算公式

    • 最大线程数 = 核心线程数 + 临时线程数(核心线程数 > = 1 ,临时线程数 > = 0 ,最大线程数 > = 核心线程数) 最大线程数 = 核心线程数 + 临时线程数(核心线程数 >= 1,临时线程数 >= 0,最大线程数 >= 核心线程数) 最大线程数=核心线程数+临时线程数(核心线程数>=1,临时线程数>=0,最大线程数>=核心线程数)

    • 对于CPU密集型: 核心线程数 = C P U 核数 + 1 核心线程数=CPU核数+1 核心线程数=CPU核数+1

    • 对于IO密集型: 核心线程数 = C P U 核数 ∗ 2 核心线程数=CPU核数*2 核心线程数=CPU核数2

    • 最佳线程数 = C P U 核数 ∗ ( 1 + W T / S T ) 最佳线程数 = CPU核数*(1+WT/ST) 最佳线程数=CPU核数(1+WT/ST)

      其中 WT(Wait Time) 代表CPU的等待时间,ST(Service Time)CPU的服务时间或执行时间,

      CPU密集型的, W T / S T WT/ST WT/ST 接近于0,IO密集型 W T / S T WT/ST WT/ST 会很大,为了避免创建过多的线程,这里就直接选用了2,关于这个比值我们可以通过 JDK 自带的工具 VisualVM 来查看

线程池工作流程解析

image-20231006160758857

    /**
     * 线程池核心构造方法
     *
     * @param corePoolSize    核心线程数
     * @param maximumPoolSize 最大线程数
     * @param keepAliveTime   临时线程存活期
     * @param unit            临时线程存活期时间单位
     * @param workQueue       工作队列
     * @param threadFactory   线程工厂
     * @param handler         拒绝策略
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        // 参数校验
        if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
            throw new IllegalArgumentException();
        // 判断工作队列、拒绝策略是否为空
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        // 获取当前线程的安全上下文,以便进行安全管理和权限控制的相关操作
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        // 初始化线程池参数
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

    /**
     * 提交任务,没有返回值
     * @param command
     */
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        // 获取当前工作线程数和线程池运行状态(共32位,前3位为运行状态,后29位为运行线程数)
        int c = ctl.get();
        // 判断当前工作线程数是否小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
            // 小于核心线程数,直接新建
            if (addWorker(command, true))
                return;
            // 然后更新当前工作线程数
            c = ctl.get();
        }
        // 判断当前是否 有处于运行状态 并且 工作队列可以添加任务(其实就是判断判断任务队列是否已满)
        if (isRunning(c) && workQueue.offer(command)) { // 如果当前线程池状态为RUNNING,并且任务成功添加到阻塞队列
            int recheck = ctl.get();
            // 双重检查,因为从上次检查到进入此方法,线程池可能已成为SHUTDOWN状态
            if (! isRunning(recheck) && remove(command))
                // 如果当前线程池状态不是 RUNNING 则从队列删除任务,然后执行拒绝策略
                reject(command);
            else if (workerCountOf(recheck) == 0)
                // 当线程池中的工作线程数量为0时,并且工作队列中还有任务,则往线程池中新增一个工作线程
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) // 这里的 if 相当于是双检,判断此时是否有空余的线程
            // 任务队列已满,直接执行拒绝策略
            reject(command);
    }

    /**
     * 提交任务,有返回值
     * @param task
     * @return
     */
    public Future<?> submit(Runnable task) {
        // 判断任务是否为空
        if (task == null) throw new NullPointerException();
        // 相较于execute,对原始的Runnable任务进行了一层包装,这样就能够有返回值了
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

线程池最佳实践

详情参考:👉Java 线程池最佳实践 | JavaGuide(Java面试 + 学习指南)👈好文值得推荐👍b( ̄▽ ̄)d👍

  • 正确声明线程池:线程池必须手动通过 ThreadPoolExecutor 的构造函数来声明,避免使用Executors 类创建线程池

    原因

    1. 降低发生 OOM 的风险
    2. 线程池参数需要根据具体项目环境测试敲定,而不是写死的
    3. 便于定位问题,手动创建线程池可以设置线程池的名称
  • 监测线程池运行状态

    原因

    1. 便于及时发现问题,了解系统的运行状况
    2. 便于快速定位问题
  • 不同业务使用不同的线程池

    原因

    1. 不同的业务具体所需的线程池参数可能不同
    2. 不同的业务共用一个线程池可能造成线程安全问题
  • 给每一个线程池都进行命名

    原因

    1. 便于定位问题
  • 合理配置线程池参数

    原因

    1. 提高系统性能
    • 对于CPU密集型: 核心线程数 = C P U 核数 + 1 核心线程数=CPU核数+1 核心线程数=CPU核数+1

      原因:CPU密集型是需要占用CPU的,如果CPU核数大于核心线程数,就会造成线程竞争问题,多个线程抢占同一个CPU,从而容易引发频繁的线程上下文切换,说白了就是为了不让CPU太劳累了

    • 对于IO密集型: 核心线程数 = C P U 核数 ∗ 2 核心线程数=CPU核数*2 核心线程数=CPU核数2

      原因:IO密集型不需要占用CPU,但是会占用线程,所以我们在配置更多的线程,这样即使一部分线程因为IO被阻塞了,还有其他线程去执行CPU相关计算操作,说白了就是为了不让CPU太闲了

  • 线程池不用了就关闭

    原因

    1. 避免资源浪费
  • 线程池尽量不要放耗时任务

    原因

    1. 避免线程被长时间在占用,线程池的作用就是为了让线程进行复用,耗时任务建议异步执行
  • 避免重复创建线程池

    原因

    1. 线程池本身就可以复用

常见面试题

Runnable 和 Callable 的区别

  • 使用方式不同

    • Runnable任务直接就可以被线程池执行,需要重写run()方法,方法没有返回值;
    • Callable任务需要封装成FutureTask对象后才能被线程池执行,需要重写call()方法,方法具有返回值
    class MyCallableTask implements Callable<Object> {
        @Override
        public Object call() throws Exception {
            System.out.println("任务执行");
            return null;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            FutureTask<Object> task = new FutureTask<>(new MyCallableTask());
            executorService.execute(task);
        }
    }
    
    class MyRunnableTask implements Runnable {
        @Override
        public void run() {
            System.out.println("任务执行");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.execute(new MyRunnableTask());
        }
    }
    
  • 返回值不同

    • Runnable任务没有返回值,run()方法内部抛出异常外部无法捕获
    • Callable任务有返回值,call()方法内部抛出的异常外部可以通过 Future 对象的get()方法进行捕获
  • 使用场景不同。Runnable任务不会返回异常或者线程执行结果,Callable任务会返回异常或线程执行结果,所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

知识拓展

Runaable任务和Callable任务的相互转换

  • Runaable任务转为Callable任务

    // 将Runaable任务转换为Callable任务,转换后的Callable任务返回值为 null
    Executors.callable(myCallableTask);
    // 将Runaable任务转换为Callable任务,转换后的Callable任务返回值为 result
    Executors.callable(myCallableTask, result);
    
  • Callable任务转为Runaable任务

    注意由于Callable任务没有返回值,所以转换后的Runaable任务的返回值是直接就丢失了

    Runnable myRunnable = () -> {
        try {
            new MyCallableTask().call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    };
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(myRunnable);
    

execute() 和 submit() 的区别

  • execute() :提交任务,没有返回值
  • submit():提交任务,有返回值,可以通过 Future 对象的get()方法获取任务的执行结果

注意get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法的话,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException异常

shutdown() 和 shutdownNow() 的区别

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  • shutdownNow() :关闭线程池,线程池的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List,这个List是正在等待的任务所组成的一个集合。

总结一句话,shutdown() 是比较温柔的,shutdownNow() 是比较粗暴的

isTerminated() 和 isShutdown() 的区别

  • isShutDown(): 判断线程是否关闭,也就是判断线程状态是否为 SHUTDOWN,是该状态返回true,否则false
  • isTerminated(): 判断线程是否完全关闭,也就是判断线程状态是否为 Terminated,是该状态返回true,否则false

其实这个方法应该是比较好区分的,因为一看名字就知道是判断啥的了O(∩_∩)O

参考资料

在此致谢各位大佬的文章,让我汲取到了线程池相关的知识

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知识汲取者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值