做外贸网站需要什么,wordpress 安装,东昌府区住房和城乡建设局网站,注册网站商城需要什么条件日常开发中#xff0c;为了更好管理线程资源#xff0c;减少创建线程和销毁线程的资源损耗#xff0c;我们会使用线程池来执行一些异步任务。但是线程池使用不当#xff0c;就可能会引发生产事故。今天跟大家聊聊线程池的10个坑。大家看完肯定会有帮助的~线程池默认使用无界…日常开发中为了更好管理线程资源减少创建线程和销毁线程的资源损耗我们会使用线程池来执行一些异步任务。但是线程池使用不当就可能会引发生产事故。今天跟大家聊聊线程池的10个坑。大家看完肯定会有帮助的~线程池默认使用无界队列任务过多导致OOM线程创建过多导致OOM共享线程池次要逻辑拖垮主要逻辑线程池拒绝策略的坑Spring内部线程池的坑使用线程池时没有自定义命名线程池参数设置不合理线程池异常处理的坑使用完线程池忘记关闭ThreadLocal与线程池搭配线程复用导致信息错乱。1.线程池默认使用无界队列任务过多导致OOM JDK开发者提供了线程池的实现类我们基于Executors组件就可以快速创建一个线程池。日常工作中一些小伙伴为了开发效率反手就用Executors新建个线程池。写出类似以下的代码public class NewFixedTest {public static void main(String[] args) {ExecutorService executor Executors.newFixedThreadPool(10);for (int i 0; i Integer.MAX_VALUE; i) {executor.execute(() - {try {Thread.sleep(10000);} catch (InterruptedException e) {//do nothing}});}}
}使用newFixedThreadPool创建的线程池是会有坑的它默认是无界的阻塞队列如果任务过多会导致OOM问题。运行一下以上代码出现了OOM。Exception in thread main java.lang.OutOfMemoryError: GC overhead limit exceededat java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)at com.example.dto.NewFixedTest.main(NewFixedTest.java:14)这是因为newFixedThreadPool使用了无界的阻塞队列的LinkedBlockingQueue如果线程获取一个任务后任务的执行时间比较长(比如上面demo代码设置了10秒)会导致队列的任务越积越多导致机器内存使用不停飙升 最终出现OOM。看下newFixedThreadPool的相关源码是可以看到一个无界的阻塞队列的如下//阻塞队列是LinkedBlockingQueue并且是使用的是无参构造函数
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueueRunnable());
}//无参构造函数默认最大容量是Integer.MAX_VALUE相当于无界的阻塞队列的了
public LinkedBlockingQueue() {this(Integer.MAX_VALUE);
}因此工作中建议大家自定义线程池并使用指定长度的阻塞队列。2. 线程池创建线程过多导致OOM 有些小伙伴说既然Executors组件创建出的线程池newFixedThreadPool使用的是无界队列可能会导致OOM。那么Executors组件还可以创建别的线程池如newCachedThreadPool我们用它也不行嘛我们可以看下newCachedThreadPool的构造函数public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueueRunnable());}它的最大线程数是Integer.MAX_VALUE。大家应该意识到使用它可能会引发什么问题了吧。没错如果创建了大量的线程也有可能引发OOM笔者在以前公司遇到这么一个OOM问题一个第三方提供的包是直接使用new Thread实现多线程的。在某个夜深人静的夜晚我们的监控系统报警了。。。这个相关的业务请求瞬间特别多监控系统告警OOM了。所以我们使用线程池的时候还要当心线程创建过多导致OOM问题。大家尽量不要使用newCachedThreadPool并且如果自定义线程池时要注意一下最大线程数。3. 共享线程池次要逻辑拖垮主要逻辑 要避免所有的业务逻辑共享一个线程池。比如你用线程池A来做登录异步通知又用线程池A来做对账。如下图如果对账任务checkBillService响应时间过慢会占据大量的线程池资源可能直接导致没有足够的线程资源去执行loginNotifyService的任务最后影响登录。就这样因为一个次要服务影响到重要的登录接口显然这是绝对不允许的。因此我们不能将所有的业务一锅炖都共享一个线程池因为这样做风险太高了犹如所有鸡蛋放到一个篮子里。应当做线程池隔离4. 线程池拒绝策略的坑使用不当导致阻塞 我们知道线程池主要有四种拒绝策略如下AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。(默认拒绝策略)DiscardPolicy丢弃任务但是不抛出异常。DiscardOldestPolicy丢弃队列最前面的任务然后重新尝试执行任务。CallerRunsPolicy由调用方线程处理该任务。如果线程池拒绝策略设置不合理就容易有坑。我们把拒绝策略设置为DiscardPolicy或DiscardOldestPolicy并且在被拒绝的任务Future对象调用get()方法,那么调用线程会一直被阻塞。我们来看个demopublic class DiscardThreadPoolTest {public static void main(String[] args) throws ExecutionException, InterruptedException {// 一个核心线程队列最大为1最大线程数也是1.拒绝策略是DiscardPolicyThreadPoolExecutor executorService new ThreadPoolExecutor(1, 1, 1L, TimeUnit.MINUTES,new ArrayBlockingQueue(1), new ThreadPoolExecutor.DiscardPolicy());Future f1 executorService.submit(()- {System.out.println(提交任务1);try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}});Future f2 executorService.submit(()-{System.out.println(提交任务2);});Future f3 executorService.submit(()-{System.out.println(提交任务3);});System.out.println(任务1完成 f1.get());// 等待任务1执行完毕System.out.println(任务2完成 f2.get());// 等待任务2执行完毕System.out.println(任务3完成 f3.get());// 等待任务3执行完毕executorService.shutdown();// 关闭线程池阻塞直到所有任务执行完毕}
}运行结果一直在运行中。。。这是因为DiscardPolicy拒绝策略是什么都没做源码如下public static class DiscardPolicy implements RejectedExecutionHandler {/*** Creates a {code DiscardPolicy}.*/public DiscardPolicy() { }/*** Does nothing, which has the effect of discarding task r.*/public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}
}我们再来看看线程池 submit 的方法public Future? submit(Runnable task) {if (task null) throw new NullPointerException();//把Runnable任务包装为Future对象RunnableFutureVoid ftask newTaskFor(task, null);//执行任务execute(ftask);//返回Future对象return ftask;
}public FutureTask(Runnable runnable, V result) {this.callable Executors.callable(runnable, result);this.state NEW; //Future的初始化状态是New
}我们再来看看Future的get() 方法//状态大于COMPLETING才会返回要不然都会阻塞等待public V get() throws InterruptedException, ExecutionException {int s state;if (s COMPLETING)s awaitDone(false, 0L);return report(s);}FutureTask的状态枚举private static final int NEW 0;private static final int COMPLETING 1;private static final int NORMAL 2;private static final int EXCEPTIONAL 3;private static final int CANCELLED 4;private static final int INTERRUPTING 5;private static final int INTERRUPTED 6;阻塞的真相水落石出啦FutureTask的状态大于COMPLETING才会返回要不然都会一直阻塞等待。又因为拒绝策略啥没做没有修改FutureTask的状态因此FutureTask的状态一直是NEW所以它不会返回会一直等待。这个问题可以使用别的拒绝策略比如CallerRunsPolicy它让主线程去执行拒绝的任务会更新FutureTask状态。如果确实想用DiscardPolicy则需要重写DiscardPolicy的拒绝策略。温馨提示日常开发中使用 Future.get() 时尽量使用带超时时间的因为它是阻塞的。future.get(1, TimeUnit.SECONDS);难道使用别的拒绝策略就万无一失了嘛不是的如果使用CallerRunsPolicy拒绝策略它表示拒绝的任务给调用方线程用如果这是主线程那会不会可能也导致主线程阻塞呢总结起来大家日常开发的时候多一份心眼吧多一点思考吧。5. Spring内部线程池的坑 工作中个别开发者为了快速开发喜欢直接用spring的Async来执行异步任务。Async
public void testAsync() throws InterruptedException {System.out.println(处理异步任务);TimeUnit.SECONDS.sleep(new Random().nextInt(100));
}Spring内部线程池其实是SimpleAsyncTaskExecutor这玩意有点坑它不会复用线程的它的设计初衷就是执行大量的短时间的任务。有兴趣的小伙伴可以去看看它的源码/**
* {link TaskExecutor} implementation that fires up a new Thread for each task,
* executing it asynchronously.
*
* pSupports limiting concurrent threads through the concurrencyLimit
* bean property. By default, the number of concurrent threads is unlimited.
*
* pbNOTE: This implementation does not reuse threads!/b Consider a
* thread-pooling TaskExecutor implementation instead, in particular for
* executing a large number of short-lived tasks.
*
* author Juergen Hoeller
* since 2.0
* see #setConcurrencyLimit
* see SyncTaskExecutor
* see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
* see org.springframework.scheduling.commonj.WorkManagerTaskExecutor
*/
SuppressWarnings(serial)
public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator implements AsyncListenableTaskExecutor, Serializable {
......
}也就是说来了一个请求就会新建一个线程大家使用spring的Async时要避开这个坑自己再定义一个线程池。正例如下Bean(name threadPoolTaskExecutor)
public Executor threadPoolTaskExecutor() {ThreadPoolTaskExecutor executornew ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setThreadNamePrefix(tianluo-%d);// 其他参数设置return new ThreadPoolTaskExecutor();
}6. 使用线程池时没有自定义命名 使用线程池时如果没有给线程池一个有意义的名称将不好排查回溯问题。这不算一个坑吧只能说给以后排查埋坑哈哈。我还是单独把它放出来算一个点因为个人觉得这个还是比较重要的。反例如下public class ThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueueRunnable(20));executorOne.execute(()-{System.out.println(HelloJava);throw new NullPointerException();});}
}运行结果HelloJava
Exception in thread pool-1-thread-1 java.lang.NullPointerExceptionat 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 ArrayBlockingQueueRunnable(20),new CustomizableThreadFactory(Tianluo-Thread-pool));executorOne.execute(()-{System.out.println(HelloJava);throw new NullPointerException();});}
}7. 线程池参数设置不合理 线程池最容易出坑的地方就是线程参数设置不合理。比如核心线程设置多少合理最大线程池设置多少合理等等。当然这块不是乱设置的需要结合具体业务。比如线程池如何调优如何确认最佳线程数最佳线程数目 线程等待时间线程CPU时间/线程CPU时间 * CPU数目我们的服务器CPU核数为8核一个任务线程cpu耗时为20ms线程等待网络IO、磁盘IO耗时80ms那最佳线程数目( 80 20 )/20 * 8 40。也就是设置 40个线程数最佳。8. 线程池异常处理的坑 我们来看段代码public class ThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES, new ArrayBlockingQueueRunnable(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 ArrayBlockingQueueRunnable(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 ArrayBlockingQueueRunnable(20), new CustomizableThreadFactory(Tianluo-Thread-pool));executorOne.execute(() - {System.out.println(HelloJava);});//关闭线程池executorOne.shutdown();}
}10. ThreadLocal与线程池搭配线程复用导致信息错乱。 使用ThreadLocal缓存信息如果配合线程池一起有可能出现信息错乱的情况。先看下一下例子private static final ThreadLocalInteger currentUser ThreadLocal.withInitial(() - null);GetMapping(wrong)
public Map wrong(RequestParam(userId) Integer userId) {//设置用户信息之前先查询一次ThreadLocal中的用户信息String before Thread.currentThread().getName() : currentUser.get();//设置用户信息到ThreadLocalcurrentUser.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的工作线程设置为1server.tomcat.max-threads1用户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();}
}参考与感谢[1]线程池拒绝策略的坑不得不防: http://rainbowhorse.site/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E8%B8%A9%E5%9D%91/[2]Java业务开发常见错误100例:: https://time.geekbang.org/column/article/220230