并发场景使用SimpleDateFormat异常问题和解决
SimpleDateFormat类主要是负责日期的格式化与转换操作,因为它不是线程安全的,所以使用SimpleDateFormat时,务必确保同一个SimpleDateFormat对象不要与其他线程共享,否则并发情况下会出现问题
目录
- 异常示例
- 解决方案1:创建了多个SimpleDateFormat类的实例
- 解决方案2:使用ThreadLocal类
- 解决方案3:使用DateTimeFormatter处理(JDK1.8)
异常示例
演示一下SimpleDateFormat在多线程情况下报错问题,先创建一个线程,在run方法中转换日期
public class ErrorTest extends Thread{
private final SimpleDateFormat sdf;
private final String date;
public ErrorTest(SimpleDateFormat sdf, String date) {
super();
this.sdf = sdf;
this.date = date;
}
@Override
public void run() {
try {
String newDate = sdf.format(sdf.parse(date));
if (!newDate.equals(date)) {
System.out.println(this.getName() + "报错了! " +
"日期:" + date + " 转换成了:" + newDate);
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
写完发现在idea开发中就已经开始报警告了
下面在启动类中同时传多个日期去转换
public class Run {
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String[] dateArray = new String[] { "2023-01-01", "2023-01-02",
"2023-01-03", "2023-01-04", "2023-01-05", "2023-01-06", "2023-01-07"};
int length = dateArray.length;
ErrorTest[] array = new ErrorTest[length];
for (int i = 0; i < length; i++) {
array[i] = new ErrorTest(sdf, dateArray[i]);
}
for (int i = 0; i < length; i++) {
array[i].start();
}
}
}
输出:
启动后可以看到控制台报错了,或者是日期转换成了其他莫名其妙的数据,说明SimpleDateFormat在多线程下极易出现日期转换错误的问题
解决方案1:创建了多个SimpleDateFormat类的实例
创建一个类,在每次执行线程时new一个新的SimpleDateFormat对象,让每一个线程享用一个SimpleDateFormat对象
public class SolveTest1 extends Thread{
private final String date;
public SolveTest1(String date) {
super();
this.date = date;
}
@Override
public void run() {
try {
// 转换
Date dateD = new SimpleDateFormat("yyyy-MM-dd").parse(date);
// 打印
String newDate = new SimpleDateFormat("yyyy-MM-dd").format(dateD);
if (newDate.equals(date)) {
System.out.println(this.getName() + "成功了! " +
"日期:" + date + " 转换成了:" + newDate);
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
启动类不再传SimpleDateFormat对象
/**
* ThreadLocal
*/
public class Run {
public static void main(String[] args) {
String[] dateArray = new String[] { "2023-01-01", "2023-01-02",
"2023-01-03", "2023-01-04", "2023-01-05", "2023-01-06", "2023-01-07"};
int length = dateArray.length;
SolveTest1[] array = new SolveTest1[length];
for (int i = 0; i < length; i++) {
array[i] = new SolveTest1(dateArray[i]);
}
for (int i = 0; i < length; i++) {
array[i].start();
}
}
}
输出可以看到七次都转换成功了
解决方案2:使用ThreadLocal类
ThreadLocal
简介:每个Thread线程内部都有一个ThreadLocalMap
,以线程作为key,要储存的值作为value,所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。这种变量在多线程环境下通过get
和set
方法访问时能保证各个线程的变量相对独立于其他线程内的变量
下面创建一个类来使用ThreadLocal,记得使用完要调用remove()方法回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。
/**
* ThreadLocal
*/
public class SolveTest2 extends Thread{
private final String date;
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = new ThreadLocal<>();
public SolveTest2(String date) {
super();
this.date = date;
}
@Override
public void run() {
try {
// 转换
Date dateD = getSimpleDateFormat("yyyy-MM-dd").parse(date);
// 打印
String newDate = getSimpleDateFormat("yyyy-MM-dd").format(dateD);
if (newDate.equals(date)) {
System.out.println(this.getName() + "成功了! " +
"日期:" + date + " 转换成了:" + newDate);
}
} catch (ParseException e) {
e.printStackTrace();
}
}
/**
* 获取SimpleDateFormat
* @param dateFormat 日期格式
* @return SimpleDateFormat
*/
public static SimpleDateFormat getSimpleDateFormat(String dateFormat) {
SimpleDateFormat sdf = THREAD_LOCAL.get();
if (sdf == null) {
sdf = new SimpleDateFormat(dateFormat);
THREAD_LOCAL.set(sdf);
}
// 回收
THREAD_LOCAL.remove();
return sdf;
}
}
启动类和解决方案1
的启动类代码一样,只需把调用线程改一下就行了。
可以看到执行结果也都成功了
解决方案3:使用DateTimeFormatter处理(JDK1.8)
在JDK1.8只后出来了DateTimeFormatter,和SimpleDateFormat不同的是,DateTimeFormatter是线程安全的,他是不变对象,可以只创建一个实例,到处引用。
public class SolveTest3 extends Thread{
private final String date;
/**
* 定义DateTimeFormatter格式
*/
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public SolveTest3(String date) {
super();
this.date = date;
}
@Override
public void run() {
// 转换
LocalDate localDate = LocalDate.parse(date, DATE_TIME_FORMATTER);
// 打印
String newDate = localDate.format(DATE_TIME_FORMATTER);
if (newDate.equals(date)) {
System.out.println(this.getName() + "成功了! " +
"日期:" + date + " 转换成了:" + newDate);
}
}
}
启动类同前面方案的一样,只需把调用线程改一下。输出可以看到都成功了
PS:把LocalDate
转换为 Date
如下:
/**
* atStartOfDay():
* atZone():
* toInstant():
*/
Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
总结:以上总结了三种方式解决SimpleDateFormat转换日期在多线程场景情况下存在的异常问题。但他们也有缺点~~ 方法一创建了多个SimpleDateFormat类的实例比较占用内存,效率较低;方法二使用ThreadLocal不会自动释放,如果不手动remove()容易造成内存泄露;个人建议使用方法三DateTimeFormatter处理。
完…