记一次多线程奇怪的现象

做了一个数据交换的程序,程序中使用JAXB来进行xml文档的解析,上级要求程序可以配置解析的线程数量,以在高峰值的时候提高解析速度——此为背景说明。
某天,测试跑来和我说,你这程序的处理效率太低了,单线程1分钟才处理了120条数据,增加了10条线程,也就是130条,处理能力没有明显的提升,这样不行啊。


处理速度慢

我就很奇怪了,第一个反应是是不是代码敲错了多线程没有启动,还是单线程在跑。又或者有哪个方法是同步的造成了线程的等待。

对于第一种猜测,我在运行时增加了日志,在一些节点上增加了Thread.sleep(),通过日志判断确实是有多个线程在运行解析任务,排除这个原因。
对于第二种猜测,在log的基础上查看各个方法的用时。发现了一个很奇怪的现象。用JAXB把string类型的xml文档转成对应的pojo,这个方法在只有1个线程的情况下只要30毫秒,而10个线程的情况下居然要3秒多才能运行完。也就是说这个方法的耗时是不是一个常数值,他会随着线程数线性增长!

Package xmlPackage = jaxbUtil.convertXmlStrToJavaBean(messageContent, path, Package.class);

解决思路

找到原因了,那么问题就解决了一半,先看一下这个方法的内部实现

    public static <T> T convertXmlStrToJavaBean(String xmlStr, String xmlExpress, Class<T> clazz)
            throws XPathExpressionException,ParserConfigurationException, SAXException,IOException,JAXBException {
        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        Document document = builder.parse(new ByteArrayInputStream(xmlStr.getBytes()));
        XPath xpath = XPathFactory.newInstance().newXPath();
        Node widgetNode = (Node) xpath.evaluate(xmlExpress, document, XPathConstants.NODE);

        JAXBContext context = JAXBContext.newInstance(clazz);
        Binder<Node> binder = context.createBinder();
        return (T)binder.unmarshal(widgetNode);
    }

分析一下这个方法,每次调用的时候DocumentBuilder 、XPath 、JAXBContext 、Binder。4个东西都是需要重新创建的,而我的clazz对象是固定的Package.class所以应该也能抽取出来。所以我的第一版改进就是把这4个对象定义成了类的私有成员变量,方法改为非静态了,然后新建了一个jaxbUtil类的bean,把他注入了spring中。
其实, JAXBContext context = JAXBContext.newInstance(clazz); 只有这句语句运行时间会增长,其他语句的初始化时间基本可以忽略。具体为什么实例化的时间会随着线程数的增加而增加,我也没弄明白。


多线程

再次测试,速度已经起飞了10个线程500条数据2秒不到就处理完了。愉快的提交了测试~
过了一会测试再次反馈,速度是达标了,但是出现了无法解析的错误,需要赶紧处理。我拉下数据一看,各种问题有空指针,非法参数,考虑到我刚刚的修改,只了初始化了一个对象却有多个线程需要用,会不会线程不安全呢。于是我调了一个异常

org.xml.sax.SAXException: FWK005 parse may not be called while parsing.
        at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:263)
        at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:28)
        at javax.xml.parsers.DocumentBuilder.parse(DocumentBuilder.java:208)

从程序逻辑来看,如果当前DocumentBuilder对象正在转换文档,此时再次请求转换文档,那么直接抛出XNIException(“FWK005 parse may not be called while parsing.”);异常。
这个问题也比较好解决,一种是对转换xml文档的方法,增加synchronized关键字,这样子不会有两个线程同时访问方法。
还有一种方法是创建一个DocumentBuilder类型的ThreadLocal变量,这样子每个线程都拥有自己的
DocumentBuilder对象,能够同时转换多个xml文件。代码如下:

private static ThreadLocal docBuildeIns = new ThreadLocal() {
    protected DocumentBuilder initialValue() {
        try {
            return DocumentBuilderFactory.newInstance().newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            String msg = "DocumentBuilder 对象初始化失败!";
            log.error(msg, e);
            throw new IllegalStateException(msg, e);
        }
    } 
};

ok了同步是肯定不能用了,定义ThreadLocal变量吧,跟着这个思路,改了4个ThreadLocal变量,问题得到完美的解决!


这里篇文章主要还是记录一下多线程中ThreadLocal的用法,在多线程的时候需要多考虑一下调用的方法是否是线程安全的。

感谢以下文章作者
Java并发编程:深入剖析ThreadLocal
ThreadLocal-面试必问深度解析

猜你喜欢

转载自www.cnblogs.com/idelo/p/9578118.html
今日推荐