Java线程池及业务实践

1.什么是线程池?

线程池是一种用于管理和复用线程的机制。

线程池的核心思想是预先创建一定数量的线程,并把它们保存在线程池中,当有任务需要执行时,线程池会从空闲线程中取出一个线程来执行该任务。任务执行完毕后,线程不是被销毁,而是返还给线程池,可以立即或稍后被再次用来执行其他任务。这种机制可以避免因频繁创建和销毁线程而带来的性能开销,同时也能控制同时运行的线程数量,从而提高系统的性能和资源利用率。

线程池的主要组成部分包括工作线程、任务队列、线程管理器等。线程池的设计有助于优化多线程程序的性能和资源利用,同时简化了线程的管理和复用的复杂性。

2.线程池有什么好处?

减少线程创建和销毁的开销,线程的创建和销毁需要消耗系统资源,线程池通过复用线程,避免了对资源的频繁操作,从而提高系统性能;

控制和优化系统资源利用,线程池通过控制线程的数量,可以尽可能地压榨机器性能,提高系统资源利用率;

提高响应速度,线程池可以预先创建线程且通过多线程并发处理任务,提升任务的响应速度及系统的并发性能;

3.Java线程池的实现原理

1).类继承关系:Java线程池的核心实现类是ThreadPoolExecutor,

ThreadPoolExecutor的部分核心方法:

execute(Runnable r):没有返回值,仅仅是把一个任务提交给线程池处理

submit(Runnable r):返回值为Future类型,当任务处理完毕后,通过Future的get()方法获取返回值时候,得到的是null

submit(Runnable r,Object result):返回值为Future类型,当任务处理完毕后,通过Future的get()方法获取返回值时候,得到的是传入的第二个参数result

shutdown():关闭线程池,不接受新任务,但是等待队列中的任务处理完毕才能真正关闭

shutdownNow():立即关闭线程池,不接受新任务,也不再处理等待队列中的任务,同时中断正在执行的线程

setCorePoolSize(int corePoolSize):设置核心线程数

setKeepAliveTime(long time, TimeUnit unit):设置线程的空闲时间

setMaximumPoolSize(int maximumPoolSize):设置最大线程数

setRejectedExecutionHandler(RejectedExecutionHandler rh):设置拒绝策略

setThreadFactory(ThreadFactory tf):设置线程工厂

beforeExecute(Thread t, Runnable r):任务执行之前的钩子函数,这是一个空函数,使用者可以继承ThreadPoolExecutor后重写这个方法,实现其中的逻辑

afterExecute(Runnable r, Throwable t):任务执行之后的钩子函数,这是一个空函数,使用者可以继承ThreadPoolExecutor后重写这个方法,实现其中的逻辑

2).线程池的状态

RUNNING:线程池一旦被创建,就处于RUNNING状态,任务数为0,能够接收新任务,对已排队的任务进行处理。

SHUTDOWN:不接收新任务,但能处理已排队的任务。当调用线程池的shutdown()方法时,线程池会由RUNNING转变为SHUTDOWN状态。

STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。当调用线程池的shutdownNow()方法时,线程池会由RUNNING或SHUTDOWN转变为STOP状态。

TIDYING:当线程池在SHUTDOWN状态下,任务队列为空且执行中任务为空,或者线程池在STOP状态下,线程池中执行中任务为空时,线程池会变为TIDYING状态,会执行terminated()方法。这个方法在线程池中是空实现,可以重写该方法进行相应的处理。

TERMINATED:线程池彻底终止。线程池在TIDYING状态执行完terminated()方法后,就会由TIDYING转变为TERMINATED状态。

3).线程池的执行流程

4)线程池的核心线程可以回收吗?

答案:ThreadPoolExecutor默认不回收核心线程,但是提供了allowCoreThreadTimeOut(boolean value)方法,当参数为true时,可以在达到线程空闲时间后,回收核心线程,在业务代码中,如果线程池是周期性的使用,可以考虑将该参数设置为true;

5).线程池在提交任务前,可以提前创建线程吗?

答案:ThreadPoolExecutor提供了两个方法:

prestartCoreThread():启动一个线程,等待任务,如果核心线程数已达到,这个方法返回false,否则返回true;

prestartAllCoreThreads():启动所有的核心线程,返回启动成功的核心线程数 ;

通过这种设置,可以在提交任务前,完成核心线程的创建,从而实现线程池预热的效果;

  1. 线程池在业务中的最佳实践

线程池的核心参数

1.corePoolSize:  核心线程数

2.maximumPoolSize:  最大线程数

3.keepAliveTime:  线程的空闲时间

