我们都知道SimpleDateFormat
是Java中用来日期格式化的一个类,可以将特定格式的字符转转化成日期,也可将日期转化成特定格式的字符串。比如
- 将特定的字符串转换成日期
public static void main(String[] args) throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date format = simpleDateFormat.parse("2022-12-19 11:55:56");
System.out.println(format);
}
- 将日期转化成特定格式的字符串
public static void main(String[] args) throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// Date format = simpleDateFormat.parse("2022-12-19 11:55:56");
String format = simpleDateFormat.format(new Date());
System.out.println(format);
}
这种都是在单线程的情况下,这样子是没有问题的,但是如果多线程调用同一个SimpleDateFormat
对象就会出现安全问题。
线程安全问题演示(参考了冰河的文章)
在下面的代码中我们定义了SimpleDateFormat对象被使用的总次数以及同时使用的最大线程的数量。在里面我用到了Semaphore 信号量用来限流,也就是限制线程的最大数量是20个。同时用到了CountDownLatch ,用来保证所有的线程都执行完成之后主线程才能继续往下执行。
package com.dongmu;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* 重现多线程下SimplDateFormat线程不安全的现象
*/
public class SimpleDateFormatTest {
// 定义执行的总次数
private static final int EXECUTE_COUNT = 2000;
// 同时运行的最大的线程数量
private static final int THREAD_COUNT = 20;
// SimpleDateFormat对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0;i<EXECUTE_COUNT;i++){
executorService.execute(()->{
try {
semaphore.acquire();
try {
simpleDateFormat.parse("2022-12-19");
} catch (ParseException e) {
System.out.println("线程"+Thread.currentThread().getName()+"格式化日期失败");
throw new RuntimeException(e);
}catch (NumberFormatException e){
System.out.println("线程"+Thread.currentThread().getName()+"格式化日期失败");
e.printStackTrace();
}
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("所有线程格式化日期完成");
}
}
可以看到执行的结果如下:
这里报出了一个异常class NumberFormatException
,这就是由于多线程使用同一个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;
}
而上面的携带两个参数的parse方法的最后调用了
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();
}
}
}
这个establish方法接收了一个参数calendar,而这个对象是一个SimpleDateFormat对象的遍历,多线程使用同一个这个对象,而在这个establish
方法中对calendar
进行了clear
和set
的操作。这就导致了calendar
对象内部的数据在多线程的操作下混乱了,也就导致在进行数据格式化的时候出现了原本不应该出现的数字,也就导致了NumberFormatException
Calendar establish(Calendar cal) {
boolean weekDate = isSet(WEEK_YEAR)
&& field[WEEK_YEAR] > field[YEAR];
if (weekDate && !cal.isWeekDateSupported()) {
// Use YEAR instead
if (!isSet(YEAR)) {
set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
}
weekDate = false;
}
cal.clear();
// Set the fields from the min stamp to the max stamp so that
// the field resolution works in the Calendar.
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
for (int index = 0; index <= maxFieldIndex; index++) {
if (field[index] == stamp) {
cal.set(index, field[MAX_FIELD + index]);
break;
}
}
}
if (weekDate) {
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
int dayOfWeek = isSet(DAY_OF_WEEK) ?
field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
if (dayOfWeek >= 8) {
dayOfWeek--;
weekOfYear += dayOfWeek / 7;
dayOfWeek = (dayOfWeek % 7) + 1;
} else {
while (dayOfWeek <= 0) {
dayOfWeek += 7;
weekOfYear--;
}
}
dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
}
cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
}
return cal;
}
同样在format中也存在同样的问题,也可能出现异常。
解决方案
- 第一种就是直接每次使用的时候都new一个新SimpleDateFormat的对象。
- 第二种就是在使用的时候用synchronized修饰。
- 第三种就是
//Lock对象
private static Lock lock = new ReentrantLock();
在simpleDateFormat.parse("2022-12-19");的前面使用lock.lock();然后加上finally语句块,在这个语句快钟进行lock.unlock();
注意这里一定要在finally语句块钟进行执行释放锁的操作,避免因为程序异常导致锁无法是释放的问题。
- 第四种方案,使用ThreadLocal保证每一个线程都拥有自己的simpleDateFormat变量,这样就不会出现线程安全问题了。
//定义成员变量
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
//然后将原来parse的部分该成
threadLocal.get().parse("2022-12-19");
- 第五种方案,使用DateTimeFormatter类,这是Java8提供的新的如期时间Api中的类,这是个线程安全的类,可以在高并发环境下直接使用。
private static DateTimeFormatter dateTimeFormatter =DateTimeFormatter.ofPattern("yyyy-MM-dd");
dateTimeFormatter.parse("2022-12-19");
- 第六种方式使用.joda-time方式,这种方式需要引入依赖,这里不做过多介绍,上面的方法已经够用了。