坑1:定义的static的SimpleDateFormat 可能出现线程安全问题
SimpleDateFormat是线程不安全的类,定义为static对象,会有数据同步风险。通过源码可以看出,SimpleDateFormat内部有一个Calendar对象,在日期转字符串或字符串转日期的过程中,多线程共享时有非常高的概率产生错误,推荐的方式之一时使用ThreadLocal,让每个线程单独拥有这个对象。
示例代码:
public class SimpleDateFormatterTest {
static ExecutorService threadPool = Executors.newFixedThreadPool(20);
static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
for(int i=0;i<200;i++){
threadPool.execute(()->{
try {
System.out.println(format.parse("2021-07-09 16:29:21"));
} catch (ParseException e) {
e.printStackTrace();
};
});
}
}
}
运行程序后大量报错,且没有报错的输出结果也不正确
为什么会出现上述问题呢?
SimpleDateFormat 的作用是的另一解析和格式化日期时间的模式,这看起来是一次性工作。应该复用,但它的解析和格式化的操作是非线程安全的。
*SimpleDateFormat 继承 DateFormat ,DateFormat 有一个成员变量 calendar。
SimpleDateFormat 的parse 方法如下:
public Date parse(String source) throws ParseException
{
ParsePosition pos = new ParsePosition(0);
Date result = parse(source, pos);
if (pos.index == 0)
throw new ParseException("Unparseable date: \"" + source + "\"" ,
pos.errorIndex);
return result;
}
最终会调用CalendarBuilder的establish 方法来构建Calendar
parsedDate = calb.establish(calendar).getTime();
establish 方法内部是先清空 Calendar再构建Calendar,整个的操作没有加锁
Calendar establish(Calendar cal) {
......
cal.clear();
......
return cal;
}
```
如果多线程在并发操作一个Calendar, 可能会产生一个线程还没来得及处理Calendar 就被另外一个线程清空了,所以会出现解析错误和异常。
那么怎么解决呢?
* 每次使用时new一个SimpleDateFormat 的 实例,这样可以保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其他引用,又需要回收,开销会很大。
* 可以使用syncheronized 对SimpleDtaFormat实例进行同步
* 【推荐】 使用ThreadLocl,这样每个线程只需要使用一个SimpleDateFormate实例,这相比第一种方式 节省了对象的创建销毁开销,并且不需要使多个线程同步。
* 使用Java8的DateTimeFormatter 类
下面是用ThreadLocal实现的示例:
```java
public class SimpleDateFormatterTest {
static ExecutorService threadPool = Executors.newFixedThreadPool(20);
private static final ThreadLocal<SimpleDateFormat> SIMPLEDATEFORMAT_THREADLOCAL = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<200;i++){
threadPool.execute(()->{
try {
System.out.println(SIMPLEDATEFORMAT_THREADLOCAL.get().parse("2021-07-09 16:29:21"));
} catch (ParseException e) {
e.printStackTrace();
};
});
}
threadPool.shutdown();
threadPool.awaitTermination(10, TimeUnit.SECONDS);
}
}
坑2:当需要解析的字符串和格式不匹配的时候,SimpleDateFormat 并不报错,而是返回其他日期
public static void main(String[] args) throws InterruptedException, ParseException {
String dateString = "20210908";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMM");
System.out.println(simpleDateFormat.parse(dateString));
}
结果:
Wed Aug 01 00:00:00 CST 2096
竟然输出了 2096年了
对于上面的两个坑,我们可以使用java8的DateTimeFormatter 来避免
1) 解决第一个坑
public class DateTimeFormatterTest {
static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
static ExecutorService threadPool = Executors.newFixedThreadPool(20);
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<200;i++){
threadPool.execute(()->{
System.out.println(dateTimeFormatter.parse("2021-07-09 16:29:21"));
});
}
threadPool.shutdown();
threadPool.awaitTermination(10, TimeUnit.SECONDS);
}
}
- 解决第二个坑
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM");
System.out.println(dateTimeFormatter.parse("2021-07-09 16:29:21"));
此时会直接报错,而不会出现不正确的结果;
DateTimeFormatter 是线程安全的,可以定义为static 使用,最后,DateTimeFormatter 的解析比较严格,需要解析的字符串和格式不匹配时,会直接报错。而不是错误的解析。