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

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

点击关注公众号,实用技术文章及时了解还在用 SimpleDateFormat 做时间格式化?小心项目崩掉!

  • 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();
        }
    }
}

报错:

还在用 SimpleDateFormat 做时间格式化?小心项目崩掉!
1.2 非线程安全原因分析

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

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

SimpleDateFormatparse(String source) 方法 会调用继承自父类的 DateFormatparse(String source) 方法

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

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

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

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

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

该方法中调用了 Calendarclear() 方法

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

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

正常情况下:

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

非线程安全的流程:

还在用 SimpleDateFormat 做时间格式化?小心项目崩掉!
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(
                101060, 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); // 打印最终结果
    }
}
还在用 SimpleDateFormat 做时间格式化?小心项目崩掉!

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

2.2 非线程安全原因分析

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

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

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

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

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

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

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

还在用 SimpleDateFormat 做时间格式化?小心项目崩掉!
2.3 解决方法

同样有三种解决方法

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

public class SimpleDateFormatTest {
   
    public static void main(String[] args) throws InterruptedException {
        // 创建线程池执行任务
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                101060, 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(
                101060, 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(101060,
                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

推荐

Java面试题宝典

技术内卷群,一起来学习!!

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

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。“在看”支持我们吧!

原文始发于微信公众号(Java知音):还在用 SimpleDateFormat 做时间格式化?小心项目崩掉!