4.unit:  空闲时间的单位(秒、分、小时等等)

5.workQueue:  等待队列

6.threadFactory:  线程工厂

7.handler:  拒绝策略

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异

ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

ThreadPoolExecutor.CallerRunsPolicy:由调用线程直接处理该任务(可能为主线程Main),保证每个任务执行完毕

1).如何选择合适的线程池参数

①根据任务场景选择​

CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间;

I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N;

②根据线程池用途选择​

一:快速响应用户请求的需求​

比如说用户查询商品详情页,会涉及查询商品关联的一系列信息如价格、优惠、库存、基础信息等,站在用户体验的角度,希望商详页的响应时间越短越好,此时可以考虑使用线程池并发地查询价格、优惠、库存等信息,再聚合结果返回,降低接口总rt。这种线程池用途追求的是最快响应速度,所以可以考虑不设置队列去缓冲并发任务,而是尽可能设置更大的corePoolSize和maxPoolSize;

二:快速处理批量任务的需求​

比如说项目中在对接渠道同步商品供给时,需要查询大量的商品数据并同步给渠道,此时可以考虑使用线程池快速处理批量任务。这种线程池用途关注的是如何使用有限的机器资源,尽可能地在单位时间内处理更多的任务,提升系统吞吐量,所以需要设置阻塞队列缓冲任务,并根据任务场景调整合适的corePoolSize;

2).如何正确地创建线程池对象

使用Executors创建特定的线程池,线程池参数比较固定,不推荐使用。​

Executors是一个java.util.concurrent包中的工具类,可以方便的为我们创建几种特定参数的线程池。

FixedThreadPool:具有固定线程数量的线程池,无界阻塞队列;

CachedThreadPool:线程数量可以动态伸缩的线程池,最大线程数为Integer.MAX_VALUE

SingleThreadPool:单个线程的线程,核心线程数和最大线程数都是1,无界阻塞队列...

推荐使用饿汉式的单例模式创建线程池对象,支持灵活的参数配置,在类加载阶段即完成线程池对象的创建,且只会实例化一个对象,再封装统一的获取线程池对象的方法,暴露给业务代码使用,

3).局部变量定义的线程池对象在方法结束后可以被垃圾回收吗?

  1. public static void main(String[] args) {
  2.     test1();
  3.     test2();
  4. }
  5. public static void test1(){
  6.     Object obj = new Object();
  7.     System.out.println("方法一执行完成");
  8. }
  9. public static void test2(){
  10.     ExecutorService executorService = Executors.newFixedThreadPool(10);
  11.     executorService.execute(new Runnable() {
  12.         @Override
  13.         public void run() {
  14.             System.out.println("方法二执行完成");
  15.         }
  16.     });
  17. }

上图代码,obj是定义在test1()方法体内的局部变量,正常来说局部变量会保存在栈中,随着方法的结束,栈帧出栈,栈帧中的局部变量也会销毁,此时没有任何变量指向堆内存中的new Object()对象,所以堆中的new Object()对象可以被垃圾回收;executorService同样也是定义在方法体中的局部变量,但在方法结束后,线程池中还存在活跃的线程,根据GC Roots可达性分析原理,可作为GC Roots的对象有:

虚拟机栈(栈帧中的本地变量表)中引用的对象;

方法区中的类静态属性引用的对象;

方法区中常量引用的对象;

本地方法栈中JNI(即一般说的Native方法)引用的对象;

正在运行的线程;

所以在test2()方法执行结束后,线程池中的线程会进入getTask()的阻塞状态,但依然是活跃线程,此时堆中的线程池对象依然GC Roots可达,所以不会被垃圾回收;

这个问题带来两个启发:

①不要在代码中使用局部变量定义线程池对象,这样不仅会导致频繁创建线程池对象,违背了线程复用的设计原则,还有可能造成局部变量的线程池对象无法及时垃圾回收的内存泄漏问题;

②业务代码中,优先定义静态内部类而不是非静态内部类,可以有效防止内存泄露的风险;

4).合理选择submit()和execute()方法

execute(Runnable r):没有返回值,仅仅是把一个任务提交给线程池处理,轻量级方法,适用于处理不需要返回结果的任务;

submit(Runnable r):返回值为Future类型,future可以用来检查任务是否已经完成,获取任务的结果等,适用于需要处理返回结果的任务;

5).如果线程池中执行任务的线程异常,发生异常的线程会销毁吗?其他任务还能正常执行吗?

线程池中执行任务的线程异常,并不会影响其他任务的执行,而且execute()提交任务,直接打印了异常信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值