背景说明
之前为了在日志监控场景下节约机器成本就自己写了一个存储引擎用来替代ES,这个存储引擎的写压力比较大,大约单机5万左右,并发也比较多,然后就发现了在多线程极端并发的场景 new String 使用不当,会导致大量锁等待,造成死锁假象。
JDK版本
openjdk version “1.8.0_232”
OpenJDK Runtime Environment (build 1.8.0_232-b09)
OpenJDK 64-Bit Server VM (build 25.232-b09, mixed mode)
问题过程说明
有一天突然发现了,日志查询不出来了,排查了一下,发现查询请求到日志存储引擎之后,无响应。机器cpu非常低,内存正常,网络IO也非常低,进程存活正常 初步怀疑是存储引擎出现了死锁,然后 jstack -l 进行排查,根据打印出来的结果文件并没有发现明显的死锁情况,但是发现大量的查询线程都卡在了同一个地方 at java.lang.String.(String.java:426),线程状态是 BLOCKED,又详细的分析了一下,bistore-query-getdata-thread线程池中256个线程,都在抢一把锁 locked <0x00007f696a6941c0> 。其中有255个线程因为争抢这把锁,线程状态是 BLOCKED。
只有一个线程抢到了这把锁
查询线程池的256多个线程全部卡在一个锁上,也就是卡在了创建String这一句代码上 new String(bytes, dataStart, i - dataStart, Constants.DEFAULT_CHARSET) 【备注:Constants.DEFAULT_CHARSET = “UTF-8”】
然后再翻了翻代码,发现是 StringCoding 类的 lookupCharset(“UTF-8”) 获取 Charset 对象时,导致的锁等待,看来大量的对字符串 lookupCharset 会出严重的锁等待问题
有了猜测,我们继续往下看,追到最后一层。FastCharsetProvider.charsetForName
果然上锁了, 还是锁了 this 对象 是 FastCharsetProvider 类型的对象。 那我们再出去看看 这个 this 对象是哪一个。
很明显这是一个全局静态变量,对一个全局静态变量加锁,那么在高并发场景下,对性能的损耗可想而知。
既然已经很明确了是 lookupCharset 方法导致的 锁等待,那么问题来了,如何避免呢, 其实 new String 是有重载方法的,只要用这个方法就可以避免锁等待问题
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}
问题解决办法
问题分析清楚了,解决起来就简单了,
只要改成 new String(bytes, dataStart, i - dataStart, Charsets.UTF_8) 【备注:public static final Charset UTF_8 = Charset.forName(“UTF-8”);】
总结
这是一个多线程并发 大量的 new String 抢占同一个锁(全局静态变量),导致大量线程锁等待的问题,产生了死锁的假象。
感觉这是一个正常人都经常会范的错误,就算你的代码的并发频率没有那么高,不足以引发这个问题,但是由于他底层是上锁的会让线程BLOCK,所以对性能方面的损害还是非常大的。建议还是改了吧。