前言
这个周在使用Java做需求的时候,有遇到一个奇怪的问题,数据库写入会卡住,使用的技术栈是SprintBoot+mybatis,稍微一点儿不同的是我引入了多数据源支持,搞不清楚是为什么写入卡住。
分析
伪代码:
...
static class Solution {
private static final Map<String, String> STRUCT_MAP = Arrays.asList(
"a", "b", "c"
).parallelStream().filter(e -> e != null).collect(Collectors.toMap(e -> e, e -> e, (o1, o2) -> o1));
public void insertData(Bean bean) {
bean.value = STRUCT_MAP.getOrDefault(bean.id, "");
beanMapper.insert(bean);
}
}
static class Bean {
String id;
String value;
}
...
但是这里的insert方式,总是没有写入数据库,可以确定的是,这里已经调用了insertData方法,但是就是没有写入数据库,一度以为是我的多数据源出问题了,但是实际上我的多数据源已经在测试环境中跑了两天多了,都是好的,只不过第三天才发现数据写入失败了,很迷,不知道是什么出了问题。
可能的原因一,多数据源引起的问题
分析:这里由于使用到了多数据源,因此使用了一些自定义的bean,猜想是不是由于这些配置不正确导致的,经过去掉多数据源验证,结果问题依旧,说明不是多数据源的问题。
可能的原因二,是不是数据库链接池的锅
分析:使用的是SpringBoot内置的HikariPool,将其改成Duird后,问题依旧,说明不是数据库链接池的锅。
可能的原因三,是不是方法上的事务导致的
分析:尝试去掉@Transactional注解,以及尝试手动设置TransactionManager,问题依旧,说明不是事务的问题。
祭出大招
最后实在是没辙了,只能祭出大招,逐行打印日志,看看到底运行到哪里卡住了,通过日志输出观察到,
bean.value = STRUCT_MAP.getOrDefault(bean.id, "");
map的初始化
static class Solution {
private static final Map<String, String> STRUCT_MAP = Arrays.asList(
"a", "b", "c"
).parallelStream().filter(e -> e != null).collect(Collectors.toMap(e -> e, e -> e, (o1, o2) -> o1));
}
在这一行,就卡住了,跑不下去了,但是这里的逻辑很简单,从map中取值,这能有什么问题尼,所以就想着是不是空指针了,但是代码也没有报错,就是卡住了,虽然不敢相信这里会出问题,不过还是把代码拎出来,跑了个单元测试,结果还真的有问题,测试代码:
@Test
public void testStream() {
String s = Solution.STRUCT_MAP.get("");
// stuck here
System.out.println(s);
}
static class Solution {
private static final Map<String, String> STRUCT_MAP = Arrays.asList(
"a", "b", "c"
).parallelStream().filter(e -> e != null).collect(Collectors.toMap(e -> e, e -> e, (o1, o2) -> o1));
}
这里这个Solution类的属性初始化会卡住,惊呆了,不知道为啥,按照多年写代码的经验来看,这里应该是没啥问题的,第一次用到的时候去初始化,语法语义都是正确的,但就是跑不过。百思不得解。但是尝试将并行流改为串行流就可以跑过,真神奇,求助Google大佬,结果还真的有bug,
Parallel stream can deadlock when called from static initializer
链接,看来还真的是遇到jdk的bug了,并行流在static initializer初始化的时候,会产生死锁。
reason:
The reason is that the first line has a lambda expression, which gets compiled into a static method in this class. When another thread invokes the lambda, it results in a call to that static method, which will block awaiting class initialization. The second line has a method reference, which doesn’t produce a static method on this class. When it’s invoked, it calls the isNull() method on the java.util.Objects class directly, avoiding blocking on class initialization.
大意是,当static finalizer有个lambda表达式的时候,会将这个表达式编译为一个静态方法,而当有别的线程调用这个静态方法获取结果的时候,则会等待这个类初始化完成。
虽然还是不怎么懂,据我的理解应该是可重入锁由于其它线程的竞争导致死锁了,否则如果不是可重入锁,那么改为串行流应该也还是会卡住,不过到这一步也就差不多了,解决的办法很简单,要么把这个初始化操作,不这样写,要么改为串行流。
解决方法
- 改为串行流
- 优化逻辑,不要将static final和并行流放在一块儿
后记
这里这样写的原因虽然有硬编码之嫌,但是改进的方法会更不优雅,头大。