还在用 SimpleDateFormat 做时间格式化?小心项目崩掉

左手的ㄟ右手 2023-10-01 22:20 92阅读 0赞
  • SimpleDateFormat.parse() 方法的线程安全问题

    • 错误示例
    • 非线程安全原因分析
    • 解决方法
  • SimpleDateFormat.format() 方法的线程安全问题

    • 错误示例
    • 非线程安全原因分析
    • 解决方法

SimpleDateFormat在多线程环境下存在线程安全问题。

1 SimpleDateFormat.parse() 方法的线程安全问题

1.1 错误示例

错误使用SimpleDateFormat.parse()的代码如下:

  1. import java.text.SimpleDateFormat;
  2. public class SimpleDateFormatTest {
  3. private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  4. public static void main(String[] args) {
  5. /**
  6. * SimpleDateFormat线程不安全,没有保证线程安全(没有加锁)的情况下,禁止使用全局SimpleDateFormat,否则报错 NumberFormatException
  7. *
  8. * private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  9. */
  10. for (int i = 0; i < 20; ++i) {
  11. Thread thread = new Thread(() -> {
  12. try {
  13. // 错误写法会导致线程安全问题
  14. System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }, "Thread-" + i);
  19. thread.start();
  20. }
  21. }
  22. }

报错:

7580292e0d949e1b49e5ad77be843fc9.png

1.2 非线程安全原因分析

查看源码中可以看到:SimpleDateFormat继承DateFormat类,SimpleDateFormat转换日期是通过继承自DateFormat类的Calendar对象来操作的,Calendar对象会被用来进行日期-时间计算,既被用于format方法也被用于parse方法。

88f72d69da5542eab470072171a32dd4.png

SimpleDateFormat 的 parse(String source) 方法 会调用继承自父类的 DateFormat 的 parse(String source) 方法

d930eb7c2866e59adf9116bcb4a7e511.png

DateFormat 的 parse(String source) 方法会调用SimpleDateFormat中重写的 parse(String text, ParsePosition pos) 方法,该方法中有个地方需要关注

a0f518b1225181eca1b0ff8815316bdd.png

SimpleDateFormat 中重写的 parse(String text, ParsePosition pos) 方法中调用了 establish(calendar) 这个方法:

714b1236940b011ff881cc1823df4724.png

该方法中调用了 Calendar 的 clear() 方法

cf54b908cf9b59281535031cce27967e.png

可以发现整个过程中Calendar对象它并不是线程安全的,如果,a线程将calendar清空了,calendar 就没有新值了,恰好此时b线程刚好进入到parse方法用到了calendar对象,那就会产生线程安全问题了!

正常情况下:

4630bdc4a445354726a2f3b4fc52bd7e.png

非线程安全的流程:

d4767cf7b94fdd83ccf417d600b2e6ce.png

1.3 解决方法

方法1:每个线程都new一个SimpleDateFormat

  1. import java.text.SimpleDateFormat;
  2. public class SimpleDateFormatTest {
  3. public static void main(String[] args) {
  4. for (int i = 0; i < 20; ++i) {
  5. Thread thread = new Thread(() -> {
  6. try {
  7. // 每个线程都new一个
  8. SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  9. System.out.println(Thread.currentThread().getName() + "--" + simpleDateFormat.parse("2020-06-01 11:35:00"));
  10. } catch (Exception e) {
  11. e.printStackTrace();
  12. }
  13. }, "Thread-" + i);
  14. thread.start();
  15. }
  16. }
  17. }

方式2:synchronized等方式加锁

  1. public class SimpleDateFormatTest {
  2. private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  3. public static void main(String[] args) {
  4. for (int i = 0; i < 20; ++i) {
  5. Thread thread = new Thread(() -> {
  6. try {
  7. synchronized (SIMPLE_DATE_FORMAT) {
  8. System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));
  9. }
  10. } catch (Exception e) {
  11. e.printStackTrace();
  12. }
  13. }, "Thread-" + i);
  14. thread.start();
  15. }
  16. }
  17. }

方式3:使用ThreadLocal 为每个线程创建一个独立变量

  1. import java.text.DateFormat;
  2. import java.text.SimpleDateFormat;
  3. public class SimpleDateFormatTest {
  4. private static final ThreadLocal<DateFormat> SAFE_SIMPLE_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
  5. public static void main(String[] args) {
  6. for (int i = 0; i < 20; ++i) {
  7. Thread thread = new Thread(() -> {
  8. try {
  9. System.out.println(Thread.currentThread().getName() + "--" + SAFE_SIMPLE_DATE_FORMAT.get().parse("2020-06-01 11:35:00"));
  10. } catch (Exception e) {
  11. e.printStackTrace();
  12. }
  13. }, "Thread-" + i);
  14. thread.start();
  15. }
  16. }
  17. }

ThreadLocal的详细使用细节见:

https://blog.csdn.net/QiuHaoqian/article/details/117077792

2 SimpleDateFormat.format() 方法的线程安全问题

