当前位置:网站首页>还在用 SimpleDateFormat 做时间格式化?小心项目崩掉
还在用 SimpleDateFormat 做时间格式化?小心项目崩掉
2022-06-24 19:43:00 【锐湃】
SimpleDateFormat.parse()
方法的线程安全问题错误示例
非线程安全原因分析
解决方法
SimpleDateFormat.format()
方法的线程安全问题错误示例
非线程安全原因分析
解决方法
SimpleDateFormat
在多线程环境下存在线程安全问题。
1 SimpleDateFormat.parse() 方法的线程安全问题
1.1 错误示例
错误使用SimpleDateFormat.parse()
的代码如下:
import java.text.SimpleDateFormat;
public class SimpleDateFormatTest {
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
/**
* SimpleDateFormat线程不安全,没有保证线程安全(没有加锁)的情况下,禁止使用全局SimpleDateFormat,否则报错 NumberFormatException
*
* private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
*/
for (int i = 0; i < 20; ++i) {
Thread thread = new Thread(() -> {
try {
// 错误写法会导致线程安全问题
System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-" + i);
thread.start();
}
}
}
报错:
1.2 非线程安全原因分析
查看源码中可以看到:SimpleDateFormat
继承DateFormat
类,SimpleDateFormat
转换日期是通过继承自DateFormat
类的Calendar
对象来操作的,Calendar
对象会被用来进行日期-时间计算,既被用于format
方法也被用于parse
方法。
SimpleDateFormat
的 parse(String source)
方法 会调用继承自父类的 DateFormat
的 parse(String source)
方法
DateFormat
的 parse(String source)
方法会调用SimpleDateFormat
中重写的 parse(String text, ParsePosition pos)
方法,该方法中有个地方需要关注
SimpleDateFormat
中重写的 parse(String text, ParsePosition pos)
方法中调用了 establish(calendar)
这个方法:
该方法中调用了 Calendar
的 clear()
方法
可以发现整个过程中Calendar
对象它并不是线程安全的,如果,a线程将calendar
清空了,calendar
就没有新值了,恰好此时b线程刚好进入到parse方法用到了calendar
对象,那就会产生线程安全问题了!
正常情况下:
非线程安全的流程:
1.3 解决方法
方法1:每个线程都new一个SimpleDateFormat
import java.text.SimpleDateFormat;
public class SimpleDateFormatTest {
public static void main(String[] args) {
for (int i = 0; i < 20; ++i) {
Thread thread = new Thread(() -> {
try {
// 每个线程都new一个
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(Thread.currentThread().getName() + "--" + simpleDateFormat.parse("2020-06-01 11:35:00"));
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-" + i);
thread.start();
}
}
}
方式2:synchronized
等方式加锁
public class SimpleDateFormatTest {
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
for (int i = 0; i < 20; ++i) {
Thread thread = new Thread(() -> {
try {
synchronized (SIMPLE_DATE_FORMAT) {
System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-" + i);
thread.start();
}
}
}
方式3:使用ThreadLocal
为每个线程创建一个独立变量
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class SimpleDateFormatTest {
private static final ThreadLocal<DateFormat> SAFE_SIMPLE_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args) {
for (int i = 0; i < 20; ++i) {
Thread thread = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "--" + SAFE_SIMPLE_DATE_FORMAT.get().parse("2020-06-01 11:35:00"));
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-" + i);
thread.start();
}
}
}
ThreadLocal的详细使用细节见:
https://blog.csdn.net/QiuHaoqian/article/details/117077792
2 SimpleDateFormat.format() 方法的线程安全问题
2.1 错误示例
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class SimpleDateFormatTest {
// 时间格式化对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
// 创建线程池执行任务
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
Date date = new Date(finalI * 1000); // 得到时间对象
formatAndPrint(date); // 执行时间格式化
}
});
}
threadPool.shutdown(); // 线程池执行完任务之后关闭
}
/**
* 格式化并打印时间
*/
private static void formatAndPrint(Date date) {
String result = simpleDateFormat.format(date); // 执行格式化
System.out.println("时间:" + result); // 打印最终结果
}
}
从上述结果可以看出,程序的打印结果竟然有重复内容的,正确的情况应该是没有重复的时间才对。
2.2 非线程安全原因分析
为了找到问题所在,查看 SimpleDateFormat
中 format
方法的源码来排查一下问题,format
源码如下:
从上述源码可以看出,在执行 SimpleDateFormat.format()
方法时,会使用 calendar.setTime()
方法将输入的时间进行转换,那么我们想想一下这样的场景:
线程 1 执行了
calendar.setTime(date)
方法,将用户输入的时间转换成了后面格式化时所需要的时间;线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;
线程 2 执行了
calendar.setTime(date)
方法,对时间进行了修改;线程 2 暂停执行,线程 1 得出 CPU 时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。
正常的情况下,程序的执行是这样的:
非线程安全的执行流程是这样的:
2.3 解决方法
同样有三种解决方法
方法1:每个线程都new一个SimpleDateFormat
public class SimpleDateFormatTest {
public static void main(String[] args) throws InterruptedException {
// 创建线程池执行任务
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
// 得到时间对象
Date date = new Date(finalI * 1000);
// 执行时间格式化
formatAndPrint(date);
}
});
}
// 线程池执行完任务之后关闭
threadPool.shutdown();
}
/**
* 格式化并打印时间
*/
private static void formatAndPrint(Date date) {
String result = new SimpleDateFormat("mm:ss").format(date); // 执行格式化
System.out.println("时间:" + result); // 打印最终结果
}
}
方式2:synchronized
等方式加锁
所有的线程必须排队执行某些业务才行,这样无形中就降低了程序的运行效率了
public class SimpleDateFormatTest {
// 时间格式化对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
// 创建线程池执行任务
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 执行任务
threadPool.execute(new Runnable() {
@Override
public void run() {
Date date = new Date(finalI * 1000); // 得到时间对象
formatAndPrint(date); // 执行时间格式化
}
});
}
// 线程池执行完任务之后关闭
threadPool.shutdown();
}
/**
* 格式化并打印时间
*/
private static void formatAndPrint(Date date) {
// 执行格式化
String result = null;
// 加锁
synchronized (SimpleDateFormatTest.class) {
result = simpleDateFormat.format(date);
}
// 打印最终结果
System.out.println("时间:" + result);
}
}
方式3:使用ThreadLocal
为每个线程创建一个独立变量
public class SimpleDateFormatTest {
// 创建 ThreadLocal 并设置默认值
private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
public static void main(String[] args) {
// 创建线程池执行任务
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
// 执行任务
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 执行任务
threadPool.execute(() -> {
Date date = new Date(finalI * 1000); // 得到时间对象
formatAndPrint(date); // 执行时间格式化
});
}
threadPool.shutdown(); // 线程池执行完任务之后关闭
}
/**
* 格式化并打印时间
*/
private static void formatAndPrint(Date date) {
String result = dateFormatThreadLocal.get().format(date); // 执行格式化
System.out.println("时间:" + result); // 打印最终结果
}
}
来源:blog.csdn.net/QiuHaoqian/article/
details/116594422
边栏推荐
- Financial management [4]
- 【js】-【栈、队-应用】-学习笔记
- Getting started with the go Cobra command line tool
- docker-mysql8-主从
- QT to place the form in the lower right corner of the desktop
- [JS] - [array application] - learning notes
- R语言使用MatchIt包进行倾向性匹配分析、使用match.data函数构建匹配后的样本集合、通过双样本t检验分析(双独立样本t检验)来判断倾向性评分匹配后样本中的所有协变量的平衡情况
- Écoutez le fichier markdown et mettez à jour Hot next. Page JS
- 慕思股份深交所上市:靠床垫和“洋老头”走红 市值224亿
- Installation and deployment of ganglia
猜你喜欢
【js】-【链表-应用】-学习笔记
Main cause of EMI - mold current
Installation and deployment of ganglia
选择类排序法
Uncover the secrets of Huawei cloud enterprise redis issue 16: acid'true' transactions beyond open source redis
Pousser l'information au format markdown vers le robot nail
Pseudo original intelligent rewriting API Baidu - good collection
Non single file component
Dig deep into MySQL - resolve the clustered index / secondary index / federated index of InnoDB storage engine
File contains vulnerability issues
随机推荐
【js】-【链表-应用】-学习笔记
Daily practice (22): maximum sum of continuous subarrays
How should we measure agile R & D projects?
R语言使用MatchIt包进行倾向性匹配分析、使用match.data函数构建匹配后的样本集合、对匹配后的样本的不同分组对应的目标变量的均值进行Welch双样本t检验分析、双独立样本t检验
Concurrent shared model management
22map introduction and API
01_ Getting started with the spingboot framework
【基础知识】~ 半加器 & 全加器
378. 骑士放置
Building Survey [1]
The large-scale market of graduate dormitory! Here comes the enviable graduate dormitory!
376. machine tasks
Laravel 认证模块 auth
Uip1.0 active sending problem understanding
R语言使用MatchIt包进行倾向性匹配分析、使用match.data函数构建匹配后的样本集合、通过双样本t检验分析(双独立样本t检验)来判断倾向性评分匹配后样本中的所有协变量的平衡情况
Financial management [3]
R语言dplyr包select函数将dataframe数据中的指定数据列移动到dataframe数据列中的第一列(首列)
376. 機器任務
SQL -convert function
laravel用户授权