多线程大家都用过,可以让一个程序同时执行多个任务,提高效率和性能,一个人干的慢,三个人干。但是,多线程也带来了一些问题和挑战,比如线程同步、线程安全、线程死锁等问题,三个人抢一碗米饭,没有个规矩肯定要打架的。
这里我介绍一种可能出现的多线程问题:如果一个线程在执行过程中一直卡住,线程不释放最终导致线程耗尽怎么办。
这是前一阶段对接外部系统时遇到的一个问题,对方提供了个SDK,我们集成后用他们的netty的方式建立连接,推送相关数据,但是呢这个推送不能阻塞业务,所以我们采用异步推送,搞了个线程池,随取随还,如图
后来就发现有一个现象,推着推着就不推了,卡到多线程前那里,也不进去,也不拒绝
然后我用executor.getTaskCount(), executor.getActiveCount(),executor.getCompletedTaskCount()打印日志发现活跃数没有了,并且拒绝策略是CallerRunsPolicy()(这个策略的意思:当任务添加到线程池中被拒绝时,会使用调用线程池的Thread线程对象处理被拒绝的任务,有个问题就是都没有线程对象了,自然也取不到来执行),所以看起来也不报错,像是假死,拒绝策略先不说,这里我们只处理线程耗尽的问题
我们升华一下对于这类问题我们要如何处理
问题分析
首先我们分析下为什么会出现这种情况。一个线程在执行过程中可能会遇到下面几种原因导致卡住:
- 线程等待某个资源或条件,但是资源或条件一直不满足,比如网络请求超时、锁竞争失败等。
- 线程遇到死循环或无限递归,导致程序逻辑无法继续执行。
- 线程遇到异常或错误,但是没有正确处理,导致程序崩溃或挂起。
- 如果一个线程卡住了,那么它就无法释放它占用的资源,比如内存、CPU、锁等等。这样就会影响其他线程的运行,甚至导致整个程序的性能下降或崩溃。如果我们不断地创建新的线程,而旧的线程不释放,最终就会导致线程耗尽。就会抛出OutOfMemoryError或UnableToCreateThreadException异常。
解决方案
针对上面的问题,我列了几种解决方案:
- 限制线程的数量。我们可以使用线程池来管理和复用线程,而不是每次都创建新的线程。这样可以避免创建过多的线程,也可以提高线程的利用率和管理性。
- 设置超时机制。我们可以给每个线程设置一个合理的超时时间,如果超过了这个时间,就认为该线程卡住了,并且强制中断或杀死该线程。这样可以避免某个线程无限等待或执行。
- 检查和优化代码逻辑。我们应该检查和测试我们的代码逻辑,避免出现死循环或无限递归等错误。如果发现有问题,我们应该及时修复和优化。
- 处理异常和错误。我们应该在每个线程中捕获并处理可能出现的异常和错误,并且在合适的时候释放资源和结束线程。这样可以避免程序崩溃或挂起。
不同的场景大家可以选用不同的方案,下面我针对第二种方案举个栗子,看看超时机制怎么设计,其他三种就不多说了
大家可以看到我们用了submit方法,得到一个Future,然后利用超时等待get方法来控制如果超时了TimeoutException,我们future.cancel(),这样就可以主动释放线程,不用一直阻塞了
// 创建一个线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交一个任务
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
// 这里是你要执行的代码,比如br.readLine()
return br.readLine();
}
});
// 设置一个超时时间,比如5秒
long timeout = 5;
// 尝试获取任务的结果,如果超时就抛出异常
try {
String s = future.get(timeout, TimeUnit.SECONDS);
// 如果没有超时,就正常处理结果
System.out.println(s);
} catch (TimeoutException e) {
// 如果超时,就取消任务,并且处理异常
future.cancel(true);
System.out.println("Time out has occurred");
}
// 关闭线程池
executor.shutdown();
多线程是一种强大而复杂的编程技术,使用时要注意避免一些常见的问题和风险,后面会继续分享一些案例以及在在这期间用到的技术,找到每种技术合适的使用场景很重要