部分程序员可能都遇到过SimpleDateFormat的线程安全问题,在JDK文档中也说明了该类是线程非安全的,建议对于每个线程都创建一个SimpleDateFormat对象。那么为什么SimpleDateFormat会有线程安全问题呢?已经如何重现SimpleDateFormat线程安全问题和如何解决将是本文的重点。
SimpleDateFormat产生线程不安全的原因
我们从java的源代码开始,查看SimpleDateFormat和DateFormat对parse方法的实现部分。整个方法的代码片段翻译一下大致功能如下:
Date parse() {
calendar.clear(); // 清理calendar
... // 执行一些操作, 设置 calendar 的日期什么的
calendar.getTime(); // 获取calendar的时间
}
SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用,这里会导致的问题就是, 如果 线程A 调用了 sdf.parse(), 并且进行了 calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用sdf.parse()并顺利i结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date。
重现SimpleDateFormat线程安全问题
下面我们将通过以下代码对SimpleDateFormat线程安全的问题进行重现:
public class DateFormatTest extends Thread {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
//:www.xttblog.com
private String name;
private String dateStr;
private boolean sleep;
public DateFormatTest(String name, String dateStr, boolean sleep) {
this.name = name;
this.dateStr = dateStr;
this.sleep = sleep;
}
@Override
public void run() {
Date date = null;
if (sleep) {
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
date = sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(name + " : date: " + date);
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newCachedThreadPool();
// A 会sleep 2s 后开始执行sdf.parse()
executor.execute(new DateFormatTest("A", "1991-09-13", true));
// B 打了断点,会卡在方法中间
executor.execute(new DateFormatTest("B", "2013-09-13", false));
executor.shutdown();
}
}
使用Debug模式执行这段代码,并在sdf.parse()方法里打上断点,操作步骤如下:
- 首先A线程跑起来以后会进入sleep
- B线程跑起来, 卡在断点处
- A线程醒过来, 执行 calendar.clear(), 并将设置sdf.calendar的date为1991-09-13, 此时 A B 的 calendar 都为 1991-09-13
- 让断点继续执行, 输出如下
A : date: Fri Sep 13 00:00:00 CDT 1991 B : date: Fri Sep 13 00:00:00 CDT 1991
上面的结果可以说明一切了,当然这样的测试可能还不够明显,下面我们再来一段代码进行测试,如下:
@Test
public void testUnThreadSafe() throws Exception {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,S");
//:www.xttblog.com
final String[] dateStrings = {
"2014-04-30 18:51:01,61",
"2014-04-30 18:51:01,461",
"2014-04-30 18:51:01,361",
"2014-04-30 18:51:01,261",
"2014-04-30 18:51:01,161",
};
int threadNum = 5;
Thread[] parseThreads = new Thread[threadNum];
for (int i=0; i<threadNum; i++) {
parseThreads[i] = new Thread(new Runnable() {
public void run() {
for (int j=0; j<dateStrings.length; j++) {
try {
System.out.println(Thread.currentThread().getName() + " " + sdf.parse(dateStrings[j]));
} catch (ParseException e) {
e.printStackTrace();
}
}
}
});
parseThreads[i].start();
}
for (int i=0; i<threadNum; i++) {
parseThreads[i].join();
}
}
执行这个方法,将会抛出异常:java.lang.NumberFormatException: multiple points
SimpleDateFormat线程安全问题的解决办法
我将解决方案总结为以下几种:
- 对SimpleDateFormat实例的相关部分代码进行加锁处理
- SimpleDateFormat实例不要使用static修饰,也不要使用全局变量
- 使用ThreadLocal
- 每次都new一个SimpleDateFormat实例
在并发情况下,网站的请求任务与线程执行情况大概可以理解为如下:

如果在并发请求很高的时候,我们就需要特别注意了,上面的第三种方法是我推荐的使用方式。其他的做法不是不彻底就是太消耗性能。比如每一个线程都new一个SimpleDateFormat,太浪费资源了。因此,我建议使用ThreadLocal创建一个对单个线程来说全局的变量,保证线程安全,当然可以使用第三方工具类如Apache commons 里的FastDateFormat或者Joda-Time类库来处理。
版权声明:本文为博主原创文章,未经博主允许不得转载。

: » 从SimpleDateFormat的实现原理讲线程安全问题与解决方案
原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/251476.html