有一个异常,网上都在描述现象,没有人深究其原因以及给出解决方案:
nested exception is java.sql.SQLException: interrupt
单刀直入,这是数据库驱动抛出的线程异常,这是线程的问题。
如果读者无心探索原理,只想知道解决方案,可以直接索引到目录的结论中。
目录
起因
如果我们希望线程等待1秒,通常我们会这样写:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
这时IDE会提醒我们关联出InterruptedException,这是一个很古老且基础的异常类。
InterruptedException
当线程正在等待、休眠或以其他方式被占用,并且线程在活动之前或活动期间被中断时抛出。 有时,方法可能希望测试当前线程是否被中断,如果是,则立即抛出此异常。
from JDK1.0
通过阅读源码注释,我们可以得知,这个InterruptedException异常是与线程相关的异常,且有一个很重要的点,就是活动线程的执行中断行为,可以抛出这样的异常。
简而言之:线程非执行结束中断的,JDK都会提醒你 interrupt。
为什么需要InterruptedException?
正常来说,程序的执行理论上都会是一个有限的资源占用,即程序是可以在有限时间内被执行结束的,可能是不到1ms,甚至长达几小时(规范而言不建议单程序片运行这么久,会建议拆分这个方法)。
与正常相对的,就是不正常的执行情况:无限的资源占用。
说到无限的资源占用,最经典的就是死循环。在loop中,如果没有中断条件,那么循环内的所有程序看作一个程序片段,那么这个程序片段将无限地占用CPU的时间片资源。
当然,我们很清楚的知道,非并发编程的情况下,当前执行loop的线程就是我们的主线程。有关多线程与并发的知识参见作者这篇文章:Java设计思想深究----多线程与并发(图文)_kevinmeanscool的博客-CSDN博客一切的缘起是昂贵的CPU我们都十分清楚,计算机的核心是计算,而负责这个功能的组件就是CPU。CPU有一个特性,在一个时刻只能处理一个程序。开发人员编写代码,代码被编译为机器语言,CPU收到机器语言(指令集),开始处理程序,而这个正在被CPU处理的程序就是进程(正在进行的程序)。当CPU正在处理一个程序时,由于其特性,其他程序就只能等待。你可能会想,一个接一个处理,不是很合理的设计吗?这仅仅对于CPU执行指令而言,的确如此。可是,数据在存储媒介上的I/O速度与CPU的速度相比,...https://blog.csdn.net/kevinmeanscool/article/details/122333614?spm=1001.2014.3001.5501
基于此,主线程将永远处于激活状态,由于没有中断条件,将无限地尝试去获取CPU的时间片。
这时,读者可能会觉得,这一定是异常的情况。
对于单机服务的情况,的确这是一种灾难,主线程一直被错误逻辑异常激活,需要我们强制kill掉这个线程。
但对于代理服务(即互联网模式),伺机服务,实际上就是一种无限地资源占用:服务端一旦启用后,将会一直等待请求,这个等待请求的过程实际上就是对于请求集合的轮询行为。
在合理需求的情况下,无限地占用资源也是一种选择方案。
因此,在伺机服务的情况下,InterruptedException便是优雅的退出无限占用资源的方式。
举个例子,对于线程,我们实际上是有一个执行时间(T)预期的,超过T的线程,不是程序逻辑有误就是性能恶化的现象,这时,我们可以对这种不太可控的情况进行一个异常捕获:
static class TestThread implements Callable {
@Override
public Object call() throws Exception {
//构造一个1秒以上的线程执行时间
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()+"执行成功");
return null;
}
}
public static void main(String[] args) {
//用于轮询的FutureList,建议使用有序线性结构
List<FutureTask> futureList = new ArrayList<>();
//new 线程
for (int i = 0; i < 3; i++){
// new 一个待唤起的任务
TestThread testThread = new TestThread();
// new 一个存放结果的实例
FutureTask futureTask = new FutureTask(testThread);
// 填充至轮询列表
futureList.add(futureTask);
// 申请一个线程,并指定run()方法是唤起一个任务
Thread thread = new Thread(futureTask);
// 激活线程
thread.start();
}
//轮询结果队列
futureList.forEach(futureTask -> {
try {
//监听线程最多执行1秒
futureTask.get(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
// 超时了,主动中断线程
futureTask.cancel(true);
//log,这里使用控制台演示
System.out.println(futureTask+"超时被中断,info:"+e.getMessage());
}
});
}
如此便优雅地实现了线程的退出。
InterruptedException的应用
实际上在使用线程时,编程规范就需要我们去设置线程超时机制,否则资源将无限制地浪费。
线程使用的最为频繁的,就是各种池,如:
- Web容器:每个请求都将申请一个线程来处理请求
- 消息队列:每个发布者、订阅者都将申请单独的线程处理事务
- DB连接池:每个事务请求都将申请一个线程来处理事务
- 等等
很多类似的生产者-消费者模式,都由JDK原生线程池实现,线程池实现原理可参考博文:
就比如文章开头提到的异常:
nested exception is java.sql.SQLException: interrupt
这就是一类中间件主动中断的线程的行为。但要分清,中断的是事务请求线程并不是连接池的核心线程。
结论
当然,这是有一个前提,就是将事务请求委托给了数据源中间件,比如Druid等。
如果是我们手动声明-开启事务-预加载-执行,如果连接池满载,只会让事务线程处于阻塞状态,也就是“卡住了”,直到分到资源来处理。
如果我们使用了数据源中间件,在多线程的情况下,很有可能出现连接池满载的情况,比如:
# 最大连接数量
maxActive: 10
# 配置获取连接等待超时的时间
maxWait: 60000
可以通过扩大连接上限和增加等待超时的时间来缓解interrupt出现的可能性、频率。