还在用 SimpleDateFormat 做时间格式化?小心背锅!
共 19120字,需浏览 39分钟
·
2022-08-08 16:27
超全技术栈的Java入门+进阶+实战!(非白嫖,点击查看)
后端开发经常通过SimpleDateFormat对时间进行格式化,但是在多线程环境下,SimpleDateFormat存在线程安全问题,今天就一起来分析一下问题点,并看看如何解决,防止一不小心踩坑。
一、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) {
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 {
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();
}
}
}
二、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 {
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
胖虎联合一线大厂朋友花费8个月的时间,录制了一份Java入门+进阶视频教程
课程特色:
总共88G,时常高达365小时,覆盖所有主流技术栈
均为同一人录制,不是东拼西凑的
对标线下T0级别的培训课,讲师大厂架构师,多年授课经验,通俗易懂
内容丰富,每一个技术点除了视频,还有课堂源码、笔记、PPT、图解
五大实战项目(视频+源码+笔记+SQL+软件)
一次付费,持续更新,永无二次费用
点击下方超链接查看详情(或者点击文末阅读原文):