Java线程池让使用线程变得更加高效

简介: 使用一个线程需要经过创建、运行、销毁三大步骤,如果业务系统每个线程都要经历这个过程,那会带来过多不必要的资源消耗。线程池就是为了解决这个问题而生,需要时就从池中拿取,使用完毕就放回去,池化思想通过复用对象大大提高了系统的性能。线程池、数据库连接池、对象池等都采用了池化技术,下面我们就来学习下线程池的核心知识、面试重点~

使用一个线程需要经过创建、运行、销毁三大步骤,如果业务系统每个线程都要经历这个过程,那势必带来过多不必要的资源消耗。线程池就是为了解决这个问题而生,需要时就从池中拿取,使用完毕就放回去,池化思想通过复用对象大大提高了系统的性能。线程池、数据库连接池、对象池等都采用了池化技术,下面我们就来学习下线程池的核心知识、面试重点~

1. 线程池使用

1.1 如何配置线程池大小

面试官:你说下线程池的大小要怎么配置?

这个问题要看业务系统执行的任务更多的是计算密集型任务,还是I/O密集型任务。大家可以从这两个方面来回答面试官。

(1)如果是计算密集型任务,通常情况下,CPU个数为N,设置N + 1个线程数量能够实现最优的资源利用率。因为N + 1个线程能保证至少有N个线程在利用CPU,提高了CPU利用率;同时不设置过多的线程也能减少线程状态切换所带来的上下文切换消耗。

(2)如果是I/O密集型任务,线程的主要等待时间是花在等待I/O操作上,另外就是计算所花费的时间。一般可以根据这个公式得出线程池合适的大小配置。
$$ 线程池大小 = CPU数量 * CPU期望的利用率 * (1 + IO操作等待时间/CPU计算时间) $$

1.2 创建线程池

面试官:那线程池怎么创建?

可以使用ThreadPoolExecutor自定义创建线程池,这也是创建线程池推荐的创建方式。

