ThreadLocal解决dateFormat多线程错误

标签: threadlocal dateformat 多线程 | 发表时间:2016-09-19 17:44 | 作者:yunlong167167
出处:http://www.iteye.com

出处  http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html

    上周在线上系统发现了两个bug,值得记录下查找的过程和原因。以后如果还有查找bug比较有价值的经历,我也会继续分享。
    第一个bug的起始,是在线上日志发现一个频繁打印的异常——java.lang.ArrayIndexOutOfBoundsException。但是却没有堆栈,只有一行一行的ArrayIndexOutOfBoundsException。没有堆栈,不知道异常是从什么地方抛出来的,也就不能找到问题的根源,更谈不上解决。题外,工程师在用log4j记录错误异常的时候,我看到很多人这样用(假设e是异常对象):

log.error("发生错误:"+e);

或者:

log.error("发生错误:"+e.getMessage());

    这样的写法是不对,只记录了异常的信息,而没有将堆栈输出到日志,正确的写法是利用error的重载方法:

log.error("xxx发生错误",e);


    这样才能在日志中完整地输出异常堆栈来。如何写好日志是另一个话题,这里不展开。继续我们的找bug经历。刚才提到,我们线上日志一直出现一行错误信息ArrayIndexOutOfBoundsException却没有堆栈,是我们没有正确地写日志吗?检查代码不是的,这个问题其实是跟JDK5引入的一个新特性有关,对于一些频繁抛出的异常,JDK为了性能会做一个优化,在JIT重新编译后会抛出没有堆栈的异常。在使用server模式的时候,这个优化是开启的,我们的服务器跑在server模式下并且jdk版本是6,因此在频繁抛出ArrayIndexOutOfBoundsException异常一段时间后优化开始起作用,只抛出没有堆栈的异常信息了。



    那么怎么解决这个问题呢?因为这个优化是在JIT重新编译后才起作用,因此一开始抛出异常的时候还是有堆栈的,所以可以查看较旧的日志,寻找有完整堆栈的异常信息。但是由于我们的日志太大,会定期删除,我们的服务器也启动了很长时间,因此查找日志不是很靠谱的方法。
    另一个解决办法是暂时禁用这个优化,强制要求每次都要抛出有堆栈的异常。幸好JDK提供了选项来关闭这个优化,配置JVM参数

-XX:-OmitStackTraceInFastThrow

    就可以禁止这个优化(注意选项中的减号,加号是启用)。

    我们找了台机器,配置了这个参数并重启一下。过了一会就找到问题所在,堆栈类似这样

Caused by: java.lang.ArrayIndexOutOfBoundsException: -1831238
    at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:436)
    at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2081)
    at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:1996)
    at java.util.Calendar.setTimeInMillis(Calendar.java:1109)
    at java.util.Calendar.setTime(Calendar.java:1075)
    at java.text.SimpleDateFormat.format(SimpleDateFormat.java:876)
    at java.text.SimpleDateFormat.format(SimpleDateFormat.java:869)
    at java.text.DateFormat.format(DateFormat.java:316)


    读者肯定猜到了,这个问题是由于SimpleDateFormat的误用引起的。 SimpleDateFormatjavadoc中有这么句话:

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. 

    但是很悲剧的是这句话是放在整个doc的最后面,在我看来,这句话应该放在最前面才对。简单来说就是SimpleDateFormat不是线程安全的,你要么每次都new一个来用,要么做加锁来同步使用。

    出问题的代码就是由于工程师认为SimpleDateFormat的创建代价很高,然后搞了个map做缓存,所有线程共用这个instance做format,同时没有做同步。悲剧就诞生了。
    这里就引出我想提到的第二点问题,在使用一个类或者方法的时候 ,最好能详细地看下该类的javadoc,JDK的javadoc是做的非常好的,javadoc除了做说明之外,通常还会给示例,并且会点出一些关键问题,如线程安全性和平台移植性。

    最后,我将做个测试,到底在使用SimpleDateFormat怎么做才是最好的方式?假设我们要实现一个formatDate方法将日期格式化成"yyyy-MM-dd"的格式。
    第一个方法是每次使用都创建一个instance,并调用format方法:

   public static String formatDate1(Date date) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        return format.format(date);
    }


    第二个方法是只创建一个instance,但是在调用方法的时候做同步:

   private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");

    public static synchronized String formatDate2(Date date) {
        return formatter.format(date);
    }


    第三个方法比较特殊,我们为每个线程都缓存一个instance,存放在ThreadLocal里,使用的时候从ThreadLocal里取就可以了:

   private static ThreadLocal<SimpleDateFormat> formatCache = new ThreadLocal<SimpleDateFormat>() {

        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }

    };

    public static String formatDate3(Date date) {
        SimpleDateFormat format = formatCache.get();
        return format.format(date);
    }

另外一种写法:

package com.peidasoft.dateformat;

 

import java.text.DateFormat;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;

 

public class ThreadLocalDateUtil {

    private static final String date_format = "yyyy-MM-dd HH:mm:ss";

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); 

 

    public static DateFormat getDateFormat()   

    {  

        DateFormat df = threadLocal.get();  

        if(df==null){  

            df = new SimpleDateFormat(date_format);  

            threadLocal.set(df);  

        }  

        return df;  

    }  

 

    public static String formatDate(Date date) throws ParseException {

        return getDateFormat().format(date);

    }

 

    public static Date parse(String strDate) throws ParseException {

        return getDateFormat().parse(strDate);

    }   

}

    然后我们测试下三个方法并发调用下的性能并做一个比较,并发100个线程循环调用1000万次,记录耗时。我们设置了JVM参数:

