最近使用Sonarqube扫描代码的时候发现一个问题,使用static修饰SimpleDateFormat的实例被标记为了Bug
Sonarqube中的提示信息如下:
并不是标准Java库中的所有类都被编写为线程安全的。以多线程方式使用它们很可能在运行时导致数据问题或异常
当Calendar, DateFormat, javax.xml.xpath.XPath, 或者 javax.xml.validation.SchemaFactory的实例被标记为静态时会出现问题
public class MyClass {
private static SimpleDateFormat format = new SimpleDateFormat("HH-mm-ss");
private static Calendar calendar = Calendar.getInstance();
public class MyClass {
private SimpleDateFormat format = new SimpleDateFormat("HH-mm-ss");
private Calendar calendar = Calendar.getInstance();
又查看了一下《阿里巴巴开发手册》中对SimpleDateFormat是怎么对待的
其实主要的问题在于SimpleDateFormat并不是一个线程安全的类
在多线程环境下:
public class SimpleDateFormatTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date) {
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException {
return sdf.parse(strDate);
}
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
service.execute(() -> {
try {
System.out.println(parse("2019-04-03 17:45:00"));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);
}
}
输出信息如下:
Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-3" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.hand.thread.SimpleDateFormatTest.parse(SimpleDateFormatTest.java:18)
at com.hand.thread.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:26)
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)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.hand.thread.SimpleDateFormatTest.parse(SimpleDateFormatTest.java:18)
at com.hand.thread.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:26)
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)
Fri Apr 03 17:45:00 CST 2201
Wed Apr 03 17:45:00 CST 2019
Fri Apr 03 17:45:00 CST 2201
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Wed Apr 03 17:45:00 CST 2019
Exception in thread "pool-1-thread-20" Exception in thread "pool-1-thread-19" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)Wed Apr 03 17:45:00 CST 2019
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.hand.thread.SimpleDateFormatTest.parse(SimpleDateFormatTest.java:18)
at com.hand.thread.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:26)
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)
java.lang.NumberFormatException: multiple points
Wed Apr 03 17:00:00 CST 2019
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
Wed Apr 03 17:00:00 CST 2019
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.hand.thread.SimpleDateFormatTest.parse(SimpleDateFormatTest.java:18)
at com.hand.thread.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:26)
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)
Mon Apr 02 17:45:00 CST 3
部分线程获取的时间不对,部分线程直接报java.lang.NumberFormatException: multiple points
把SimpleDateFormat定义为静态变量,多线程环境下SimpleDateFormat的实例就会被多个线程共享,B线程会读取到A线程的时间,就会出现时间差异和其他各种问题
SimpleDateFormat和它继承的DateFormat都不是线程安全的
看一下SimpleDateFormat的format()方法的源码:
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo,
FieldPosition pos)
{
pos.beginIndex = pos.endIndex = 0;
return format(date, toAppendTo, pos.getFieldDelegate());
}
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
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, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
calendar.setTime(date),SimpleDateFormat的format方法实际操作的就是Calendar。
因为我们声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问
假设线程A执行完calendar.setTime(date),把时间设置成2019-01-02,这时候被挂起,线程B获得CPU执行权。线程B也执行到了calendar.setTime(date),把时间设置为2019-01-03。线程挂起,线程A继续走,calendar还会被继续使用(subFormat方法),而这时calendar用的是线程B设置的值了,而这就是引发问题的根源,出现时间不对,线程挂死等等
使用JDK1.8的DateTimeFormatter
public class DateTimeFormatterTest {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String formatDate(LocalDateTime date) {
return formatter.format(date);
}
public static LocalDateTime parse(String dateNow) {
return LocalDateTime.parse(dateNow, formatter);
}
public static void main(String[] args) throws InterruptedException, ParseException {
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
service.execute(() -> {
for (int j = 0; j < 10; j++) {
try {
System.out.println(parse(formatDate(LocalDateTime.now())));
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);
}
}
参考:https://542869246.github.io/2019/01/02/还在使用SimpleDateFormat?你的项目崩没?/