还在用 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 做时间格式化?小心项目崩掉!

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

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

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

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

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

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

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

还在用 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 非线程安全原因分析

为了找到问题所在,查看 SimpleDateFormat 中 format 方法的源码来排查一下问题,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 做时间格式化?小心项目崩掉!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/101481.html

(0)
Java知音的头像Java知音bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!