rviceDateUtil.getBetweenDates(startTime, endTime)
public static List<String> getBetweenDates(String begin, String end) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
List<String> betweenList = new ArrayList<String>();
try {
Calendar startDay = Calendar.getInstance();
startDay.setTime(format.parse(begin));
startDay.add(Calendar.DATE, -1);
while (true) {
startDay.add(Calendar.DATE, 1);
Date newDate = startDay.getTime();
String newend = format.format(newDate);
betweenList.add(newend);
if (end.equals(newend)) {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
betweenList.remove(begin);
betweenList.remove(end);
return betweenList;
}
看到这段代码时,突然一道光穿透了小光的两耳(此处配名侦探柯南发现真相图)。下面那个while (true)非常可疑!
分析代码,循环终止条件是end.equals(newend)。newend则是startDay每次循环加一的结果,那么就是在某种情况下无法触发循环结束条件。
把请求中参数的”docStartTime”:["2018-01-08 08:22:50"],”docEndTime”:["2018-01-10 08:22:56"]作为参数传入方法调用后,直接死循环了…结果是因为判断条件是拿全部String做的equals。而newend是经过日期格式化的,只取日期那一部分的值,而end则不做任何变化,那么一个日期与一个日期时间,做字符串equals是肯定不会相等的,必然会死循环。
到这里,小光以为案件已破。但是仔细一想,这样的话岂不是所以进到这里的逻辑,都会直接触发死循环,这一点又解释不通。
查看调用该方法传入的参数,发现start和end其实是经过格式化的日期字符串
String startTime = DateUtil.formatDate(attenceDoc.getDocStartTime());
String endTime = DateUtil.formatDate(attenceDoc.getDocEndTime());
继续分析equals条件,当传入参数start大于传入参数end时,就永远不会触发结束条件。什么情况下会start大于end呢。
此时,又一道光闪过小光的眼前!DateFormat是线程不安全的,当多个线程使用同一个DateFormat时,有可能导致格式化后的结果不是自己想要的!看DateUtil.formatDate的代码
private static final DateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public static String formatDate(Date date) {
return DEFAULT_DATE_FORMAT.format(date);
}
果然与猜想的一样,此处使用了一个静态的DEFAULT_DATE_FORMAT来格式化日期,当多线程操作时,会导致start变为其他线程正在操作的时间。若此时间正好比end大,那么就会触发死循环。关于DateFormat线程不安全,参考归档3.
再看死循环逻辑,往一个list中丢String对象,与最开始jmap结果也一致!
3. 还原作案手法
在多线程并发环境下,并发越高,上面的问题越容易重现。这也解释了之前到月底发生该问题的现象,月底考勤系统并发增加,问题出现概率增高。
通过构建多线程共用同一个DateFormat,开启500个线程同时Format日期2018.01.12,主线程调用两次Format,分别Format 2018.01.08和2018.01.10,之后作为参数传入getBetweenDates,尝试几次后,果然重现
public static void main(String[] args) {
Date start = DateUtil.parseDate("2018.01.08");
Date end = DateUtil.parseDate("2018.01.10");
final Date other = DateUtil.parseDate("2018.01.12");
for (int i = 0; i < 500; i++) {
new Thread(() -> DateUtil.formatDate(other)).start();
}
ServiceDateUtil.getBetweenDates(DateUtil.formatDate(start), DateUtil.formatDate(end));
}
4. 真相大白
至此,凶器已被找到,证据确凿,嫌疑人也已无法狡辩,只得乖乖俯首认罪。而小光也说出了那就他早就想说的话:真相永远只有一个!
后记
思考上面问题出现的原因。
Util作者写了个方法,期望该方法的使用者,传入参数时,一定符合他的约定。
而使用者在使用该方法时,也都严格按照该约定进行传参。
但是中间杀出了个程咬金,多线程环境下结果的不确定性,最终导致了严重的堆内存泄漏问题。
案件已经侦破,那么各位针对上面的问题,有什么好的解决方法吗?当然直接换线程安全的日期格式化工具是非常有必要的,方法中做参数的必要性校验也是有必要的。
插曲
归档
- JVisualVM长时间无法连接、工具Attach失败的原因:
首先要了解一下这些工具执行的原理,为何会报出Attach失败?
这些工具的执行原理都是基于Sun公司的Attach API。通过com.sun.tools.attach.VirtualMachine类,调用attach(pid)方法,attach到目标JVM上。
那么目标JVM又是怎么觉察到attach进来一个工具呢?通过线程Signal Dispatcher来实现。当attach时,会对目标线程发出一个信号量,该信号量交由该线程处理,信号量处理程序发现是attach信号,检测Attach Listener线程是否启动,如果没有启动则启动一个Attach Listener线程,attach的具体操作交由Attach Listener执行。
attach到目标虚拟机后,真正逻辑的执