线程池 坑中之王 !

向量数据库大模型云存储

大家好,我是苏三,又跟大家见面了。

文末留言送书了

前言

线程池是 Java 中处理多线程的强大工具,但它不仅仅是“直接用就完事”的工具。

很多小伙伴在用线程池时,因为配置不当或忽略细节,踩过许多坑。

今天跟大家一起聊聊线程池中容易踩的 10 个坑,以及如何避免这些坑,希望对你会有所帮助。

  1. 直接使用 Executors 创建线程池

许多初学者在创建线程池时,直接使用 Executors 提供的快捷方法:


        
          
ExecutorService executor = Executors.newFixedThreadPool(10);  

      

问题在哪?

  • 无界队列newFixedThreadPool 使用的队列是 LinkedBlockingQueue,它是无界队列,任务堆积可能会导致内存溢出。
  • 线程无限增长newCachedThreadPool 会无限创建线程,在任务量激增时可能耗尽系统资源。

示例:内存溢出的风险


        
          
ExecutorService executor = Executors.newFixedThreadPool(2);  
for (int i = 0; i < 1000000; i++) {  
    executor.submit(() -> {  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    });  
}  

      

任务数远大于线程数,导致任务无限堆积在队列中,最终可能导致 OutOfMemoryError

解决办法

使用 ThreadPoolExecutor,并明确指定参数:


        
          
ThreadPoolExecutor executor = new ThreadPoolExecutor(  
    2,  
    4,  
    60L,  
    TimeUnit.SECONDS,  
    new ArrayBlockingQueue<>(100), // 有界队列  
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略  
);  

      
  1. 错误配置线程数

很多人随意配置线程池参数,比如核心线程数 10,最大线程数 100,看起来没问题,但这可能导致性能问题或资源浪费。

示例:错误配置导致的线程过载


        
          
ThreadPoolExecutor executor = new ThreadPoolExecutor(  
    10, // 核心线程数  
    100, // 最大线程数  
    60L,  
    TimeUnit.SECONDS,  
    new ArrayBlockingQueue<>(10)  
);  
  
