找回密码
 立即注册
首页 业界区 业界 Java并发编程(5)

Java并发编程(5)

殳世英 14 小时前
线程池

1、什么是线程池?


  • 管理线程,避免增加创建线程和销毁线程的资源消耗:线程也是一个对象,创建一个对象要类加载,销毁一个对象要走GC垃圾回收流程,都是有资源开销的。
  • 提高响应速度:对比普通的做法,是重新创建一个线程执行,要慢很多。
  • 重复利用:线程用完再放回池子,可以达到重复利用的效果,节省资源。
2、说说工作中线程池的应用

   可以讲在拼团中怎么用的。
3、说一下线程池的工作流程


  •  线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的(因为可以选择)。
  • 当调用excute()方法添加一个任务时,线程池会做如下判断:

    • 若正在运行的线程数量小于corePoolSize,则马上创建线程运行这个任务。
    • 若正在运行的线程数量大于或等于corePoolSize,则将这个任务放入队列。
    • 若这时队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻执行这个任务。
    • 若队列满了,且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会根据拒绝策略来应对处理。

  • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  • 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,若当前正在运行的线程数大于corePoolSize,那么这个线程会被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
为什么不直接填满到maximyumPoolSize:

  • 内存占用大、上下文切换频繁消耗CPU、同步开销大。
  • 动态扩展的方案只在高峰期临时创建更多线程,是一种“按需分配”的原则:先用少量核心线程处理常规负载,队列缓冲短期突发请求,实在处理不过来才会额外创建线程,空闲时回收多余线程。是为了在响应速度和资源效率之间取得最佳平衡。
4、线程池主要参数有哪些?

<ul>corePoolSize:初始化线程池中核心线程数,当线程池中线程数小于corePoolSize时,系统默认是添加一个任务才从创建一个线程。
maximumPoolSize:表示允许的最大线程数 = 非核心线程数+ 核心线程数。当工作队列也满了,但线程池中总线程数 submit(Runnable task) Future submit(Callable task)特点:可以提交 Runnable 或 Callable 任务;返回 Future 对象,可以获取结果、取消任务、捕获异常;如果任务抛出异常,异常会被封装在 Future 中,只有调用 get() 时才会抛出 ExecutionException。[/code]6、线程池怎么关闭

(1) shutdown()

  • 行为:停止接收新任务;已提交的任务(正在执行的 + 队列中等待的)会执行完。
  • 特点:平滑关闭,常用方式。
  • 注意:调用后线程池状态会变为 SHUTDOWN,不会立刻终止。
(2) shutdownNow()

  • 行为:停止接收新任务,清空队列中的等待任务,并尝试中断正在执行的任务。
  • 返回值:未执行的任务列表。
  • 特点:强制关闭,风险较大(可能造成数据不一致)。
  • 注意:线程是否真正中断,取决于任务代码是否响应中断。
(3) awaitTermination(timeout, unit)

  • 用法:
     pool.shutdown(); if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { pool.shutdownNow(); }
  • 作用:在调用 shutdown() 后等待一段时间,如果线程池没有在指定时间内终止,再调用 shutdownNow() 强制关闭。
  • 特点:常用于“优雅关闭”+“兜底强制关闭”组合。
(4) isShutdown() / isTerminated()

  • isShutdown():调用 shutdown() 或 shutdownNow() 后返回 true。
  • isTerminated():所有任务执行结束,线程池完全终止时返回 true。
  • 可配合轮询检测线程池状态,做收尾逻辑。
7、线程池的线程数应该怎么配置

(1) 计算密集型任务

  • 特点:大部分时间消耗在 CPU 运算上。
  • 配置:CPU核数 + 1。
  • 理由:核心数保证最大并行度,额外的 +1 用来应对偶尔的线程挂起(页缺失、GC、上下文切换)。
(2) IO 密集型任务

  • 特点:大部分时间等待 IO(数据库、网络、磁盘等)。
  • 配置:CPU核数 * 2(甚至更高,取决于 IO 等待比例)。
  • 理由:IO 期间线程空闲,更多线程可提升 CPU 利用率。