public ThreadPoolExecutor(int corePoolSize, // 要保留在池中的线程数
                          int maximumPoolSize, // 池中允许的最大线程数
                          long keepAliveTime, // 当线程数大于corePoolSize时,多余的空闲线程在终止之前等待新任务的最长时间
                          TimeUnit unit, // 时间单位
                          BlockingQueue<Runnable> workQueue, // 在执行任务之前用于保存任务的队列
                          ThreadFactory threadFactory) {
   
    // 执行程序创建新线程时使用的工厂
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

另外Executors类也提供了一些静态工厂方法,可以用来创建一些预配置的线程池。

newFixedThreadPool可以设置线程池的固定线程数量。

public static ExecutorService newFixedThreadPool(int nThreads) {
   
   
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

newSingleThreadExecutor可以让线程按序执行,适用于需要确保所有任务按序执行的场景。

public static ExecutorService newSingleThreadExecutor() {
   
   
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

大家看下以下源码,newCachedThreadPool的线程数没有上限限制,同时空闲线程的存活时间是60秒。newCachedThreadPool更适合系统负载不太高、线程执行时间短的场景下,因为线程任务不需要经过排队,直接交给空闲线程就可以。

public static ExecutorService newCachedThreadPool() {
   
   
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

newScheduledThreadPool可以安排任务在给定的延迟后运行,或者定期执行。

public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
   
   
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

1.3 预配置线程池弊端

面试官:你说的这些预配置线程池会有什么问题?

小伙伴要记得上述静态工厂方法在使用过程中可能会出现OOM内存溢出的情况。

  1. newFixedThreadPoolnewSingleThreadExecutor:因为线程池指定的请求队列类型是链表队列LinkedBlockingQueue<Runnable>(),故允许的请求队列长度是无上限的,可能会出现OOM内存溢出。
  2. newCachedThreadPoolnewScheduledThreadPool:线程池指定的线程数上限是Integer.MAX_VALUE,故允许创建的线程数量是无上限的Integer.MAX_VALUE,可能会出现OOM内存溢出。

1.3 Spring创建线程池

面试官:你们项目线程池用的这种创建方式?

一般Spring工程创建线程池不直接使用ThreadPoolExecutor。

Spring框架提供了以Bean形式来配置线程池的ThreadPoolTaskExecutor类,ThreadPoolExecutor类的底层实现还是基于JDK的ThreadPoolExecutor。

# 示例代码
@Bean(name = "testExecutor")
public ThreadPoolTaskExecutor testExecutor() {
   
   
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // 配置核心线程数
    executor.setCorePoolSize();
    // 配置最大线程数
    executor.setMaxPoolSize();
    // 配置队列大小
    executor.setQueueCapacity();
    executor.initialize();
    return executor;
}

2. 线程池拒绝策略

面试官:线程池请求队列满了,有新的请求进来怎么办?

大家如果有看ThreadPoolExecutor源码就知道,ThreadPoolExecutor类实现了setRejectedExecutionHandler方法,顾名思义意思是设置拒绝执行处理程序。

# ThreadPoolExecutor源码
/**
* Sets a new handler for unexecutable tasks. // 为无法执行的任务设置新的处理程序
*
* @param handler the new handler
* @throws NullPointerException if handler is null
* @see #getRejectedExecutionHandler
*/
public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {
   
   
    if (handler == null)
        throw new NullPointerException();
    this.handler = handler;
}

该方法可以为线程池设置拒绝策略,目前JDK8一共有四种拒绝策略,也对应入参RejectedExecutionHandler的四种子类实现。

在这里插入图片描述

  1. AbortPolicy:默认的拒绝策略,直接抛出RejectedExecutionException异常。
  2. CallerRunsPolicy:直接在execute方法的调用线程中运行被拒绝的任务。
  3. DiscardPolicy:直接丢弃被拒绝的任务。
  4. DiscardOldestPolicy:丢弃最旧的未处理请求,然后重试execute 。

另外如果线程池拒绝策略设置为DiscardOldestPolicy,线程池的请求队列类型最好不要设置为优先级队列PriorityBlockingQueue。因为该拒绝策略是丢弃最旧的请求,也就意味着丢弃优先级最高的请求

3. 线程工厂的作用

面试官:线程池的入参ThreadFactory有什么作用吗?

ThreadFactory定义了创建线程的工厂,回答这个问题我们就要结合实际场景了。

ThreadFactory线程工厂能够为线程池里每个线程设置名称、同时设置自定义异常的处理逻辑,可以方便我们通过日志来定位bug的位置。

以下是一个代码示例。

@Slf4j
public class CustomGlobalException {
   
   
    public static void main(String[] args) {
   
   
        ThreadFactory factory = r -> {
   
   
            String threadName = "线程名称";
            Thread thread = new Thread(r, threadName);
            thread.setUncaughtExceptionHandler((t, e) -> {
   
   
                log.error("{}执行了自定义异常日志", threadName);
            });
            return thread;
        };
        ExecutorService executor = new ThreadPoolExecutor(6,
                6,
                0,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(66),
                factory);

        executor.execute(() -> {
   
   
            throw new NullPointerException();
        });
        executor.shutdown();
    }
}

控制台打印:2024-04-26 22:04:45[ ERROR ]线程名称执行了自定义异常日志

🌱以【面试官面试】形式覆盖Java程序员所需掌握的Java核心知识、面试重点,本博客收录在我开源的《Java学习指南》中,会一直完善下去,希望收到大家的 ⭐ Star ⭐支持,这是我创作的最大动力: https://github.com/hdgaadd/JavaGetOffer

未完待续。。。

创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️

相关文章
|
5天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
35 6
|
18天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
14天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
14天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
38 3
|
15天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
18天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
26 2
|
18天前
|
监控 Java 开发者
Java线程管理:守护线程与本地线程的深入剖析
在Java编程语言中,线程是程序执行的最小单元,它们可以并行执行以提高程序的效率和响应性。Java提供了两种特殊的线程类型:守护线程和本地线程。本文将深入探讨这两种线程的区别,并探讨它们在实际开发中的应用。
25 1
|
20天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
54 1
|
7月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
|
4月前
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
71 1