零、概述
任何线程不安全的问题,其实本质就是共用了一份数据且没有进行加锁同步,SimpleDateFormat 也是一样。
一、错误案例
public class Test {
static DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws Exception{
ExecutorService ts = Executors.newFixedThreadPool(1000);
for (;;) {
ts.execute(new Runnable() {
@Override
public void run() {
try {
String format = df.format(new Date(Math.abs(new Random().nextLong())));
System.out.println(format);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
});
}
}
}
运行后出现报错:以下只是报错的一种,多线程场景下会出现多种不同的错误
192456611-09-28 19:38:41
26312543-11-28 13:35:35
233029168-10-09 00:28:24
100149768-11-04 22:59:40
141560024-07-30 14:30:46
52461160-03-13 16:40:29
131434392-08-03 13:32:17
246393230-02-21 08:50:04
71490728-12-01 23:32:16
284007186-04-28 14:34:21
184430351-11-02 17:30:38
49670911-09-27 20:43:33java.lang.ArrayIndexOutOfBoundsException: -2850986
at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2397)
at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2312)
at java.util.Calendar.complete(Calendar.java:2268)
at java.util.Calendar.get(Calendar.java:1826)
at java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1119)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:966)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:936)
at java.text.DateFormat.format(DateFormat.java:345)
at test.Test$1.run(Test.java:102)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
SimpleDateFormat 注释里面就描述了,此类为非线程安全的,所以上述用了 static 或者定义为全局变量在多线程场景就会出现问题。
二、原理分析
SimpleDateFormat.java
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
......
}
可以看到传入的 Date 是赋值在全局变量 calendar 上的,那么在多线程场景,calendar 就会被覆盖产生数据错乱,数组越界等问题。
三、解决方案
public class Test {
static DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws Exception{
ExecutorService ts = Executors.newFixedThreadPool(100);
for (;;) {
ts.execute(new Runnable() {
@Override
public void run() {
try {
Instant instant = new Date(Math.abs(new Random().nextLong())).toInstant();
ZoneId zone = ZoneId.systemDefault();
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
System.out.println(df.format(localDateTime));
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
});
}
}
}
上述使用 JDK1.8 的方案,实现类是线程安全的,就解决了问题。
当然还有其他方案,比如
1、将SimpleDateFormat定义成局部变量
缺点:定义为局部对象,每调用一次方法就会创建一个 SimpleDateFormat 对象,增加GC对象
2、方法加同步锁synchronized,在同一时刻,只有一个线程可以执行类中的某个方法
缺点:性能较差