(3) 混合型任务

  • 特点:既包含计算,又包含 IO。
  • 配置方法:

    • 比例分解法:根据任务中 IO 和计算占比,估算最优线程数。
    • 分池法:将任务拆分为计算型和 IO 型,分别放入不同线程池,避免资源竞争。

(4) 经验公式(IO 密集型可参考)
线程数=CPU核心数×(1+IO时间计算时间)线程数 = CPU核心数 \times (1 + \frac{IO时间}{计算时间})线程数=CPU核心数×(1+计算时间IO时间​)

  • 如果 IO 占比高,线程数可比 CPU 数大得多。
8、有哪几种常见的线程池

(1) newFixedThreadPool

  • 核心线程数 = 最大线程数 = 固定值
  • keepAliveTime = 0,线程不会被回收。
  • 阻塞队列 = LinkedBlockingQueue(无界队列)
  • 特点:线程数固定,任务多了就排队。
  • 适用场景:适合 CPU 密集型任务,线程数可设置为 CPU 核心数 或略大。
(2) newCachedThreadPool

  • 核心线程数 = 0;最大线程数 = Integer.MAX_VALUE(相当于无限大)。
  • keepAliveTime = 60s,空闲线程超过时间会回收。
  • 阻塞队列 = SynchronousQueue(没有容量,任务直接交给线程执行)
  • 特点:任务多就创建新线程,空闲一段时间自动回收。
  • 适用场景:适合 大量并发的短期小任务,但要注意可能导致线程数过多,风险较大。
(3) newSingleThreadExecutor

  • 核心线程数 = 最大线程数 = 1
  • keepAliveTime = 0
  • 阻塞队列 = LinkedBlockingQueue(无界队列)
  • 特点:单线程串行执行,保证任务按照提交顺序(FIFO)执行。
  • 适用场景:适合 需要顺序执行任务、或全局只需要一个后台线程的场景。
(4) newScheduledThreadPool

  • 核心线程数 = 指定值;最大线程数 = Integer.MAX_VALUE。
  • keepAliveTime = 0
  • 阻塞队列 = DelayQueue
  • 特点:支持定时和周期性任务调度。
  • 适用场景:适合定时执行任务(如心跳检测、周期任务)。
(5) newWorkStealingPool(JDK 1.8+ 新增)

  • 使用 ForkJoinPool 实现。
  • 核心线程数 = CPU 核心数(Runtime.getRuntime().availableProcessors())。
  • 特点:基于“工作窃取算法”,空闲线程可以从其他线程队列里偷任务执行,负载均衡。
  • 适用场景:适合 大批量、小任务并行计算(如分治计算)。
9、线程池异常怎么处理?

  在使用线程池时,经常会遇到这样的情况:线程池本身没问题,但任务(Runnable/Callable)运行过程中抛出了异常
如果没有额外处理,这些异常可能会被“吞掉”,导致我们难以及时发现问题。
本文总结了常见的几种线程池任务异常处理方式。
(1) 在任务内部 try-catch
  1. // 1. 默认工厂(抽象但可用)
  2. ThreadFactory defaultFactory = Executors.defaultThreadFactory();
  3. // 2. 自定义工厂 - 可以完全控制线程创建过程
  4. ThreadFactory customFactory = new ThreadFactory() {
  5.     private final AtomicInteger threadNumber = new AtomicInteger(1);
  6.    
  7.     @Override
  8.     public Thread newThread(Runnable r) {
  9.         Thread t = new Thread(r);
  10.         t.setName("my-pool-worker-" + threadNumber.getAndIncrement());
  11.         t.setDaemon(false); // 设置为非守护线程
  12.         t.setPriority(Thread.NORM_PRIORITY); // 设置优先级
  13.         return t;
  14.     }
  15. };
  16. // 3. 在线程池中使用
  17. ThreadPoolExecutor pool = new ThreadPoolExecutor(
  18.     CORE_POOL_SIZE,
  19.     MAXIMUM_POOL_SIZE,
  20.     60L, TimeUnit.SECONDS,
  21.     new LinkedBlockingQueue<Runnable>(),
  22.     customFactory, // 这里传入自定义工厂
  23.     new ThreadPoolExecutor.CallerRunsPolicy()
  24. );
