可以去非菲律宾做游戏网站吗,软件定制开发 报价,网站设计师待遇,隧道建设网站无法登录1. 引言
在当今高度并发的软件开发环境中#xff0c;有效地管理线程是确保程序性能和稳定性的关键因素之一。Java线程池作为一种强大的并发工具#xff0c;不仅能够提高任务执行的效率#xff0c;还能有效地控制系统资源的使用。 本文将深入探讨Java线程池的原理、参数配置…1. 引言
在当今高度并发的软件开发环境中有效地管理线程是确保程序性能和稳定性的关键因素之一。Java线程池作为一种强大的并发工具不仅能够提高任务执行的效率还能有效地控制系统资源的使用。 本文将深入探讨Java线程池的原理、参数配置、自定义以及实际应用。通过理解这些关键概念开发者将能够更好地应对不同的并发场景优化程序的执行效率。 首先我们将介绍线程池的基本概念解释它在并发编程中的作用和优势。随后我们将深入研究Java线程池的工作原理剖析其在任务提交、执行和线程管理方面的内部机制。
2. Java线程池的基础概念
在并发编程中线程池是一种重要的设计模式它能够有效地管理和复用线程提高程序的性能和资源利用率。Java线程池作为Java并发包java.util.concurrent的一部分为开发者提供了方便、高效的多线程处理方式。同时在阿里巴巴开发规范中强制要使用线程池去提供线程不允许在代码中显示的创建线程。
2.1 什么是线程池
线程池是由一组线程组成的线程队列它们在程序启动时就被创建并一直存在。这些线程可被用来执行提交到线程池的各种任务从而避免为每个任务都创建新线程。这种机制能够降低线程创建和销毁的开销提高系统性能。
2.2 线程池的工作原理
线程池的工作原理基于任务队列和线程管理机制。当任务被提交到线程池时线程池会选择合适的线程来执行任务。如果核心线程数未达到上限新任务可能会导致新线程的创建。如果核心线程已满任务将被放入任务队列等待执行。当任务队列也已满而同时线程数未达到最大线程数新的任务将创建临时线程来执行。
2.3 线程池的优势
使用线程池的优势主要体现在以下几个方面 减少资源消耗 线程的创建和销毁是有开销的线程池通过复用线程减少了这些开销。 提高响应速度 由于线程池中的线程一直存在可以更迅速地响应任务的到来。 避免系统过载 控制线程数量防止系统因过多线程而过载。
3. Java线程池的工作原理
Java线程池的工作原理涉及线程的创建、任务的提交与执行以及对线程的管理。深入理解这些机制对于优化并发程序至关重要。
3.1 线程池的创建与初始化
在程序启动时线程池被创建并初始化。这一过程包括设置线程池的基本参数如核心线程数、最大线程数、任务队列等。核心线程数是线程池中一直存活的线程数量而最大线程数则是线程池允许创建的最大线程数量。例如创建一个固定核心线程数的线程
ExecutorService executorService Executors.newFixedThreadPool(corePoolSize);其中参数corePoolSize即为核心线程数
3.2 任务的提交与执行
任务提交到线程池后线程池会根据一定的策略选择线程来执行任务。首先线程池会检查核心线程是否已满如果未满新的任务可能会导致新线程的创建。如果核心线程已满任务将被放入任务队列。 在Java线程池中任务的提交与执行有两个主要的方法submit和execute。这两种方法有一些区别主要体现在返回值、异常处理和任务包装上。
3.2.1 submit方法
submit方法用于提交实现了Callable接口的任务它可以返回一个Future对象通过该对象可以获取任务执行的结果取消任务等。submit方法还可以接受实现了Runnable接口的任务但它无法获取任务的执行结果。submit方法在ExecutorService中定义的并定义了三种重载方式
T FutureT submit(CallableT task);T FutureT submit(Runnable task, T result);Future? submit(Runnable task);具体使用如下
class MyCallable implements CallableInteger{ Override public Integer call() throws Exception { return null; }
}// 提交callable任务可以拿到返回值
FutureInteger future1 executorService.submit(new MyCallable());class MyRunnable implements Runnable { Override public void run() { }
}
FutureVoid future2 executorService.submit(new MyRunnable(), null);
Future? future3 executorService.submit(new MyRunnable());主要特点
返回一个Future对象可通过Future的get()方法可以获取到线程执行的返回值get()方法是同步的执行get()方法时如果线程还没执行完会同步等待直到线程执行完成。可以接受Callable和Runnable类型的任务。执行Runnable和Callable的任务时run()/call()方法没显式抛出异常。
3.2.2 execute方法
execute方法用于提交实现了Runnable接口的任务它没有返回值因此无法获取任务的执行结果。如果任务执行过程中抛出了异常线程池会捕获并记录该异常但无法通过execute方法获知。execute方法是在线程池的顶级接口Executor中定义的而且只有这一个接口。
public interface Executor { void execute(Runnable command);
}使用
executorService.execute(() - { // 具体业务逻辑
});主要特点
没有返回值无法获取任务的执行结果。只能接受Runnable类型的任务。
总的来说submit方法更加灵活适用于更多场景而execute方法更加简单适用于只关心任务执行而不需要获取结果的场景。在实际应用中根据具体需求选择合适的方法。如果需要获取任务的执行结果、取消任务等建议使用submit方法。只是执行任务而不关心返回值可以使用execute方法。
4. 线程池的参数以及配置
Java线程池的性能和行为可以通过一系列参数进行调整以满足不同的并发需求。ThreadPoolExecutor中提供的构造器如下 4.1 七大参数
4.1 核心线程数Core Pool Size
核心线程数是线程池中一直存活的线程数量即使它们处于空闲状态。这些线程用于执行提交到线程池的任务。通过合理设置核心线程数可以在系统负载增加时迅速响应任务。
4.2 最大线程数Maximum Pool Size
最大线程数定义了线程池中允许创建的最大线程数量。当核心线程都在执行任务而新任务仍然到来时线程池会创建新线程直到达到最大线程数。超过最大线程数的任务会被拒绝。
4.3 线程存活时间Keep Alive Time
线程存活时间指的是非核心线程在空闲状态下的最大存活时间。当线程池中线程数量超过核心线程数时空闲的非核心线程在经过一定时间后会被终止从而释放系统资源。
4.4 TimeUnit
keepAliveTime的单位(ms、s…)
4.5 工作队列Work Queue
工作队列用于存放等待执行的任务。不同类型的队列对线程池的行为有重要影响例如有界队列和无界队列。有界队列在任务数达到上限时会触发拒绝策略。
4.6 ThreadFactory
线程池中生成线程的工厂。默认使用默认工厂Executors.defaultThreadFactory()。但是实际使用时建议使用Guava的ThreadFactory自定义线程的名字方便排查线程问题阿里开发规范中也建议这么做。如下
ThreadFactory threadFactory new ThreadFactoryBuilder().setNameFormat(mythread-pool-).build();4.7 拒绝策略Rejected Execution Policy
拒绝策略定义了当工作队列满并且当前工作的线程数等于最大线程数时后续再提交的任务如何处理。例如可以选择抛弃任务、抛出异常或在调用线程中直接执行。Java线程池提供了几种常见的拒绝策略
AbortPolicy默认策略 直接抛出RejectedExecutionException阻止系统继续接受新任务保持原有状态。
new ThreadPoolExecutor.AbortPolicy();CallerRunsPolicy 将任务返回给调用者由调用线程直接执行。
new ThreadPoolExecutor.CallerRunsPolicy();DiscardPolicy 直接丢弃无法处理的任务不抛出异常。
new ThreadPoolExecutor.DiscardPolicy();DiscardOldestPolicy 当任务被拒绝添加时会抛弃任务队列中最旧的任务也就是最先加入队列的再把这个新任务添加进去。
new ThreadPoolExecutor.DiscardOldestPolicy();4.2 线程池提交任务执行流程
4.2.1 执行流程 4.2.2 实例讲解
某银行柜台共有5个窗口Maximum Pool Size平时常开2个窗口办理业务Core Pool Size银行大厅摆了5个椅子Work Queue供客户等待。银行规定当常开的窗口都在办理业务并且大厅椅子上都坐满了客户那么另外3个不常开的窗口也要打开办理业务。如果这3个窗口也都全部在办理业务后面继续来银行办理业务的客户银行将拒绝办理。如果某个员工空闲下并且超过了5Keep Alive Time秒钟TimeUnit那么他就可以关闭窗口去休息。但是必须保留2个常开的窗口。 我们先按照上述流程创建一个线程池
// 推荐使用Guava的ThreadFactory构建ThreadFactory自定义线程名称 方便后续排查问题
ThreadFactory threadFactory new ThreadFactoryBuilder().setNameFormat(mythread-pool-).build();
// 定义号线程
ExecutorService executorService new ThreadPoolExecutor( // 核心线程数即2个常开窗口 2, // 最大的线程数银行所有的窗口 5, // 空闲时间 5, TimeUnit.SECONDS, // 工作队列 new LinkedBlockingQueue(5), // 线程工厂 threadFactory, // 拒绝策略 new ThreadPoolExecutor.AbortPolicy()
);1初始状态下只有2个窗口为2个客户办理业务。 2在客户1,客户2办理业务或者说常开窗口一直都有客户在办理业务此时陆续有客户进来需要在银行大厅的椅子上等待。 3直到大厅的椅子都坐满。 4此时如果在所有的窗口都在办理业务大厅椅子坐满如果再来一个客户将开启3/4/5的窗口 5此时如果在所有的窗口都在办理业务大厅椅子坐满还从外面再来2个客户办理业务那么就需要把剩下的窗口都要打开去办理业务。 6此时如果再来1个客户就会按照线程池定义的拒绝策略去执行比如我们设置策略为AbortPolicy就会抛出异常。 4.3 线程池参数配置
线程池的配置参数在实际应用中需要根据具体的业务场景和性能需求进行巧妙调整。这就好比在日常生活中如果有一个任务需要三人协同完成但却有六人前来参与就会造成三人的资源浪费反之若只安排两人协作可能会超负荷而不切实际。因此在线程池参数配置时过小或过大都会带来问题。
当线程池数量设置过小时面对大量同时到达的任务或请求可能导致这些任务在任务队列中排队等待执行。甚至在任务队列达到最大容量时无法处理额外的任务或者导致任务队列积压有可能引发内存溢出OOM问题。这明显是一个问题因为CPU资源无法得到充分利用。
相反若线程数量设置过大大量线程可能会同时争夺CPU资源导致频繁的上下文切换从而增加线程的执行时间影响整体执行效率。因此在线程池配置中需要平衡线程数量以满足高并发场景下的任务处理需求同时避免不必要的资源争夺和上下文切换以保障系统的稳定性和性能。
并没有一个通用的标准来设置参数因此需要结合实际实战经验、业务需求以及服务器资源的状况灵活而合理地进行参数配置。最终合适的配置才是最为优越的选择。
当然也有一个简单而广泛适用的公式可以用于确定线程池中的线程数
CPU 密集型任务N1 对于消耗主要是CPU资源的任务可以将线程数设置为NCPU核心数1。额外的一个线程用于防止线程偶发的缺页中断或其他原因导致的任务暂停防止空闲时间的浪费。一旦任务暂停多出来的一个线程可以充分利用CPU的空闲时间。 I/O 密集型任务2N 对于主要涉及I/O交互的任务系统会在大部分时间内处理I/O而在线程处理I/O的时间段内不会占用CPU。因此在I/O密集型任务中可以配置更多的线程具体计算方法是2N。
那我们如何判断任务是CPU密集型还是IO密集型呢简而言之CPU密集型任务主要利用CPU计算能力例如对内存中大量数据进行排序。而IO密集型任务涉及网络读取、文件读取等其特点是CPU计算耗费的时间相对较少大部分时间花在等待IO操作完成上。
但是我们在实际的业务中会发现我们一个服务器上可能跑多种类型的业务不太好判断到底是CPU密集任务还是IO密集型。我们可以根据监控服务线程池资源利用情况结合业务场景动态配制合理参数。这里我们就不得不提一下美团的线程池参数动态化配置Java线程池实现原理及其在美团业务中的实践 - 美团技术团队。
5. 线程池的使用
日常开发中我们可以通过Executors去创建线程池例如 1newFixedThreadPool()
ExecutorService executorService1 Executors.newFixedThreadPool(2);创建固定线程数的线程池核心线程数等于最大线程数此时keepAliveTime失效 。但是他的工作队列的长度为Integer.MAX_VALUE。可能会导致堆积大量的请求导致OOM。
2newSingleThreadExecutor()
ExecutorService executorService2 Executors.newSingleThreadExecutor();创建单线程的线程池即核心线程数等于最大线程数均等于1keepAliveTime失效 。但是他的工作队列的长度为Integer.MAX_VALUE。可能会导致堆积大量的请求导致OOM。
3newCachedThreadPool()
ExecutorService executorService3 Executors.newCachedThreadPool();创建一个核心线程数等于0并且允许创建的最大线程数等于Integer.MAX。keepAliveTime为60秒。可能会造成创建大量的线程从而导致OOM。
4newScheduledThreadPool()
ExecutorService executorService4 Executors.newScheduledThreadPool(2);创建一个允许最大线程数等于Integer.MAX但是他使用的阻塞工作队列是DelayedWorkQueue。DelayedWorkQueue的核心数据结构是二叉最小堆的优先队列队列满时会自动扩容。所以最大线程数没有意义线程池中永远会保持至多有核心线程数个工作线程正在运行。
注意 以上创建线程池的方法可以做自己Demo使用不应该用在项目中。在阿里巴巴代码规范中不支持使用这种方式去创建支持手动创建线程池。 6.总结
Java线程池是多线程编程中的重要工具能够有效管理和复用线程提高系统性能和资源利用率。本文深入探讨了线程池的基础概念、工作原理、参数配置、自定义以及使用示例并强调了注意事项。 通过了解线程池的工作原理开发者可以更好地配置线程池以适应不同的并发需求。自定义线程池则使得线程池更灵活地适应特定业务场景。在实际应用中要谨慎选择线程池类型、合理配置参数、注意任务的生命周期和线程安全等问题以确保系统的稳定性和性能。
参考文献
1、Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 (meituan.com) 2、《Java并发编程实战》
本文已收录于我的个人博客码农Academy的博客专注分享Java技术干货包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等