Java中的线程池理解

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
最近项目用频繁用到它,有必要总结下。
首先看它的好处:

  1. 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。但是,要做好合理利用线程池,必须对其实现原理了如指掌。

以装修公司类比线程池
以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。
线程池就是程序中的“装修公司”,代劳各种脏活累活。

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 正式工数量
  int maximumPoolSize, // 工人数量上限,包括正式工和临时工
  long keepAliveTime, TimeUnit unit, // 临时工游手好闲的最长时间,超过这个时间将被解雇
  BlockingQueue<Runnable> workQueue, // 排期队列
  ThreadFactory threadFactory, // 招人渠道
  RejectedExecutionHandler handler) // 拒单方式
1 基础知识:

Executors创建线程池
Java中创建线程池很简单, 只需要调用 Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads), 方便的同时也隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。
Execute创建线程池便捷方法列表:

  • newFixedThreadPool(int nThreads) 创建固定大小的线程池
  • newSingleThreadExecutor() 创建只有一个线程的线程池
  • newCachedThreadPool() 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行。

小程序使用这些快捷方法都没什么问题,对于服务器需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor
ThreadPoolExecutor构造方法
Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:

// Java线程池的完整构造函数
public ThreadPoolExecutor(
  int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
  int maximumPoolSize, // 线程数的上限
  long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
                                     // 超过这个时间,多余的线程会被回收。
  BlockingQueue<Runnable> workQueue, // 任务的排队队列
  ThreadFactory threadFactory, // 新线程的产生方式
  RejectedExecutionHandler handler) // 拒绝策略

这些参数中,比较容易引起问题的有 corePoolSize,maxmumPoolSize, workQueue以及handler:

  1. CorePoolSize 和maxumumPoolSize设置不当会影响效率,甚至耗尽线程。
  2. workQueue 设置不当容易导致OOM
  3. handler 设置不当会导致提交任务时抛出异常。

线程池的工作顺序:

If fewer than corePoolSize threads are running, the Exector always prefers adding a new thread rather than queuing. If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread. If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.

2.线程池的使用:
2.1 线程池的创建

new ThreadPoolExecutor(corePoolSize,maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue,handler);

2.2 向线程池提交任务

threadPool.execute(new Runnable() {
@override
public void run(){
    //TODO auto generated method stub
}
});

execute() 方法用于提供不需要返回值的任务。
submit() 方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过futureget()方法来获取返回值,get()方法会阻塞当前线程知道任务完成。而使用get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

Future<Object> future = executor.submit(harReturnValuetask);
try{
    Object s = future.get();
}catch(InterruptedException e){
   //处理中断异常
}catch(ExecutionException e){
   //处理无法执行任务异常
}finally{
   //关闭线程池
     executor.shutdown():
}

2.3 关闭线程池
可以通过调用线程池的shutdownshutdownNow方法来关闭线程池。

2.4 合理地配置线程池
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。

  1. 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。

  2. 任务的优先级:高、中和低。

  3. 任务的执行时间:长、中和短。

  4. 性质的依赖性:是否依赖其他系统资源、如数据库连接。

    性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置N+1个线程的线程池。由于IO密集型任务线程并不是一致在执行任务,则应配置尽可能多的线程,如2*N。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
    优先级不同的任务可以使用优先级队列ProprityBlockingQueue来处理。它可以让优先级高的任务先执行。

  1. 避免使用无界队列。

不要使用Executors.newXXXThreadPool() 快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM, 我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:

ExecutorService executorService = new ThreadPoolExecutor(2,2,
  0,TimeUnit.SECONDS,
  new ArrayBlockingQueue<>(512),//使用有界队列,避免OOM
  new ThreadPoolExecutor.DiscardPolicy()):// 指定拒绝策略
  1. 明确拒绝任务时的行为
    任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,接口定义如下:
 public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

线程池给我们提供了几种常见的拒绝策略:
拒绝策略
在这里插入图片描述
线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。
3. 获取处理结果和异常
线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionExceptionsubmit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:

ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者
        }
    });
    
try {
  Object result = future.get();
} catch (InterruptedException e) {
  // interrupt
} catch (ExecutionException e) {
  // exception in Callable.call()
  e.printStackTrace();
}

