高并发下的九死一生,一个不小心就掉入万丈深渊

引言

每次开篇LZ都会先说一下自己最近很忙,所以才没有时间写博客。这次这些话就不多说了,我们直入主题,尽管LZ依旧是非常的繁忙…

了解LZ的猿友应该都知道,LZ最近弄了一个hbase(不理解hbase的猿友可以把hbase当做与oracle,mysql,sqlserver等一样的数据库,并不影响阅读本文)的大数据平台,或许现在叫平台还有点名不副实,不过它很快就会发展到这个规模,LZ一直坚信着。在建立这个平台的过程中,LZ遇到过各种千奇百怪的问题,在这里LZ就分享一个非常简单,但却很奇葩的问题。 

问题来源

问题的来源特别简单,LZ为了迎合模块化开发的思想,做了很多独立的模块,这些模块以jar包的形式协同工作,类似于spring当中的spring-core,spring-beans,spring-context等等。

在LZ的一个common包中,有这样的一个工具类,代码如下。(备注:LZ为了简单,去掉了很多跟本文无关的代码,但不影响阅读,因为这个类就是一些静态的工具类方法,主要用于处理日期)

public class DateUtil {

    private DateUtil(){}

    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String date)  throws ParseException {
        return DATE_FORMAT.parse(date);
    }    

}

这个类特别简单,相信有不少猿友都会觉得这个类没有多大问题。因为这段代码太简单了,当你的项目报错的时候,你很难想到这段代码就是错误的根源。很显然,LZ就在hbase的应用中使用了这个工具类,结果就导致了一个奇葩问题。

大致描述一下这个工具类使用的场景。LZ的hbase应用接收了来自于其它系统大量的日志信息,并会将这些日志信息存储在hbase当中,其实就是一个简单的日志保存功能。如果单纯从功能上来讲,就是一个简单的curd(增删改查)操作中的c(增)操作。唯一不同的是,由于存储的是来自很多系统的系统日志,webservice日志,mq日志,url访问日志,因此并发量会有点高,至少比LZ平时做的企业应用要高太多太多了。

这个工具类就是在解析日志信息中的日期字符串(比如日志的发生时间)时报的错,具体的错误信息如下。(备注:以下是真实的报错信息,显示的错误位置与上面的代码不符,不过各位猿友完全可以认为就是上面的方法报的错,因为事实上parseTimestamp这个方法就和上面方法的代码是一样的。)

java.lang.NumberFormatException: For input string: ""
        at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
        at java.lang.Long.parseLong(Long.java:431)
        at java.lang.Long.parseLong(Long.java:468)
        at java.text.DigitList.getLong(DigitList.java:177)
        at java.text.DecimalFormat.parse(DecimalFormat.java:1297)
        at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
        at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
        at java.text.DateFormat.parse(DateFormat.java:335)
        at com.xxxxxxx.core.common.util.DateUtil.parseTimestamp(DateUtil.java:95)
        at com.xxxxxxx.core.common.util.DateUtil.parse(DateUtil.java:84)
        at com.xxxxxxx.hbase.generator.LogRowKeyGenerator.generate(LogRowKeyGenerator.java:21)
        ... 22 more

问题分析 

看到这个错误,大部分老道一点的程序猿一眼就能定位问题,肯定是传过来的日期格式不对,所以导致在解析的时候出错了。

LZ自认为还算老道吧(小小的自夸一下),自然也很快的意识到了问题的根源。于是最简单的方式,调试一下代码,看传过来的日志信息到底是什么样子。

LZ在catch块里加入了断点,当报出这个错误的时候,会进入调试(只能在catch块里捕捉,因为这个异常是时而出现的,而且毫无规律)。但是结果很意外,LZ仔细且认真的看了传送过来的日志信息,日期格式却明明是正确的。这时候LZ就傻眼了,格式明明是正确的,解析怎么可能报错呢?

LZ不相信这种奇怪的问题,于是LZ采用最简单的办法,希望印证心中所想,将代码改成如下的样子。

public class DateUtil {

    private DateUtil(){}

    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String date)  throws ParseException {
        System.out.println("date:" + date);
        return DATE_FORMAT.parse(date);
    }    

}

这是最简单粗暴的调试方式,也是LZ初入程序猿这个职业时经常用的办法。可惜,结果依然不如人意,当偶尔出现异常时,打印出来的日志格式依旧是正确的。事实上,各个系统使用的客户端也是LZ开发的,也不应该出现日期格式错误的问题。

这到底怎么回事呢?事实就是,日期格式是正确的,但就是解析失败!

水落石出

LZ在想不明白一个问题的时候,习惯出来抽根烟,透透风。不过不得不说,这个办法真的好使,LZ一瞬间灵感就袭脑了。

这么奇葩的问题,也只有高并发可以解释了!

