设为首页 加入收藏

TOP

侦探剧场:堆内存神秘溢出事件(五)
2018-03-06 09:03:08 】 浏览:1131
Tags:侦探 剧场 内存 神秘 溢出 事件
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作者写了个方法,期望该方法的使用者,传入参数时,一定符合他的约定。

而使用者在使用该方法时,也都严格按照该约定进行传参。

但是中间杀出了个程咬金,多线程环境下结果的不确定性,最终导致了严重的堆内存泄漏问题。

案件已经侦破,那么各位针对上面的问题,有什么好的解决方法吗?当然直接换线程安全的日期格式化工具是非常有必要的,方法中做参数的必要性校验也是有必要的。

插曲

归档

  1. 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到目标虚拟机后,真正逻辑的执

首页 上一页 2 3 4 5 6 下一页 尾页 5/6/6
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇如何编写相对标准的后端项目(二.. 下一篇从 Java9 共享内存加载 modules ..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目