2.5 线程池的监控
如果在系统中大量使用线程池,则有必要对线程池进行监控。快速定位问题,可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性。

  • taskCount: 线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount.
  • largestPoolSize:线程池理曾经创建国的最大线程数量。
  • getPoolSize:线程池的数量
  • getActiveCount:获取活动的线程数。
    通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute,afterExecutetermineated方法,也可以在任务执行前,执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。
 protected void beforeExecute(Thread t, Runnable r){}

总结:
Executors为我们提供了构造线程池的便捷方法,对于服务器程序我们应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造方法,避免无界队列可能导致的OOM以及线程个数限制不当导致的线程数耗尽等问题。ExecutorCompletionService提供了等待所有任务执行结束的有效方式,如果要设置等待的超时时间,则可以通过CountDownLatch完成。

参考:
1) https://www.cnblogs.com/CarpenterLee/p/9558026.html
2) <Java并发编程艺术>第9章 Java中的线程池

内容概要:本文围绕“基于分布式模型预测控制的多个固定翼无人机一致性控制”展开,利用Matlab代码实现相关算法的仿真,旨在通过分布式控制策略实现多架固定翼无人机在复杂动态环境中的协同飞行与一致性控制。研究结合模型预测控制(MPC)方法,构建适用于多无人机系统的分布式优化框架,重点解决了通信受限、信息延迟及无中心化指挥条件下的协同稳定性问题。内容涵盖固定翼无人机的动力学建模、分布式MPC优化求解机制、一致性协议设计、通信拓扑结构分析以及仿真验证全过程,确保多机系统在保持队形一致的同时完成协同任务。; 适合人群:具备自动控制理论、无人机系统建模或多智能体协同控制基础,从事智能无人系统、集群控制、自动化与机器人等领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①应用于多无人机协同编队飞行、集群侦察、分布式任务执行等实际工程场景;②为分布式MPC算法在多智能体系统中的一致性控制提供可复现的Matlab仿真案例,推动先进控制理论向工程实践转化;③服务于科研论文复现、算法验证、控制系统课程设计与毕业课题参考。; 阅读建议:建议读者结合文中提供的Matlab代码逐模块运行与调试,重点关注分布式MPC在不同通信拓扑下对一致性收敛性能的影响,并可通过调整预测时域、权重矩阵与噪声参数等方式深化对算法鲁棒性与适应性的理解
内容概要:本文提出了一种基于变分模态分解(VMD)、麻雀搜索算法(SSA)优化与长短期记忆网络(LSTM)相结合的光伏功率预测模型(VMD-SSA-LSTM),旨在提升光伏发电预测的精度与鲁棒性。该方法首先利用VMD对原始非平稳光伏功率序列进行自适应分解,获得一系列具有更稳定特征的本征模态分量(IMFs),有效降低数据复杂性与噪声干扰;随后引入麻雀搜索算法(SSA)对LSTM网络的关键超参数(如学习率、隐层节点数等)进行全局寻优,克服传统试凑法效率低、易陷入局部最优的问题,显著提升模型收敛速度与泛化能力;最后,构建多个LSTM子模型分别预测各模态分量,并将结果重构得到最终的光伏功率预测值。该混合模型充分融合了VMD在信号预处理中的优异分解性能、SSA在参数优化中的高效搜索能力以及LSTM在捕捉时间序列长期依赖关系上的强大建模优势,实现了对复杂气象因素影响下光伏出力波动的高精度拟合与预测。; 适合人群:具备一定电力系统、新能源发电或时间序列预测基础知识,熟悉MATLAB编程环境,从事光伏功率预测、智能电网调度、可再生能源集成、负荷预测等领域研究的科研人员、工程技术人员及高校研究生。; 使用场景及目标:①应用于光伏电站的短期与超短期功率预测,为电网安全调度、电力市场交易、储能系统配置及需求侧响应提供精准数据支撑;②解决传统单一预测模型(如ARIMA、BPNN、单一LSTM)在处理非平稳、强波动性光伏数据时存在的精度不足、稳定性差等问题;③为风电、负荷等其他非平稳时序预测问题提供一种有效的“分解-优化-预测”混合建模范式与技术实现路径。; 阅读建议:建议读者结合文中提供的完整MATLAB代码,深入理解VMD信号分解、SSA优化算法流程及LSTM网络构建的每一个技术环节,通过实际历史数据进行模型复现与对比实验(如与VMD-LSTM、SSA-LSTM等模型比较),掌握参数调优技巧与模型性能评估方法,从而真正掌握该先进混合预测模型的核心思想与应用精髓。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值