问题现象
写了一个例子测试-SimpleDataFormat
看下面的实验:
package com.programing.unsafe;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SimpleDateFormatThread extends Thread {
private SimpleDateFormat sdf;
private String dateString;
public SimpleDateFormatThread(SimpleDateFormat sdf, String dateString) {
this.sdf = sdf;
this.dateString = dateString;
}
@Override
public void run() {
//测试传入日期和转换日期是否一致
try {
//sdf = new SimpleDateFormat("yyyy-MM-dd"); //创建线程自己的SimpleDateFormat 才能解决这个问题
Date dateRef = sdf.parse(dateString); //使用公共的SimpleDateFormat 线程不安全
String newDateString = sdf.format(dateRef).toString();
if (!newDateString.equals(dateString)) {
System.out.println(Thread.currentThread().getName() + " 报错:" + dateString + " =>" + newDateString);
}else{
System.out.println(Thread.currentThread().getName() + " 正确:" + dateString + " =>" + newDateString);
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
package com.programing.unsafe;
import java.text.SimpleDateFormat;
public class SimpleDateFormatTest {
public static void main(String[] args) {
//从结果中可以查看到SimplateDateFormat类再多线程环境中处理日期很容易出现转换错误
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String[] dateStrings = new String[]{"2018-01-01", "2018-01-02", "2018-01-03", "2018-01-04", "2018-01-05", "2018-01-06", "2018-01-07", "2018-01-08", "2018-01-09"};
for (int i = 0; i < dateStrings.length; i++) {
SimpleDateFormatThread thread = new SimpleDateFormatThread(sdf, dateStrings[i]);
thread.start();
}
}
}
执行结果:中可以看到部分线程执行异常
Exception in thread "Thread-5" java.lang.NumberFormatException: multiple points
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1914)
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.base/java.lang.Double.parseDouble(Double.java:543)
at java.base/java.text.DigitList.getDouble(DigitList.java:169)
at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2128)
at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1933)
at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1541)
at java.base/java.text.DateFormat.parse(DateFormat.java:393)
at com.programing.unsafe.SimpleDateFormatThread.run(SimpleDateFormatThread.java:22)
Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1914)
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.base/java.lang.Double.parseDouble(Double.java:543)
at java.base/java.text.DigitList.getDouble(DigitList.java:169)
at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2128)
at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2240)
at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1541)
at java.base/java.text.DateFormat.parse(DateFormat.java:393)
at com.programing.unsafe.SimpleDateFormatThread.run(SimpleDateFormatThread.java:22)
Thread-6 报错:2018-01-07 =>2018-01-03
Thread-1 报错:2018-01-02 =>2017-12-01
Thread-4 报错:2018-01-05 =>2017-12-01
Thread-3 正确:2018-01-04 =>2018-01-04
Thread-2 正确:2018-01-03 =>2018-01-03
Thread-7 正确:2018-01-08 =>2018-01-08
Thread-8 正确:2018-01-09 =>2018-01-09
问题原因
SimpleDateFormat和DateFormat类不是线程安全的。我们之所以忽视线程安全的问题,是因为从SimpleDateFormat和DateFormat类提供给我们的接口上来看,实在让人看不出它与线程安全有何相干。只是在JDK文档的最下面有如下说明:
SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。
JDK原始文档如下:
Synchronization:
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.
下面我们通过看JDK源码来看看为什么SimpleDateFormat和DateFormat类不是线程安全的真正原因:
SimpleDateFormat继承了DateFormat,在DateFormat中定义了一个protected属性的 Calendar类的对象:calendar。只是因为Calendar累的概念复杂,牵扯到时区与本地化等等,Jdk的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。
网上看到一些朋友的分析比较清晰, 这里借用一下
查看源码
//父类: DateFormat
protected Calendar calendar;
//子类 : SimpleDateFormat
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo);
break;
}
}
return toAppendTo;
}
calendar.setTime(date)这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
线程1调用format方法,改变了calendar这个字段。
中断来了。
线程2开始执行,它也改变了calendar。
又中断了。
线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
分析一下format的实现,我们不难发现,用到成员变量calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。
这个问题背后隐藏着一个更为重要的问题--无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的
解决方法
- 需要的时候创建新实例:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- 同步代码块 synchronized(code)
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date)throws ParseException{
synchronized(sdf){
return sdf.format(date);
}
}
- 使用ThreadLocal:
也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。
private static ThreadLocal<SimpleDateFormate> threadLocal = new ThreadLocal<SimpleDateFormate>();