-Xmx512m -XX:CompileThreshold=10000

    设置堆最大为512M,设置当一个方法被调用1万次的时候就被JIT编译。测试的结果如下:

   第1次测试 第2次测试 第3次测试
formatDate1 50545 49365 53532
formatDate2 10895 10761 10673
formatDate3 10386 9919 9527

(单位:毫秒)
    
    从结果来看,方法1最慢,方法3最快,但是就算是最慢的方法1也可以达到每秒钟200 20万次的调用量,很少有系统能达到这个量级。这个点很难成为你系统的瓶颈所在。从我的角度出发,我会建议你用方法1或者方法2,如果你追求那么一点性能提升的话,可以考虑用方法3,也就是用ThreadLocal做缓存。

    总结下本文找bug经历想表达的几点想法:
(1)正确地打印错误日志
(2)在server模式下,最好都设置-XX:-OmitStackTraceInFastThrow
(3)使用类或者方法的时候,最好能详细阅读下javadoc,很多问题都能找到答案
(4)使用SimpleDateFormat的时候要注意线程安全性,要么每次new,要么做同步,两者的性能有差距,但是这个差距很难成为你的性能瓶颈。



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [threadlocal dateformat 多线程] 推荐:

ThreadLocal解决dateFormat多线程错误

- - 研发管理 - ITeye博客
出处  http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html.     上周在线上系统发现了两个bug,值得记录下查找的过程和原因. 以后如果还有查找bug比较有价值的经历,我也会继续分享.     第一个bug的起始,是在线上日志发现一个频繁打印的异常——java.lang.ArrayIndexOutOfBoundsException.

ThreadLocal介绍

- - ITeye博客
一、java.lang.ThreadLocal. 一个实例就是一个容器,所有可以访问到这个实例的线程都可以在这个容器中存储一个该线程独立使用的变量. 这个实例里面其实是一个Map结构的属性,存储以线程对象为KEY,变量为VALUE的数据. 二、ThreadLocal有这样几个方法:. 返回当前线程对应的那个变量.

正确理解ThreadLocal

- - Java - 编程语言 - ITeye博客
转自: http://www.iteye.com/topic/103804. 首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的. 另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本.

ThreadLocal的内存泄露

- - zzm
ThreadLocal的目的就是为每一个使用ThreadLocal的线程都提供一个值,让该值和使用它的线程绑定,当然每一个线程都可以独立地改变它绑定的值. 如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程序.. 关于的ThreadLocal更多内容,请参考《 ThreadLocal》.

(转)ThreadLocal的内存泄漏问题

- - 编程语言 - ITeye博客
原文:http://www.godiscoder.com/?p=479. 在最近一个项目中,在项目发布之后,发现系统中有内存泄漏问题. 表象是堆内存随着系统的运行时间缓慢增长,一直没有办法通过gc来回收,最终于导致堆内存耗尽,内存溢出. 开始是怀疑ThreadLocal的问题,因为在项目中,大量使用了线程的ThreadLocal保存线程上下文信息,在正常情况下,在线程开始的时候设置线程变量,在线程结束的时候,需要清除线程上下文信息,如果线程变量没有清除,会导致线程中保存的对象无法释放.

java 使用动态代理 - ThreadLocal实现事务管理

- - ITeye博客
动态代理:JDK动态代理只能对实现了接口的类进入代理,采用JDK动态代理必须实现InvocationHandler接口,采用Proxy 类创建相应的代理类.. 下面使用Model2(MVC)使用代理事务查询用户基本信息,使用DB2数据库:. 建立表: create table T_USER (. constraint P_KEY_1 primary key (USER_ID) ); 初始化数据: insert into t_user(user_id, user_name, password) values('root', '系统管理员', 'root');.

POJO中使用ThreadLocal实现Java嵌套事务

- - ImportNew
大多嵌套事务都是通过EJB实现的,现在我们尝试实现对POJO的嵌套事务. 这里我们使用了ThreadLocal的功能. 所以内层事务或外层事务可以在不影响其他事务的条件下进行回滚或提交. 新建的事务嵌套在外层事务中. 如果内层事务完成(不论是回滚或是提交),外层的事务就可以进行回滚或提交,这样的操作并不会影响内层事务.

ThreadLocal原理及其实际应用 - format丶

- - 博客园_首页
java猿在面试中,经常会被问到1个问题:. java实现同步有哪几种方式. 大家一般都会回答使用synchronized, 那么还有其他方式吗. 答案是肯定的, 另外一种方式也就是本文要说的ThreadLocal. ThreadLocal介绍. ThreadLocal, 看名字也能猜到, "线程本地", "线程本地变量".

Java Thread多线程

- - CSDN博客推荐文章
Java Thread多线程. Java 多线程例子1 小例子. super("zhuyong");//设置线程的名字,默认为“TestThread”. Java 多线程例子2 前台线程(用户线程) 后台线程(守护线程 ). 1,setDaemon(true)后就是后台线程(守护线程 ),反之就是前台线程(用户线程).

Java多线程之synchronized

- - CSDN博客推荐文章
这里通过三个测试类阐述了synchronized应用的不同场景. 首先是最基本的synchronized Method的使用.  * @see 概述:Java中的每个对象都有一个锁(lock)或者叫做监视器(monitor) .  * @see 说明:当synchronized关键字修饰一个方法时,则该方法为同步方法 .