于是二话不说,扔掉烟头,LZ就回到电脑前打开了SimpleDateFormat这个类的源码。果然,在这个类的注释里,有这么一段话。

 * Date formats are not synchronized.
 * It is recommended to create separate format instances for each thread.
 * If multiple threads access a format concurrently, it must be synchronized
 * externally.

这段话的意思很简单,翻译过来就是:日期格式化的类是非同步的,建议为每一个线程创建独立的格式化实例。如果多个线程并发访问同一个格式化实例,就必须在外部添加同步机制。

由于LZ错误的将SimpleDateFormat的单个实例放置于高并发的环境下,并且没有任何同步机制,于是就导致了这个奇葩的问题。接下来,LZ便快速的将代码改成了类似于如下的形式。

public class DateUtil {

    private DateUtil(){}

    public static Date parse(String date)  throws ParseException {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(date);
    }    

}

果然,那个奇怪的异常再也不出现了,事情到此已经水落石出了。最后,LZ奉上一段示例代码,猿友们运行这个程序,就会出现解析失败的异常,但是很明显,我们的日期格式是正确的。

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DateUtil {

    private DateUtil(){}

    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(500);
        for (int i = 0; i < 500; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000000; i++) {
                        try {
                            DATE_FORMAT.parse("2014-01-01 00:00:00");
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
        }
        Thread.sleep(3000000);
    }

}

小结

高并发所引发的问题往往很难解决,因为它无法稳定的重现。比如本文中的问题,如果不是在高并发的情况下,可能你的程序运行半年甚至更久,都不一定能出现几次解析失败的异常。就算是偶尔出现,你也可能会以为是日期格式错误,从而忽略掉它,殊不知事实并非如此。

同样的功能,不同的人写出来的代码质量确实是有很大差距的。就算是本文中这么简单的一个日期工具类,一不小心都可能造成意料之外的错误。幸好JDK的代码写的足够规范,大部分类的线程安全性都写的很清楚,这才让LZ找到了问题根源。

相信当下有不少猿友认为自己做的项目或是写的代码没有什么技术含量,以至于每日浑浑噩噩,激情匮乏。但是本文就告诉你这样一个道理,不是因为项目让你不能发光,而是因为你才让项目不能发光



相关文章

发表评论

Comment form

(*) 表示必填项

13 条评论

  1. Zz 说道:

    写多线程,用之前都要看看是否线程安全才敢用

    Thumb up 0 Thumb down 0

  2. Lave 说道:

    SimpleDateFormat不是线程安全的所以这个不是因为高并发的情况下才会出现,是只要两个线程同时使用同一个SimpleDateFormat对象就会出现问题。

    你一开始的时候将SimpleDateFormat定义为静态变量应该是为了节省性能消耗,但是这种做法并不推荐,你可以在每次格式化的时候new一个新对象出来或者在使用ThreadLocal变量来确保不会出现多线程冲突的问题。

    另外,时间相关的操作建议使用Java 8新添加的Time包。

    Thumb up 0 Thumb down 0

  3. reg 说道:

    不知道LZ有没有看《Java并发编程实战》,里面有一句顺便提到过SimpleDateFormat类是非线程安全的。

    Thumb up 0 Thumb down 0

  4. bug 说道:

    建议使用DateTimeFormatter对象

    Thumb up 0 Thumb down 0

  5. liujun 说道:

    大哥,说了半天,跟hbase一毛钱关系都没有好吗,SimpleDateFormat不能在多线程环境下使用,这个很基础的问题!!

    Thumb up 1 Thumb down 0

  6. 盘古大神 说道:

    JDK1.8提供了DateTimeFormatter类支持并发,可以替代。

    Thumb up 0 Thumb down 0

  7. myyu 说道:

    这个问题以前在做项目时遇到过,系统运行了1年没出现过问题,后来通过多线程方法测试才找到问题原因。

    Thumb up 0 Thumb down 0

  8. lyrics 说道:

    我虽没有遇到你这个问题(高并发环境比较少),不过这个问题用findbug检测会提示非同步,看了你的文章明白findbug为什么建议SimpleDateFormat不要设成全局变量

    Thumb up 0 Thumb down 0

  9. Justin 说道:

    这个亲身遇到过,https://github.com/zmr518/java_SimpleDateFormat

    Thumb up 0 Thumb down 0

  10. 七月天 说道:

    共享对象时, 基本都会去看看类是不是线程安全的.
    谷歌guava库, joda-time, jackson这些常用的工具包, 在类的注释上都会标明是不是thread safe

    Thumb up 0 Thumb down 0

  11. Wangfox 说道:

    org.apache.commons.lang.time.DateUtils替代就可以了。

    Thumb up 0 Thumb down 0

  12. loredp 说道:

    楼主可以用threadlocal来实现,比new的方式性能上更加强大

    Thumb up 0 Thumb down 0

跳到底部
返回顶部