使用SimpleDateFormat的时候小心点

SimpleDateFormat是Java提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个SimpleDateFormat实例对日期进行解析或者格式化会导致程序出错。

代码示例演示

写一段小Demo来模拟多线程下SimpleDateFormat做时间格式化的时候报错,代码如下:

package com.vernon.test.demo.jdk.text;  
import java.text.ParseException;  
import java.text.SimpleDateFormat;  
/**
 * Created with vernon-test
 * Description:
 * User:chenyuan
 * Date:2019/3/20 Time:2:03 PM
 */
public class SimpleDateFormatCase {  
    //1、创建单例实例
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) {
        //2、创建多个线程,并启动
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {
                          //3、使用单例日期实例解析文本
                        System.out.println(sdf.parse("2017-12-13 15:17:27"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
              //4、启动线程
            thread.start();
        }
    }
}
控制台正常的情况: 运气好~
Connected to the target VM, address: '127.0.0.1:57434', transport: 'socket'  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Disconnected from the target VM, address: '127.0.0.1:57434', transport: 'socket'  
控制台非正常的情况 运气不好~
Connected to the target VM, address: '127.0.0.1:57756', transport: 'socket'  
Exception in thread "Thread-2" Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points  
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.vernon.test.demo.jdk.text.SimpleDateFormatCase$1.run(SimpleDateFormatCase.java:23)
    at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points  
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.vernon.test.demo.jdk.text.SimpleDateFormatCase$1.run(SimpleDateFormatCase.java:23)
    at java.lang.Thread.run(Thread.java:748)
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Fri Dec 12 15:17:27 CST 2217  
Thu Dec 13 15:17:27 CST 2012  
Wed Dec 13 15:17:27 CST 2017  
Wed Dec 13 15:17:27 CST 2017  
Fri Jun 09 15:17:27 CST 5881628  
Disconnected from the target VM, address: '127.0.0.1:57756', transport: 'socket'  

通过IntelliJ IDEA的功能查看一下SimpleDateFormat的一个类关系图:

http://static.cyblogs.com/WX20200116-175032.png

可知每个SimpleDateFormat实例里面有一个Calendar对象,从后面会知道其实SimpleDateFormat之所以是线程不安全的就是因为Calendar是线程不安全的,后者之所以是线程不安全的是因为其中存放日期数据的变量都是线程不安全的,比如里面的fieldstime等。

public Date parse(String text, ParsePosition pos) {  
    //1、解析日期字符串放入CalendarBuilder的实例calb中
    .....
    Date parsedDate;
    try {
          //2、使用calb中解析好的日期数据设置calendar
        parsedDate = calb.establish(calendar).getTime();
        ...
    }
    catch (IllegalArgumentException e) {
       ...
        return null;
    }
    return parsedDate;
}

Calendar establish(Calendar cal) {  
   ...
   //3、重置日期对象cal的属性值
   cal.clear();
   //4、使用calb中中属性设置cal
   ...
   //5、返回设置好的cal对象
   return cal;
}

public final void clear() {  
    for (int i = 0; i < fields.length; ) {
        stamp[i] = fields[i] = 0; // UNSET == 0
        isSet[i++] = false;
    }
    areAllFieldsSet = areFieldsSet = false;
    isTimeSet = false;
}
  • 1、解析日期字符串放入CalendarBuilder的实例calb中;

  • 2、使用calb中解析好的日期数据设置calendar

  • 3、重置日期对象cal的属性值;

  • 4、使用calb中中属性设置cal

  • 5、返回设置好的cal对象;

从上面步骤可知步骤3、4、5操作不是原子性操作,当多个线程调用parse方法时候比如线程A执行了步骤3、4也就是设置好了cal对象,在执行步骤5前线程B执行了步骤3清空了cal对象,由于多个线程使用的是一个cal对象,所以线程A执行步骤5返回的就可能是被线程B清空后的对象,当然也有可能线程B执行了步骤4被线程B修改后的cal对象,从而导致程序错误。

那么怎么解决呢?

第一种方式:

每次使用时候new一个SimpleDateFormat的实例,这样可以保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其它引用,就会需要被回收,开销会很大。

package com.vernon.test.demo.jdk.text;  
import java.text.ParseException;  
import java.text.SimpleDateFormat;

/**
 * Created with vernon-test
 * Description:
 * User:chenyuan
 * Date:2019/3/20
 * Time:2:07 PM
 */
public class SimpleDateFormatCase2 {

    public static void main(String[] args) {
        for (int i = 0; i < 10; ++i) {
            final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {
                            System.out.println(sdf.parse("2020-01-16 15:17:27"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
        }
    }
}
第二种方式:

究其原因是因为多线程下步骤3、4、5三个步骤不是一个原子性操作,那么容易想到的是对其进行同步,让3、4、5成为原子操作,可以使用synchronized进行同步。

package com.vernon.test.demo.jdk.text;

import java.text.ParseException;  
import java.text.SimpleDateFormat;  
/**
 * Created with vernon-test
 * Description:
 * User:chenyuan Date:2019/3/21 Time:10:32 AM
 */
public class SimpleDateFormatCase3 {

    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {
                        synchronized (sdf) {
                            System.out.println(sdf.parse("2020-01-16 15:17:27"));
                        }
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
        }
    }
}
第三种方式:

使用ThreadLocal,这样每个线程只需要使用一个SimpleDateFormat实例相比第一种方式大大节省了对象的创建销毁开销,并且不需要对多个线程直接进行同步,使用ThreadLocal方式。

package com.vernon.test.demo.jdk.text;

import java.text.DateFormat;  
import java.text.ParseException;  
import java.text.SimpleDateFormat;  
/**
 * Created with vernon-test
 * Description:
 * User:chenyuan
 * Date:2019/3/21 Time:10:32 AM
 */
public class SimpleDateFormatCase4 {

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

    public static void main(String[] args) {
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {
                        System.out.println(safeSdf.get().parse("2020-01-16 15:17:27"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
        }
    }
}
第四种方式:

在JDK8中新增了DateTimeFormatter,由DateTimeFormatter的静态方法ofPattern()构建日期格式,LocalDateTimeLocalDate等一些表示日期或时间的类使用parseformat方法把日期和字符串做转换。使用新的API,整个转换过程都不需要考虑线程安全的问题。

package com.vernon.test.demo.jdk.text;  
import java.time.LocalDate;  
import java.time.format.DateTimeFormatter;  
/**
 * Created with vernon-test
 * Description:
 * User:chenyuan
 * Date:2019/3/21 Time:10:32 AM
 */
public class SimpleDateFormatCase5 {  
    static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) {
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    System.out.println(LocalDate.parse("2020-01-16 15:17:27", formatter));
                }
            });
            thread.start();
        }
    }
}

总结

Java8发布,已有数年之久,但是发现很多人都还是坚持着用SimpleDateFormatDate进行时间操作。SimpleDateFormat这个类不是线程安全的,在使用的时候稍不注意,就会产生致命的问题。Date这个类,是可以重新设置时间的,这对于一些类内部的属性来说,是非常不安全的。 如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。

简栈文化服务订阅号