Java多线程:SimpleDateFormat

一、SimpleDateFormat的线程安全问题

  为什么SimpleDateFormat是线程不安全的?

  下面通过一个案例代码来说明

public class DateUtilTest {

    public static class TestSimpleDateFormatThreadSafe extends Thread {
        @Override
        public void run() {
            while(true) {
                try {
                    this.join(2000);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                try {
                    System.out.println(this.getName()+":"+DateUtil.parse("2013-05-24 06:02:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        for(int i = 0; i < 3; i++){
            new TestSimpleDateFormatThreadSafe().start();
        }

    }
}

执行输出如下:

Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
    at java.text.DateFormat.parse(DateFormat.java:335)
    at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
    at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
    at java.text.DateFormat.parse(DateFormat.java:335)
    at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
    at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Thread-2:Mon May 24 06:02:20 CST 2021
Thread-2:Fri May 24 06:02:20 CST 2013

说明:Thread-1和Thread-0报java.lang.NumberFormatException: multiple points错误,直接挂死,没起来;Thread-2 虽然没有挂死,但输出的时间是有错误的,比如我们输入的时间是:2017-05-24 06:02:20 ,当会输出:Mon May 24 06:02:20 CST 2021 这样的灵异事件

二.原因

  作为一个专业程序员,我们当然都知道,相比于共享一个变量的开销要比每次创建一个新变量要小很多。上面的优化过的静态的SimpleDateFormat版,之所在并发情况下回出现各种灵异错误,是因为SimpleDateFormat和DateFormat类不是线程安全的。我们之所以忽视线程安全的问题,是因为从SimpleDateFormat和DateFormat类提供给我们的接口上来看,实在让人看不出它与线程安全有何相干。只是在JDK文档的最下面有如下说明:

  SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。

  JDK原始文档如下:
  Synchronization:
  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.

  下面我们通过看JDK源码来看看为什么SimpleDateFormat和DateFormat类不是线程安全的真正原因:

  SimpleDateFormat继承了DateFormat,在DateFormat中定义了一个protected属性的 Calendar类的对象:calendar。只是因为Calendar累的概念复杂,牵扯到时区与本地化等等,Jdk的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。

  在format方法里,有这样一段代码:

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)这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
  线程1调用format方法,改变了calendar这个字段。
  中断来了。
  线程2开始执行,它也改变了calendar。
  又中断了。
  线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
  分析一下format的实现,我们不难发现,用到成员变量calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。
  这个问题背后隐藏着一个更为重要的问题--无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。

  这也同时提醒我们在开发和设计系统的时候注意下一下三点:

  1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明

  2.对线程环境下,对每一个共享的可变变量都要注意其线程安全性

  3.我们的类和方法在做设计的时候,要尽量设计成无状态的

三.解决办法



1、使用同步

public class DateSyncUtil {

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

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

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

说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。



2、使用ThreadLocal

public class DateUtils extends org.apache.commons.lang3.time.DateUtils {

    public static final String PATTERN_DATE_TIME = "yyyy-MM-dd HH:mm:ss";

    private  static Map<String, ThreadLocal<DateFormat>> dateFormatMap;

    /**
     * 创建单例
     * 没有使用单例模式的话,spring在注入数据时如果有逻辑调用了getDateFormat(pattern),会因为dateFormatMap尚未初始化而报NullPoint
     * @return
     */
    private static Map<String, ThreadLocal<DateFormat>> getDateFormatMap() {
        if (dateFormatMap == null) {
            synchronized (DateUtils.class) {
                if (dateFormatMap == null) {
                    dateFormatMap = new ConcurrentHashMap<>();
                }
            }
        }
        return dateFormatMap;
    }

    /**
     * 获取线程安全的DateFormat
     * SimpleDateFormat有两个问题:
     * 1.非线程安全,仅仅是声明为static,使用时不上锁在并发状况下调用parse()有可能得到错误的时间
     * 2.频繁实例化有可能导致内存溢出
     * 使用ThreadLocal将DateFormat变为线程独享,既可以避免并发问题,又可以减少反复创建实例的开销
     * @param pattern
     * @return
     */
    public  static DateFormat getDateFormat(final String pattern) {
        ThreadLocal<DateFormat> dateFormat = getDateFormatMap().get(pattern);
        if (dateFormat == null) {
            dateFormat = new ThreadLocal<DateFormat>() {
                @Override
                protected DateFormat initialValue() {
                    return new SimpleDateFormat(pattern);
                }
            };
            dateFormatMap.put(pattern, dateFormat);
        }
        return dateFormat.get();
    }

    /**
     * Date格式化 "yyyy-MM-dd HH:mm:ss"字符串
     *
     * @param date
     * @return
     */
    public static String DateFormatToString(Date date) {
        DateFormat sdf = getDateFormat(PATTERN_DATE_TIME);
        return sdf.format(date);
    }
}

说明:使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。



3.抛弃JDK,使用其他类库中的时间格式化类

  1.使用Apache commons 里的FastDateFormat,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行format, 不能对日期串进行解析。

  2.使用Joda-Time类库来处理时间相关问题

参考资料:

  深入理解Java:SimpleDateFormat安全的时间格式化

   SimpleDateFormat的线程安全问题与ThreadLocal

时间: 2024-08-29 23:40:19

Java多线程:SimpleDateFormat的相关文章

Java多线程(三)SimpleDateFormat

多线程报错:java.lang.NumberFormatException: multiple points SimpleDateFormat是非线程安全的,在多线程情况下会有问题,在每个线程下得各自new SimpleDateFormat()就可以了 实现有两种方法: 1. public class DateTools { public static Date parse(String formatPattern,String dateString) throws ParseException

Java多线程系列八——volatile和ThreadLocal

参考资料: http://ifeve.com/java-memory-model-4/ http://www.infoq.com/cn/articles/java-memory-model-1 http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/ https://en.wikipedia.org/wiki/Singleton_pattern#Java_5_solution https://www.i

深入理解Java:SimpleDateFormat安全的时间格式化 ;

想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调试的问题,因为 DateFormat 和 SimpleDateFormat 类不都是线程安全的,在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题.下面我们通过一个具体的场景来一步步的深入学习和理解SimpleDateFormat类. 一.引子 我们都是优秀的程

java多线程的常用方法(以及注意事项)

1 /* 2 * 线程的常用方法 3 * 1.start(); 4 * 2.run(); 5 * 3.sleep(int millsecond); 6 * 4.isAlive(); -->判断线程是否还在运行 7 * 5.currentThread(); -->返回当前正在使用CPU资源的线程 8 * 6.interrupt(); -->激活休眠的线程 9 * */ 但是需要注意的一个小点是: 1 /* 2 * 需要注意到地方:一个已经运行的线程在没有进入死亡状态时, 3 * 不要在给线

java 多线程——quartz 定时调度的例子

java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 java 多线程—— 线程等待与唤醒 概述 第1部分 配置 第2部分 代码示例 第1部分 配置 有关quartz的api文档地址:Quartz Enterprise Job Scheduler 1.8.6 API 主要接口目录: 重点看下Job,Scheduler,Trigger,JobDetai

java多线程常见面试题

下面是Java线程相关的热门面试题,你可以用它来好好准备面试. 1) 什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速.比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒.J 2) 线程和进程有什么区别? 线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务.不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间.别把它

【转】深入理解Java:SimpleDateFormat安全的时间格式化

[转]深入理解Java:SimpleDateFormat安全的时间格式化 想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调试的问题,因为 DateFormat 和 SimpleDateFormat 类不都是线程安全的,在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题.下面我们通过一个具体的场景来一步步的深入

java:SimpleDateFormat使用注意项

前言:Java中,说起日期转换,SimpleDateFormat不得不提,但是在实战操作中,使用时千万要小心.否则将会出现" java.lang.NumberFormatException: multiple points"错误,那么究竟是为什么呢? 首先,请随小编来看看一个用到SimpleDateFormat的日期转换类. package com.mwq.format; import java.text.ParseException; import java.text.SimpleD

java多线程核心技术梳理(附源码)

java多线程核心技术梳理(附源码) java多线程核心技术梳理附源码 写在前面 java多线程 对象及变量的并发访问 线程间通信 Lock的使用 定时器 单例模式与多线程 拾遗补增 参考资料 本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,lock的使用,定时器,单例模式,以及线程状态与线程组. 写在前面 花了一周时间阅读<java多线程编程核心技术>(高洪岩 著),本文算是此书的整理归纳,书中几乎所有示例,我都亲手敲了一遍,并上传到了我的githu