出处 http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html
上周在线上系统发现了两个bug,值得记录下查找的过程和原因。以后如果还有查找bug比较有价值的经历,我也会继续分享。
第一个bug的起始,是在线上日志发现一个频繁打印的异常——java.lang.ArrayIndexOutOfBoundsException。但是却没有堆栈,只有一行一行的ArrayIndexOutOfBoundsException。没有堆栈,不知道异常是从什么地方抛出来的,也就不能找到问题的根源,更谈不上解决。题外,工程师在用log4j记录错误异常的时候,我看到很多人这样用(假设e是异常对象):
或者:
这样的写法是不对,只记录了异常的信息,而没有将堆栈输出到日志,正确的写法是利用error的重载方法:
这样才能在日志中完整地输出异常堆栈来。如何写好日志是另一个话题,这里不展开。继续我们的找bug经历。刚才提到,我们线上日志一直出现一行错误信息ArrayIndexOutOfBoundsException却没有堆栈,是我们没有正确地写日志吗?检查代码不是的,这个问题其实是跟JDK5引入的一个新特性有关,对于一些频繁抛出的异常,JDK为了性能会做一个优化,在JIT重新编译后会抛出没有堆栈的异常。在使用server模式的时候,这个优化是开启的,我们的服务器跑在server模式下并且jdk版本是6,因此在频繁抛出ArrayIndexOutOfBoundsException异常一段时间后优化开始起作用,只抛出没有堆栈的异常信息了。
那么怎么解决这个问题呢?因为这个优化是在JIT重新编译后才起作用,因此一开始抛出异常的时候还是有堆栈的,所以可以查看较旧的日志,寻找有完整堆栈的异常信息。但是由于我们的日志太大,会定期删除,我们的服务器也启动了很长时间,因此查找日志不是很靠谱的方法。
另一个解决办法是暂时禁用这个优化,强制要求每次都要抛出有堆栈的异常。幸好JDK提供了选项来关闭这个优化,配置JVM参数
就可以禁止这个优化(注意选项中的减号,加号是启用)。
我们找了台机器,配置了这个参数并重启一下。过了一会就找到问题所在,堆栈类似这样
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的误用引起的。SimpleDateFormat的javadoc中有这么句话:
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方法:
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
return format.format(date);
}
第二个方法是只创建一个instance,但是在调用方法的时候做同步:
public static synchronized String formatDate2(Date date) {
return formatter.format(date);
}
第三个方法比较特殊,我们为每个线程都缓存一个instance,存放在ThreadLocal里,使用的时候从ThreadLocal里取就可以了:
@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参数:
设置堆最大为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,要么做同步,两者的性能有差距,但是这个差距很难成为你的性能瓶颈。
相关推荐
这篇博客将深入探讨`DateFormat`在多线程环境下的问题及其解决方案。 `DateFormat`类在设计时并未被声明为线程安全的。这意味着当多个线程同时访问和修改同一个`DateFormat`实例时,可能会出现竞态条件,导致结果不...
Java中的ThreadLocal是解决线程安全问题的一个重要工具,它提供了一种在多线程环境下为每个线程维护独立变量副本的方法,从而避免了共享状态带来的竞态条件和线程安全问题。 线程安全问题通常由全局变量和静态变量...
然而,`DateFormat`并不是线程安全的,这意味着在多线程环境中直接使用可能会导致数据不一致或者异常。这篇博客文章《Java DateFormat并发实现》探讨了这个问题以及如何在并发环境下正确地使用`DateFormat`。 `...
在Java多线程编程中,`DateFormat`类是一个常见的日期和时间格式化工具,但它并不是线程安全的。这意味着在多线程环境下直接共享`DateFormat`实例可能会导致数据不一致或者异常。根据Java官方文档的建议,每个线程...
如果需要在多线程环境下使用SimpleDateFormat,可以使用ThreadLocal或FastDateFormat确保线程安全。 Java SimpleDateFormat线程安全问题是Java开发中一个常见的问题。通过使用ThreadLocal或FastDateFormat,我们...
如果需要在多线程环境下使用SimpleDateFormat,可以使用ThreadLocal变量来保存每个线程的SimpleDateFormat实例。 知识点6: DateFormat和SimpleDateFormat类的区别 DateFormat和SimpleDateFormat类都是Java中用于...
要在高并发环境下能有比较好的体验,可以使用ThreadLocal来限制SimpleDateFormat只能在线程内共享,这样就避免了多线程导致的线程安全问题。 ```java private static ThreadLocal<DateFormat> threadLocal = new ...
这意味着,在多线程环境下直接复用一个`SimpleDateFormat`实例可能会导致数据错误或异常。 **3.2 解决方案** 1. **每次 new 一个新的对象**:这是最简单但不是最高效的解决办法。 示例代码: ```java public ...
定时计划任务功能在Java中主要使用的就是Timer对象,它在内部使用多线程的方式进行处理,所以它和多线程技术还是有非常大的关联的。在JDK中Timer类主要负责计划任务的功能,也就是在指定的时间开始执行某一个任务。 ...
这本书全面地介绍了如何在Java平台上高效、安全地编写多线程程序,帮助读者理解并解决并发编程中的各种挑战。 首先,我们需要理解“并发”这个概念。并发是指在一段时间内,多个任务可以同时进行或交替执行。在单...
- 在高并发或多线程环境下使用 `DateFormat` 时,由于它是非线程安全的,需要通过同步机制来避免潜在的问题。例如,可以通过使用 `ThreadLocal` 来缓存每个线程的 `DateFormat` 实例。 4. **安全性问题**: - 当...
需要注意的是,`SimpleDateFormat`不是线程安全的,所以在多线程环境中,如果多个线程同时使用一个`SimpleDateFormat`实例,可能会出现错误。为了避免这个问题,可以在每个线程内部创建单独的实例,或者使用`...
25. **STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE**:在多线程环境中,不要在静态 DateFormat 实例上调用方法。创建线程安全的日期格式化器,如使用 ThreadLocal。 26. **NP_NULL_PARAM_DEREF_NONVIRTUAL**:非...
3. **线程安全**:`SimpleDateFormat`不是线程安全的,因此在多线程环境下,应该为每个线程创建一个实例或者使用`ThreadLocal`来存储实例。 #### 五、最佳实践 1. **使用常量**:对于常用的日期格式,建议定义为...
5. **多线程**:Java 6.0的线程管理更加成熟,新增了`ThreadLocal`类,提供线程局部变量,减少了数据同步的复杂性。`ExecutorService`和`Future`接口提供了更高级的线程池管理。 6. **国际化**:`java.text`和`java...
在实际开发中,如果你不想每次都创建新的`SimpleDateFormat`实例,可以考虑使用`ThreadLocal<SimpleDateFormat>`来存储线程局部的`SimpleDateFormat`,避免多线程环境下的同步问题。 总结起来,`java.text....
1.6.3 初学者容易犯的错误 18 1.7 垃圾回收机制 20 1.8 何时开始使用IDE工具 21 学生提问:老师,我想学习Java编程,到底是学习Eclipse好呢,还是学习JBuilder好呢? 21 1.9 本章小结 22 本章练习 22 第2章 ...
需要注意的是,`SimpleDateFormat`不是线程安全的,因此在多线程环境中使用时,要么为每个线程创建独立的实例,要么使用`ThreadLocal`来存储实例。 此外,`SimpleDateFormat`也存在一些效率问题,因为它的解析过程...