前言
在日常开发中,我们经常会用到时间,我们有很多办法在Java代码中获取时间。但不同的方法获取到的时间格式不尽相同,这时就需要一种格式化工具,把时间显示成我们需要的格式,最常用的方法就是使用SImpleDateFormat类。这是一个看上去功能比较简单的类,但使用不当,也有可能导致很大的问题.
在《阿里巴巴Java开发手册》中,有明确规定【强制】SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类
1.SimpleDateFormat的用法
public class MyDate {
public static void main(String[] args) throws ParseException {
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);
// Strubg 转Date
System.out.println(sdf.parse(dataStr));
}
}
2.SimpleDateFormat线程的安全性
由于SimpledateFormat比较常用,而且在一般情况下,一个应用中时间的显示模式都是一样的,所以很多人愿意使用如下方式定义SimpleDateFormat
public class MyDate {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws ParseException {
String format = sdf.format(Calendar.getInstance().getTime());
System.out.println("format = " + format);
}
}
这种定义方式存在很大的安全隐患
2.1 问题重现
当我们使用线程池来输出时间
public class MyDate {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static ThreadFactory nameThreadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("demo-pool-%d");
return t;
}
};
private static ExecutorService pool = new ThreadPoolExecutor(5,200,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1024), nameThreadFactory, new ThreadPoolExecutor.AbortPolicy());
private static CountDownLatch countDownLatch = new CountDownLatch(100);
public static void main(String[] args) throws InterruptedException {
// 定义一个线程安全的HashSet
Set<String> dates = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < 100; i++) {
// 获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
// 时间增加
calendar.add(Calendar.DATE, finalI);
// 通过simpleDateFormat把时间转换成字符串
String dateString = sdf.format(calendar.getTime());
// 把字符串放入set
dates.add(dateString);
// countDownLatch
countDownLatch.countDown();
});
}
// 阻塞,直到countDown数量为0
countDownLatch.await();
// 输出去重后的时间个数
System.out.println(dates.size());
// 线程池关闭
pool.shutdown();
}
}
代码逻辑比较简单,循环执行一百次,每次循环执行时都在当前时间的基础上增加一个天数(这个天数随着循环次数而变化),然后把所有日期放入一个线程安全的、带有去重功能的set中,最后输出Set元素的格式,正常情况下,以上代码的输出结果应该是100.但实际执行的结果是一个小于100的数字,这是因为SImpleDateFormat作为一个非线程安全的类,被当作共享变量在多个线程中使用,这就出现了线程安全问题.
这个问题在JDK文档中已经明确表明了SimpleDateFormat不应该用在多线程场景中Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.
2.2 线程不安全的原因
关键在于SimpleDateFormat#format()
DateFormat中的calendar变量是一个成员变量,它每次在格式化时都用会该变量保存当前时间,由于我们在使用时将它声明为了static类型,SimpleDateFormat中的calendar也就可以被多个线程访问,一个共享变量在多线程环境下进行修改必然会引起不安全。同理SimpleDateFormat的parse方法也有同样的问题
2.3 解决方式
- 使用局部变量
public static void main(String[] args) throws InterruptedException {
// 定义一个线程安全的HashSet
Set<String> dates = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < 100; i++) {
// 获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 时间增加
calendar.add(Calendar.DATE, finalI);
// 通过simpleDateFormat把时间转换成字符串
String dateString = sdf.format(calendar.getTime());
// 把字符串放入set
dates.add(dateString);
// countDownLatch
countDownLatch.countDown();
});
}
// 阻塞,直到countDown数量为0
countDownLatch.await();
// 输出去重后的时间个数
System.out.println(dates.size());
// 线程池关闭
pool.shutdown();
}
- 加同步锁
public static void main(String[] args) throws InterruptedException {
// 定义一个线程安全的HashSet
Set<String> dates = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < 100; i++) {
// 获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
synchronized (sdf) {
// 时间增加
calendar.add(Calendar.DATE, finalI);
// 通过simpleDateFormat把时间转换成字符串
String dateString = sdf.format(calendar.getTime());
// 把字符串放入set
dates.add(dateString);
// countDownLatch
countDownLatch.countDown();
}
});
}
// 阻塞,直到countDown数量为0
countDownLatch.await();
// 输出去重后的时间个数
System.out.println(dates.size());
// 线程池关闭
pool.shutdown();
}
- 使用ThreadLocal
package other;
import org.apache.commons.collections.CollectionUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author xieh
* @date 2024/01/24 22:25
*/
public class MyDate {
private static ThreadLocal<SimpleDateFormat> sdfThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
private static ThreadFactory nameThreadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("demo-pool-%d");
return t;
}
};
private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1024), nameThreadFactory, new ThreadPoolExecutor.AbortPolicy());
private static CountDownLatch countDownLatch = new CountDownLatch(100);
public static void main(String[] args) throws InterruptedException {
// 定义一个线程安全的HashSet
Set<String> dates = Collections.synchronizedSet(new HashSet<String>());
for (int i = 0; i < 100; i++) {
// 获取当前时间
Calendar calendar = Calendar.getInstance();
int finalI = i;
pool.execute(() -> {
SimpleDateFormat sdf = sdfThreadLocal.get();
// 时间增加
calendar.add(Calendar.DATE, finalI);
// 通过simpleDateFormat把时间转换成字符串
String dateString = sdf.format(calendar.getTime());
// 把字符串放入set
dates.add(dateString);
// countDownLatch
countDownLatch.countDown();
});
}
// 阻塞,直到countDown数量为0
countDownLatch.await();
// 输出去重后的时间个数
System.out.println(dates.size());
// 线程池关闭
pool.shutdown();
}
}
- 使用DateTimeFormatter
需要JDK8版本的支持,这是一个线程安全的格式化工具类
public static void main(String[] args) {
// 解析日期
String dateStr = "2024年01月24日";
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate parse = LocalDate.parse(dateStr, dateTimeFormatter);
System.out.println("parse = " + parse);
// 将日期转换为字符串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String format = now.format(formatter);
System.out.println("format = " + format);
}
文章来源:https://www.toymoban.com/news/detail-823754.html
2.4 总结
解决线程不安全的主要手段有使用局部变量、synchronized加锁、ThreadLocal为每一个线程单独创建一个对象等文章来源地址https://www.toymoban.com/news/detail-823754.html
到了这里,关于Java中SimpleDateFormat的线程安全性问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!