2.1 错误示例

  1. import java.text.SimpleDateFormat;
  2. import java.util.Date;
  3. import java.util.concurrent.LinkedBlockingQueue;
  4. import java.util.concurrent.ThreadPoolExecutor;
  5. import java.util.concurrent.TimeUnit;
  6. public class SimpleDateFormatTest {
  7. // 时间格式化对象
  8. private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
  9. public static void main(String[] args) throws InterruptedException {
  10. // 创建线程池执行任务
  11. ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
  12. 10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
  13. for (int i = 0; i < 1000; i++) {
  14. int finalI = i;
  15. // 执行任务
  16. threadPool.execute(new Runnable() {
  17. @Override
  18. public void run() {
  19. Date date = new Date(finalI * 1000); // 得到时间对象
  20. formatAndPrint(date); // 执行时间格式化
  21. }
  22. });
  23. }
  24. threadPool.shutdown(); // 线程池执行完任务之后关闭
  25. }
  26. /**
  27. * 格式化并打印时间
  28. */
  29. private static void formatAndPrint(Date date) {
  30. String result = simpleDateFormat.format(date); // 执行格式化
  31. System.out.println("时间:" + result); // 打印最终结果
  32. }
  33. }

5fcb1130cfcca35beb0f36b491cb5b1a.png

从上述结果可以看出,程序的打印结果竟然是有重复内容的,正确的情况应该是没有重复的时间才对。

2.2 非线程安全原因分析

为了找到问题所在,查看原因 SimpleDateFormat 中 format 方法的源码来排查一下问题,format 源码如下:

4491e3132e72f744832232d9ef4c936b.png

从上述源码可以看出,在执行任务 SimpleDateFormat.format() 方法时,会使用 calendar.setTime() 方法将输入的时间进行转换,那么我们想象一下这样的场景:

  • 线程 1 执行了 calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;
  • 线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;
  • 线程 2 执行了 calendar.setTime(date) 方法,对时间进行了修改;
  • 线程 2 暂停执行,线程 1 得出 CPU 时间的继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当前线程 1 继续执行的时候就会出现线程安全的问题了。

正常的情况下,程序的执行是这样的:

5b218d5f3a2bfd6499263aa7a988ddd7.png

非线程安全的执行流程是这样的:

e3940d2ed1061d27277931ea6134f596.png

2.3 解决方法

同样有三种解决方法

方法1:每个线程都new一个SimpleDateFormat

  1. public class SimpleDateFormatTest {
  2. public static void main(String[] args) throws InterruptedException {
  3. // 创建线程池执行任务
  4. ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
  5. 10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
  6. for (int i = 0; i < 1000; i++) {
  7. int finalI = i;
  8. // 执行任务
  9. threadPool.execute(new Runnable() {
  10. @Override
  11. public void run() {
  12. // 得到时间对象
  13. Date date = new Date(finalI * 1000);
  14. // 执行时间格式化
  15. formatAndPrint(date);
  16. }
  17. });
  18. }
  19. // 线程池执行完任务之后关闭
  20. threadPool.shutdown();
  21. }
  22. /**
  23. * 格式化并打印时间
  24. */
  25. private static void formatAndPrint(Date date) {
  26. String result = new SimpleDateFormat("mm:ss").format(date); // 执行格式化
  27. System.out.println("时间:" + result); // 打印最终结果
  28. }
  29. }

方式2:synchronized等方式加锁

所有的线程必须排队执行某些业务才行,这样无形中就降低了程序的运行效率了

  1. public class SimpleDateFormatTest {
  2. // 时间格式化对象
  3. private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
  4. public static void main(String[] args) throws InterruptedException {
  5. // 创建线程池执行任务
  6. ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
  7. 10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
  8. for (int i = 0; i < 1000; i++) {
  9. int finalI = i;
  10. // 执行任务
  11. threadPool.execute(new Runnable() {
  12. @Override
  13. public void run() {
  14. Date date = new Date(finalI * 1000); // 得到时间对象
  15. formatAndPrint(date); // 执行时间格式化
  16. }
  17. });
  18. }
  19. // 线程池执行完任务之后关闭
  20. threadPool.shutdown();
  21. }
  22. /**
  23. * 格式化并打印时间
  24. */
  25. private static void formatAndPrint(Date date) {
  26. // 执行格式化
  27. String result = null;
  28. // 加锁
  29. synchronized (SimpleDateFormatTest.class) {
  30. result = simpleDateFormat.format(date);
  31. }
  32. // 打印最终结果
  33. System.out.println("时间:" + result);
  34. }
  35. }

方式3:使用ThreadLocal 为每个线程创建一个独立变量

  1. public class SimpleDateFormatTest {
  2. // 创建 ThreadLocal 并设置默认值
  3. private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
  4. ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
  5. public static void main(String[] args) {
  6. // 创建线程池执行任务
  7. ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
  8. TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
  9. // 执行任务
  10. for (int i = 0; i < 1000; i++) {
  11. int finalI = i;
  12. // 执行任务
  13. threadPool.execute(() -> {
  14. Date date = new Date(finalI * 1000); // 得到时间对象
  15. formatAndPrint(date); // 执行时间格式化
  16. });
  17. }
  18. threadPool.shutdown(); // 线程池执行完任务之后关闭
  19. }
  20. /**
  21. * 格式化并打印时间
  22. */
  23. private static void formatAndPrint(Date date) {
  24. String result = dateFormatThreadLocal.get().format(date); // 执行格式化
  25. System.out.println("时间:" + result); // 打印最终结果
  26. }
  27. }

ba025446a242446598a4a9b55e253fcb.jpeg

发表评论

表情:
评论列表 (有 0 条评论,92人围观)

还没有评论,来说两句吧...

相关阅读