目录
日常开发中,为了更好管理线程资源,减少创建线程和销毁线程的资源损耗,我们会使用线程池来执行一些异步任务。但是线程池使用不当,就可能会引发生产事故。今天田螺哥跟大家聊聊线程池的10个坑。大家看完肯定会有帮助的~
来源:https://juejin.cn/post/7132263894801711117
- 线程池默认使用无界队列,任务过多导致OOM
- 线程创建过多,导致OOM
- 共享线程池,次要逻辑拖垮主要逻辑
- 线程池拒绝策略的坑
- Spring内部线程池的坑
- 使用线程池时,没有自定义命名
- 线程池参数设置不合理
- 线程池异常处理的坑
- 使用完线程池忘记关闭
- ThreadLocal与线程池搭配,线程复用,导致信息错乱。
6. 使用线程池时,没有自定义命名
使用线程池时,如果没有给线程池一个有意义的名称,将不好排查回溯问题。这不算一个坑吧,只能说给以后排查埋坑,哈哈。我还是单独把它放出来算一个点,因为个人觉得这个还是比较重要的。反例如下:
/** * 关注公众号:捡田螺的小男孩 */ public class ThreadTest { public static void main(String[] args) throws Exception { ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20)); executorOne.execute(()->{ System.out.println("关注公众号:捡田螺的小男孩"); throw new NullPointerException(); }); } } 复制代码
运行结果:
关注公众号:捡田螺的小男孩 Exception in thread "pool-1-thread-1" java.lang.NullPointerException at com.example.dto.ThreadTest.lambda$main$0(ThreadTest.java:17) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) 复制代码
可以发现,默认打印的线程池名字是
pool-1-thread-1
,如果排查问题起来,并不友好。因此建议大家给自己线程池自定义个容易识别的名字。其实用CustomizableThreadFactory
即可,正例如下:public class ThreadTest { public static void main(String[] args) throws Exception { ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),new CustomizableThreadFactory("Tianluo-Thread-pool")); executorOne.execute(()->{ System.out.println("关注公众号:捡田螺的小男孩"); throw new NullPointerException(); }); } } 复制代码
7. 线程池参数设置不合理
线程池最容易出坑的地方,就是线程参数设置不合理。比如核心线程设置多少合理,最大线程池设置多少合理等等。当然,这块不是乱设置的,需要结合具体业务。
比如线程池如何调优,如何确认最佳线程数?
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 复制代码
我们的服务器CPU核数为8核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:( 80 + 20 )/20 * 8 = 40。也就是设置 40个线程数最佳。
有兴趣的小伙伴,也可以看这篇文章哈: 线程池到底设置多少线程比较合适?
对于线程池参数,如果小伙伴还有疑惑的话,可以看我之前这篇文章哈:Java线程池解析
8. 线程池异常处理的坑
我们来看段代码:
/** * 关注公众号:捡田螺的小男孩 */ public class ThreadTest { public static void main(String[] args) throws Exception { ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),new CustomizableThreadFactory("Tianluo-Thread-pool")); for (int i = 0; i < 5; i++) { executorOne.submit(()->{ System.out.println("current thread name" + Thread.currentThread().getName()); Object object = null; System.out.print("result## " + object.toString()); }); } } } 复制代码
按道理,运行这块代码应该抛空指针异常才是的,对吧。但是,运行结果却是这样的;
current thread nameTianluo-Thread-pool1 current thread nameTianluo-Thread-pool2 current thread nameTianluo-Thread-pool3 current thread nameTianluo-Thread-pool4 current thread nameTianluo-Thread-pool5 复制代码
这是因为使用
submit
提交任务,不会把异常直接这样抛出来。大家有兴趣的话,可以去看看源码。可以改为execute
方法执行,当然最好就是try...catch捕获
,如下:/** * 关注公众号:捡田螺的小男孩 */ public class ThreadTest { public static void main(String[] args) throws Exception { ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),new CustomizableThreadFactory("Tianluo-Thread-pool")); for (int i = 0; i < 5; i++) { executorOne.submit(()->{ System.out.println("current thread name" + Thread.currentThread().getName()); try { Object object = null; System.out.print("result## " + object.toString()); }catch (Exception e){ System.out.println("异常了"+e); } }); } } } 复制代码
其实,我们还可以为工作者线程设置
UncaughtExceptionHandler
,在uncaughtException
方法中处理异常。大家知道这个坑就好啦。9. 线程池使用完毕后,忘记关闭
如果线程池使用完,忘记关闭的话,有可能会导致内存泄露问题。所以,大家使用完线程池后,记得关闭一下。同时,线程池最好也设计成单例模式,给它一个好的命名,以方便排查问题。
public class ThreadTest { public static void main(String[] args) throws Exception { ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20), new CustomizableThreadFactory("Tianluo-Thread-pool")); executorOne.execute(() -> { System.out.println("关注公众号:捡田螺的小男孩"); }); //关闭线程池 executorOne.shutdown(); } } 复制代码
10. ThreadLocal与线程池搭配,线程复用,导致信息错乱。
使用
ThreadLocal
缓存信息,如果配合线程池一起,有可能出现信息错乱的情况。先看下一下例子:private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null); @GetMapping("wrong") public Map wrong(@RequestParam("userId") Integer userId) { //设置用户信息之前先查询一次ThreadLocal中的用户信息 String before = Thread.currentThread().getName() + ":" + currentUser.get(); //设置用户信息到ThreadLocal currentUser.set(userId); //设置用户信息之后再查询一次ThreadLocal中的用户信息 String after = Thread.currentThread().getName() + ":" + currentUser.get(); //汇总输出两次查询结果 Map result = new HashMap(); result.put("before", before); result.put("after", after); return result; } 复制代码
按理说,每次获取的
before
应该都是null
,但是呢,程序运行在Tomcat
中,执行程序的线程是Tomcat
的工作线程,而Tomcat
的工作线程是基于线程池的。线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。
把tomcat的工作线程设置为1
server.tomcat.max-threads=1 复制代码
用户1,请求过来,会有以下结果,符合预期:
用户2请求过来,会有以下结果,「不符合预期」:
因此,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据,正例如下:
@GetMapping("right") public Map right(@RequestParam("userId") Integer userId) { String before = Thread.currentThread().getName() + ":" + currentUser.get(); currentUser.set(userId); try { String after = Thread.currentThread().getName() + ":" + currentUser.get(); Map result = new HashMap(); result.put("before", before); result.put("after", after); return result; } finally { //在finally代码块中删除ThreadLocal中的数据,确保数据不串 currentUser.remove(); } } 复制代码
参考与感谢
- 线程池拒绝策略的坑,不得不防
- Java业务开发常见错误100例: