项目中 SimpleDateFormat 的正确使用

项目中 SimpleDateFormat 的正确使用

日常开发中,我们经常需要使用时间相关类,说到时间相关类,想必大家对 SimpleDateFormat 并不陌生。主要是用它进行时间的格式化输出和解析,挺方便快捷的,但是 SimpleDateFormat 并不是一个线程安全 的类。在多线程情况下,会出现异常,想必有经验的小伙伴也遇到过。下面我们就来分析分析SimpleDateFormat为什么不安全?是怎么引发的?以及多线程下有那些SimpleDateFormat的解决方案?

先看看《阿里巴巴开发手册》对于 SimpleDateFormat 是怎么看待的:

一、问题场景重现

一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量,避免频繁创建它的对象实例,如下代码:

public class SimpleDateFormatTest {

    /** 日期格式化类. */
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String formatDate(Date date) throws ParseException {
        return sdf.format(date);
    }

    public static Date parse(String strDate) throws ParseException {
        return sdf.parse(strDate);
    }

    public static void main(String[] args) throws InterruptedException {
        /** 单线程下测试. */
        System.out.println(sdf.format(new Date()));
    }
}

是不是感觉没什么毛病?单线程下自然没毛病了,都是运用到多线程下就有大问题了。 测试下:

    public static void main(String[] args) throws InterruptedException {
        /** 单线程下测试. */
        System.out.println(sdf.format(new Date()));

        /** 多线程下测试. */
        ExecutorService service = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 20; i++) {
            service.execute(() -> {
                for (int j = 0; j < 10; j++) {
                    try {
                        System.out.println(parse("2018-01-02 09:45:59"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        // 等待上述的线程执行完后
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);
    }

控制台打印结果:

部分线程获取的时间不对,部分线程直接报 java.lang.NumberFormatException:multiple points 错,线程直接挂死了。

二、多线程不安全原因

因为我们把 SimpleDateFormat 定义为静态变量,那么多线程下SimpleDateFormat的实例就会被多个线程共享+,B线程会读取到A线程的时间,就会出现时间差异和其它各种问题。SimpleDateFormat和它继承的DateFormat类也不是线程安全的

来看看SimpleDateFormat的format()方法的源码

    // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);

        boolean useDateFormatSymbols = useDateFormatSymbols();

        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }

            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;

            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;

            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
    }

【注意】:calendar.setTime(date),SimpleDateFormat的format方法实际操作的就是 Calendar

因为我们声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问。

假设线程A执行完calendar.setTime(date),把时间设置成2019-01-02,这时候被挂起,线程B获得CPU执行权。线程B也执行到了calendar.setTime(date),把时间设置为2019-01-03。线程挂起,线程A继续走,calendar还会被继续使用(subFormat方法),而这时calendar用的是线程B设置的值了,而这就是引发问题的根源,出现时间不对,线程挂死等等。

其实SimpleDateFormat源码上作者也给过我们提示:

* Date formats are not synchronized.
* It is recommended to create separate format instances for each thread.
* If multiple threads access a format concurrently, it must be synchronized
* externally.

日期格式不同步。建议为每个线程创建单独的格式实例。 如果多个线程同时访问一种格式,则必须在外部同步该格式。

三、解决方案

只在需要的时候创建新实例,不用static修饰

    public static String formatDate(Date date) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }

    public static Date parse(String strDate) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(strDate);
    }

如上代码,仅在需要用到的地方创建一个新的实例,就没有线程安全问题,不过也加重了创建对象的负担,会 频繁地创建和销毁对象,效率较低

synchronized 大法好

    public static String formatDate(Date date) throws ParseException {
        //return sdf.format(date);
        synchronized (sdf) {
            return sdf.format(date);
        }
    }
    public static Date parse(String strDate) throws ParseException {
        //return sdf.parse(strDate);
        synchronized (sdf) {
            return sdf.parse(strDate);
        }
    }

简单粗暴,synchronized往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,线程阻塞。

ThreadLocal

    /* ThreadLocal */
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) throws ParseException {
        //return sdf.format(date);
        /*synchronized (sdf) {
            return sdf.format(date);
        }*/
        return threadLocal.get().format(date);
    }

    public static Date parse(String strDate) throws ParseException {
        //return sdf.parse(strDate);
        /*synchronized (sdf) {
            return sdf.parse(strDate);
        }*/
        return threadLocal.get().parse(strDate);
    }

ThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么自然也就不存在竞争问题了。

基于JDK1.8的DateTimeFormatter

也是《阿里巴巴开发手册》给我们的解决方案,对之前的代码进行改造:

public class SimpleDateFormatTest8 {
    // 新建 DateTimeFormatter 类
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private static String formatDate(LocalDateTime dateTime) {
        return FORMATTER.format(dateTime);
    }

    private static LocalDateTime parse(String dateNow) {
        return LocalDateTime.parse(dateNow, FORMATTER);
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService service = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 20; i++) {
            service.execute(() -> {
                for (int j = 0; j < 10; j++) {
                    try {
                        System.out.println(parse(formatDate(LocalDateTime.now())));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        // 等待上述的线程执行完后
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);
    }
}

DateTimeFormatter源码上作者也加注释说明了,他的类是不可变的,并且是线程安全的。

原文地址:https://www.cnblogs.com/christopherchan/p/11148859.html

时间: 2024-10-10 16:15:08

项目中 SimpleDateFormat 的正确使用的相关文章

如何批量测试Mybatis项目中SQL是否正确

去Oracle行动 最近公司要发展海外项目,所以要将现有的系统全部平移过去,另外数据库也要从原来的Oracle变为Mysql.公司的数据库交互层面使用的是Mybatis,而Oracle与Mysql也有一些语法上的不同.所以在项目中的Sql要改动,但是多个项目中涉及到的Sql非常多,如果仅凭人工一条一条辨别的话,工作量有点大. 所以就萌发出了直接将数据源变为Mysql,利用反射批量执行Mapper中的方法,然后如果有参数的话,就设置为默认的初始值,然后记录下来成功的数据和失败的数据,这样就可以根据

升级CUDA版本导致VS2010错误:未找到导入的项目XXX,请确认&lt;Import&gt;声明中的路径正确,且磁盘上存在该文件。。。。

VS2010错误:未找到导入的项目XXX,请确认<Import>声明中的路径正确,且磁盘上存在该文件. E:\IGSNRR\dev\PhDThesisCode_CUDA\gtcg\gtcg.vcxproj : error : 未找到导入的项目“C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\BuildCustomizations\CUDA 5.5.props”.请确认 <Import> 声明中的路径正确,且磁盘上存在该文件. E:

iOS项目中如何正确引入Html5文件(html/js/css)

iOS项目中使用项目中的html js css 文件时,有时会遇到引用路径出错的问题,导致html js css image文件无法加载的情况. 那么,引入H5相关文件的正确操作方式如下,这样就不会出现资源文件无法引入并正常使用的情况: 1 在项目目录下创建存放Html5文件文件夹:H5 2 将文件夹H5拖放至项目中对应的位置,此处注意要选择:Create groups 3 将各种Html5文件(html js css image等)拖放到H5文件夹中,此处注意选择:Create folder

未找到导入的项目,请确认 &lt;Import&gt; 声明中的路径正确

当使用vs出现下列情况: D:\xxxx\Web\Web.csproj : error  : 无法读取项目文件“Web.csproj”. D:\xxxx\WebServiceManager\Web\Web.csproj(1269,3): 未找到导入的项目“C:\Program Files\MSBuild\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets”.请确认 <Import> 声明中的

如何正确的在项目中接入微信JS-SDK

微信JS-SDK的功能 如果你点进来,那么我相信你应该知道微信的JS-SDK可以用来做什么了.微信的官方文档描述如下. 微信JS-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包. 通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照.选图.语音.位置等手机系统的能力,同时可以直接使用微信分享.扫一扫等微信特有的能力,为微信用户提供更优质的网页体验. 通过使用微信的JS-SDK,你可以让你网页在微信内调用拍照.语音.支付.位置.扫一扫这些只能在微信内使用的功能.进过下面

在vue项目中封装echarts的正确姿势

为什么需要封装echarts 每个开发者在制作图表时都需要从头到尾书写一遍完整的option配置,十分冗余 在同一个项目中,各类图表设计十分相似,甚至是相同,没必要一直做重复工作 可能有一些开发者忘记考虑echarts更新数据的特性,以及窗口缩放时的适应问题.这样导致数据更新了echarts视图却没有更新,窗口缩放引起echarts图形变形问题 我希望这个echarts组件能设计成什么样 业务数据和样式配置数据分离,我只需要传入业务数据就行了 它的大小要完全由使用者决定 不会因为缩放出现变形问题

如何正确的将HDF5加入到项目中进行编译(雾)?!

1 什么是HDF5 官方解释: HDF5 is a unique technology suite that makes possible the management of extremely large and complex data collections. 看起来HDF5就是一种用来存储各种数据的一个容器/管理者. 2 为什么要弄这个组件到项目中去? 鉴于很多模型都使用这个格式进行数据存储,所以在测试项目是难免会用到HDF5这个部分.所以这里对其加入到项目中做了以下总结. 3 将HDF

关于如何正确地在android项目中添加第三方jar包

1.下载第三方jar包 2.在android项目下创建一个libs目录(名称并不固定,你完全可以取其他名称) 3.在eclipse中右键点击libs目录,依次选择Import -> General -> File System,选中jar包所在目录, 然后选中这个目录下的jar包 注:到这一步为止你就成功地把jar包添加到项目中(但是还没有被android的虚拟机识别,因此如果这时你使用jar包中的类,编译都无法通过) 4.右键点击项目名,依次选择Build Path -> Config

android 项目中log信息的正确处理

我们在做项目中,调试程序的时候一般是debug或者打log,特别是看一些json数据之类的,都喜欢打log看看,但是在项目上线时log信息一定不能被打印出来,大家可以平时下载一些做的烂的app,连接eclipse有时候都可以看到一些请求的url或者返回的数据,这要是碰到一些技术好的人,是吧 干些啥事,你懂的,下面写一个log类,平时在项目中用到的,就是一个工具类, public class LogUitl { private static final boolean IS_OPEN = true