复制代码
优点:简单直观,能捕获异常。
缺点:需要每个任务都写 try/catch,容易遗漏,不够统一。
(2) submit() + Future.get()
  1. (1) execute()
  2. 用法:void execute(Runnable command)
  3. 特点:
  4. 只能提交 Runnable 任务;
  5. 没有返回值;
  6. 如果任务抛出异常,异常会直接抛到线程中,最终由线程的 UncaughtExceptionHandler 或 afterExecute 捕获。
复制代码
特点

  • 使用 submit() 提交任务会返回 Future;
  • 如果调用 get(),会抛出 ExecutionException,异常可以被捕获;
  • 但如果 不调用 get(),异常就会被“吞掉”。
适合需要拿任务返回值的场景。
(3) 设置线程的 UncaughtExceptionHandler

 
  1. (2) submit()
  2. 用法:
  3. Future<?> submit(Runnable task)
  4. <T> Future<T> submit(Callable<T> task)
  5. 特点:
  6. 可以提交 Runnable 或 Callable 任务;
  7. 返回 Future 对象,可以获取结果、取消任务、捕获异常;
  8. 如果任务抛出异常,异常会被封装在 Future 中,只有调用 get() 时才会抛出 ExecutionException。
复制代码
 特点

  • 给线程绑定一个全局的异常处理器。
  • 任务里不用写 try/catch,异常会被统一捕获和处理。
  • 适用于 execute() 提交的任务。
  • 但对 submit() 提交的异常无效(因为被封装到 Future 里)。
(4) 重写 afterExecute()
  1. pool.execute(() -> {
  2.     try {
  3.         int a = 1 / 0;
  4.     } catch (Exception e) {
  5.         System.err.println("任务异常: " + e.getMessage());
  6.     }
  7. });
复制代码
 特点

  • t 参数能捕获 execute() 的异常;
  • Future.get() 能捕获 submit() 的异常;
  • 这样就能 统一捕获 execute 和 submit 的异常
  • 比单纯的 UncaughtExceptionHandler 更全面。
10、说一下线程池有几种状态

(1) RUNNING

  • 默认初始状态,可以接受新任务,也可以处理阻塞队列中的任务。
  • 调用 shutdown() → 转为 SHUTDOWN;调用 shutdownNow() → 转为 STOP
(2) SHUTDOWN

  • 不再接受新任务,但会继续处理阻塞队列中的任务和正在执行的任务。
  • 当队列为空且活动线程数为 0 → 转为 TIDYING
(3) STOP

  • 不再接受新任务,也不处理阻塞队列中的任务。
  • 会中断正在运行的任务(通过调用线程的 interrupt())。
  • 一般由 shutdownNow() 触发。
(4) TIDYING

  • 所有任务都结束,活动线程数为 0。
  • 会执行 terminated() 钩子方法,通常用于清理资源。
  • 是进入终止前的过渡状态。
(5) TERMINATED

  • 线程池彻底终止,生命周期完全结束。
  1. Future<?> future = pool.submit(() -> {
  2.     int a = 1 / 0;
  3. });
  4. try {
  5.     future.get(); // 会抛 ExecutionException
  6. } catch (Exception e) {
  7.     System.err.println("捕获到异常: " + e.getCause());
  8. }
复制代码
11、线程池如何实现参数的动态修改

(1) ThreadPoolExecutor 提供的 setter 方法
<ul data-start="108" data-end="399"><li data-start="108" data-end="147">setCorePoolSize(int corePoolSize)
<li data-start="148" data-end="193">setMaximumPoolSize(int maximumPoolSize)
<li data-start="194" data-end="242">setKeepAliveTime(long time, TimeUnit unit)
<li data-start="243" data-end="294">setThreadFactory(ThreadFactory threadFactory)
<li data-start="295" data-end="399">
setRejectedExecutionHandler(RejectedExecutionHandler handler)

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册