SimpleDateFormat官方定义
SimpleDateFormat是一个具体的类,用于以区域设置敏感的方式格式化和解析日期。 它允许格式化(日期文本),解析(文本日期)和归一化。
SimpleDateFormat允许您从选择日期时间格式化的任何用户定义的模式开始。 不过,建议您创建一个日期-时间格式有两种getTimeInstance , getDateInstance ,或getDateTimeInstance在DateFormat 。 这些类方法中的每一个都可以返回使用默认格式模式初始化的日期/时间格式化程序。 您可以根据需要使用applyPattern方法修改格式模式。 有关使用这些方法的更多信息,请参见DateFormat 。
Synchronization
日期格式不同步。 建议为每个线程创建单独的格式实例。 如果多个线程同时访问格式,则必须在外部进行同步。
问题复现
官方既然说此类非线程安全,那么我们先来进行一下问题复现,看看到底会出什么问题。
package thread.juc.dateformat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* 多线程模式下SimpleDateFormat问题复现
* @author jacklaile
*/
public class DateFormatProblem {
// 创建日期格式化类
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
// 创建100个线程
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
System.out.println(sdf.parse("2020-10-10 10:10:10"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
多次执行上述代码,可能会得到如下错误:
Exception in thread "Thread-801" java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
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 thread.juc.dateformat.DateFormatProblem.lambda$main$0(DateFormatProblem.java:22)
at java.lang.Thread.run(Thread.java:748)
进入SimpleDateFormat的实现方法parse(),看到如下代码:
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
// If the year value is ambiguous,
// then the two-digit year == the default start year
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
这里的parsedDate = calb.establish(calendar).getTime();方法是线程不安全的,仔细查看establish(Calendar cal)方法,发现每次进行establish的时候,会调用cal.clear(),由于并非线程安全的,所以在多线程环境下,在线程使用cal对象很有可能已经被清空了,导致了异常。所以我们解决这个线程不安全的思路就是,需要保证每个线程使用自己的SimpleDateFormat对象。
下面来看看解决方法。
方法一
思路:每次使用时new一个SimpleDateFormat的实例,这样可以保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其他引用,又需要回收,开销会很大。
主要代码:
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
System.out.println(sdf.parse("2020-10-10 10:10:10"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
方法二
思路:由于SimpleDateFormat不是线程安全的,所以每次调用的时候对任务代码块进行加锁。
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
// 对SimpleDateFormat对象加锁,使得同时只有一个线程能够使用
synchronized (sdf) {
System.out.println(sdf.parse("2020-10-10 10:10:10"));
}
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
不过这种加锁的方式就意味着监视器资源竞争,在高并发场景下依然低效。
方法三
思路:使用ThreadLocal,这样每个线程只需要使用一个SimpleDateFormat实例,这相比第一种方式大大节省了对象的创建销毁开销,并且不需要使多个线程同步。
ThreadLocal:对于ThreadLocal变量,每个访问ThreadLocal变量的线程会在本地生成一个变量的副本,当多线程操作这个变量的时候,操作的其实是自己本地的副本变量,不会影响其他线程,提高了效率。
根据上面的思路,对代码进行改造。
package thread.juc.dateformat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* 多线程模式下SimpleDateFormat问题复现
* 使用ThreadLocal解决并发效率低下的问题
* @author jacklaile
*/
public class SafeDateFormat3 {
// 创建日期格式化类
static ThreadLocal<SimpleDateFormat> localSdf = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static void main(String[] args) {
// 创建1000个线程
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
// 得到的是每个线程自己本地的变量副本
System.out.println(localSdf.get().parse("2020-10-10 10:10:10"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
参考资料:
《Java并发编程之美》