for (int i = 0; i < 1000; i++) {  
    executor.submit(() -> {  
        try {  
            Thread.sleep(5000); // 模拟耗时任务  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    });  
}  

      

这种配置在任务激增时,会创建大量线程,系统资源被耗尽。

正确配置方式

根据任务类型选择合理的线程数:

  • CPU 密集型 :线程数建议设置为 CPU 核心数 + 1
  • IO 密集型 :线程数建议设置为 2 * CPU 核心数

示例:


        
          
int cpuCores = Runtime.getRuntime().availableProcessors();  
ThreadPoolExecutor executor = new ThreadPoolExecutor(  
    cpuCores + 1,  
    cpuCores + 1,  
    60L,  
    TimeUnit.SECONDS,  
    new ArrayBlockingQueue<>(50)  
);  

      
  1. 忽略任务队列的选择

任务队列直接影响线程池的行为。如果选错队列类型,会带来很多隐患。

常见队列的坑

  • 无界队列 :任务无限堆积。
  • 有界队列 :队列满了会触发拒绝策略。
  • 优先级队列 :容易导致高优先级任务频繁抢占低优先级任务。

示例:任务堆积导致问题


        
          
ThreadPoolExecutor executor = new ThreadPoolExecutor(  
    2,  
    4,  
    60L,  
    TimeUnit.SECONDS,  
    new LinkedBlockingQueue<>()  
);  
  
for (int i = 0; i < 100000; i++) {  
    executor.submit(() -> System.out.println(Thread.currentThread().getName()));  
}  

      

改进方法 :用有界队列,避免任务无限堆积。


        
          
new ArrayBlockingQueue<>(100);  

      
  1. 忘记关闭线程池

有些小伙伴用完线程池后,忘记调用 shutdown(),导致程序无法正常退出。

示例:线程池未关闭


        
          
ExecutorService executor = Executors.newFixedThreadPool(5);  
executor.submit(() -> System.out.println("任务执行中..."));  
// 线程池未关闭,程序一直运行  

      

正确关闭方式


        
          
executor.shutdown();  
try {  
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {  
        executor.shutdownNow();  
    }  
} catch (InterruptedException e) {  
    executor.shutdownNow();  
}  

      
  1. 忽略拒绝策略

当任务队列满时,线程池会触发拒绝策略,很多人不知道默认策略(AbortPolicy)会直接抛异常。

示例:任务被拒绝


        
          
ThreadPoolExecutor executor = new ThreadPoolExecutor(  
    1,  
    1,  
    60L,  
    TimeUnit.SECONDS,  
    new ArrayBlockingQueue<>(2),  
    new ThreadPoolExecutor.AbortPolicy() // 默认策略  
);  
  
for (int i = 0; i < 10; i++) {  
    executor.submit(() -> System.out.println("任务"));  
}  

      

执行到第四个任务时会抛出 RejectedExecutionException

改进:选择合适的策略

  • CallerRunsPolicy:提交任务的线程自己执行。
  • DiscardPolicy:直接丢弃新任务。
  • DiscardOldestPolicy:丢弃最老的任务。
  1. 任务中未处理异常

线程池中的任务抛出异常时,线程池不会直接抛出,导致很多问题被忽略。

示例:异常被忽略


        
          
executor.submit(() -> {  
    throw new RuntimeException("任务异常");  
});  

      

解决方法

  1. 捕获任务内部异常:

        
          
executor.submit(() -> {  
    try {  
        throw new RuntimeException("任务异常");  
    } catch (Exception e) {  
        System.err.println("捕获异常:" + e.getMessage());  
    }  
});  

      
  1. 自定义线程工厂:

        
          
ThreadFactory factory = r -> {  
    Thread t = new Thread(r);  
    t.setUncaughtExceptionHandler((thread, e) -> {  
        System.err.println("线程异常:" + e.getMessage());  
    });  
    return t;  
};  

      
  1. 阻塞任务占用线程池

如果线程池中的任务是阻塞的(如文件读写、网络请求),核心线程会被占满,影响性能。

示例:阻塞任务拖垮线程池


        
          
executor.submit(() -> {  
    Thread.sleep(10000); // 模拟阻塞任务  
});  

      

改进方法

  • 减少任务的阻塞时间。
  • 增加核心线程数。
  • 使用异步非阻塞方式(如 NIO)。
  1. 滥用线程池

线程池不是万能的,某些场景直接使用 new Thread() 更简单。

示例:过度使用线程池

一个简单的短期任务:


        
          
ExecutorService executor = Executors.newSingleThreadExecutor();  
executor.submit(() -> System.out.println("执行任务"));  
executor.shutdown();  

      

这种情况下,用线程池反而复杂。

改进方式


        
          
new Thread(() -> System.out.println("执行任务")).start();  

      
  1. 未监控线程池状态

很多人用线程池后,不监控其状态,导致任务堆积、线程耗尽的问题被忽略。

示例:监控线程池状态


        
          
System.out.println("核心线程数:" + executor.getCorePoolSize());  
System.out.println("队列大小:" + executor.getQueue().size());  
System.out.println("已完成任务数:" + executor.getCompletedTaskCount());  

      

结合监控工具(如 JMX、Prometheus),实现实时监控。

  1. 动态调整线程池参数

有些人在线程池设计时忽略了参数调整的必要性,导致后期性能优化困难。

示例:动态调整核心线程数


        
          
executor.setCorePoolSize(20);  
executor.setMaximumPoolSize(50);  

      

实时调整线程池参数,能适应业务的动态变化。

总结

线程池是强大的工具,但如果我们日常工作中用得不好也非常容易踩坑。

这篇文章通过实际代码示例,我们可以清楚看到线程池的问题所在及改进方法。

希望这些内容能帮你避免踩坑,写出高质量的线程池代码!

线程池用得好,效率杠杠的;用得不好,程序天天崩!

文末送书

为了感谢一路支持苏三的小伙们,今天特地给大家送一点小福利。规则非常简单:在本文留言,我会挑选其中最用心的3位留言者(不一定是点赞最多的),每人获取1本书。

后面我会朋友圈公布中奖名单,给你免费包邮到家!这些书是由电子工业出版社提供的,感谢赞助。

《 悟道领域驱动设计

picture.image

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论