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


    今天聊聊 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<DateFormatSAFE_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();        }    }}

    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(                101060TimeUnit.SECONDSnew 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(                101060TimeUnit.SECONDSnew 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(                101060TimeUnit.SECONDSnew 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);  // 打印最终结果    }}

     

    感谢阅读,更多的java课程学习路线,笔记,面试等架构资料,需要的同学可以私信我(资料)即可免费获取!

     

  • 相关阅读:
    module ‘torch‘ has no attribute ‘_six‘问题解决
    SWT/ANR问题--Dump时间过长导致的SWT
    Python入门,从19个语法开始!
    获取个人免费版Ubuntu Pro
    openstack基本命令小结
    Minimum Snap轨迹规划详解(3)闭式求解
    Python10-使用urllib模块处理URL
    【已解决】qt死活不响应鼠标移动到按钮事件
    DAP-seq在植物转录因子的应用案例助你打通研究思路
    c语言实现内存池
  • 原文地址:https://blog.csdn.net/hahazz233/article/details/125426365