Java-JUC(十四):SimpleDateFormat是线程不安全的

SimpleDateFormat是Java提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个SimpleDateFormat实例对日期进行解析、格式化都会导致程序出错,接下来就讨论下它为何是线程不安全的,以及如何避免。

问题复现

编写测试代码如下:

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) {
        String[] waitingFormatTimeItems = { "2019-08-06", "2019-08-07", "2019-08-08" };
        for (int i = 0; i < waitingFormatTimeItems.length; i++) {
            final int i2 = i;
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 100; j++) {
                        String str = waitingFormatTimeItems[i2];
                        String str2 = null;
                        Date parserDate = null;
                        try {
                            parserDate = sdf.parse(str);
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                        str2 = sdf.format(parserDate);
                        System.out.println("i: " + i2 + "\tj: " + j + "\tThreadName: " + Thread.currentThread().getName() + "\t" + str + "\t" + str2);
                        if (!str.equals(str2)) {
                            throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2);
                        }
                    }
                }
            });
            thread.start();
        }
    }

运行会抛出java.lang.RuntimeException,说明处理的结果时不正确的,从下边日志也看出来。

i: 2    j: 0    ThreadName: Thread-2    2019-08-08    2208-09-17
Exception in thread "Thread-2" Exception in thread "Thread-1" Exception in thread "Thread-0"
i: 1    j: 0    ThreadName: Thread-1    2019-08-07    2208-09-17
i: 0    j: 0    ThreadName: Thread-0    2019-08-06    2208-09-17
java.lang.RuntimeException: date conversion failed after 0 iterations. Expected 2019-08-08 but got 2208-09-17
    at dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:36)
    at java.lang.Thread.run(Thread.java:748)
java.lang.RuntimeException: date conversion failed after 0 iterations. Expected 2019-08-07 but got 2208-09-17
    at dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:36)
    at java.lang.Thread.run(Thread.java:748)
java.lang.RuntimeException: date conversion failed after 0 iterations. Expected 2019-08-06 but got 2208-09-17
    at dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:36)
    at java.lang.Thread.run(Thread.java:748)

测试代码多运行几次,会发现抛出 java.lang.NumberFormatException 异常:

Exception in thread "Thread-1" Exception in thread "Thread-0" Exception in thread "Thread-2" 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.dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:29)
    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.dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:29)
    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.dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:29)
    at java.lang.Thread.run(Thread.java:748)

问题分析

首先看下SimpleDateFormat的类图结构:

从类图和源代码从都可以发现,SimpleDateFormat内部依赖于Calendar对象,通过下边代码分析会发现:实际上SimpleDateFormat的线程不安全就是因为Calendar是线程不安全的。

Calendar内部存储的日期数据的变量field,time等都是不安全的,更重要的Calendar内部函数操作对变量操作是不具有原子性的操作。

SimpleDateFormat#parse方法:

    @Override
    public Date parse(String text, ParsePosition pos)
    {
        checkNegativeNumberExpression();

        int start = pos.index;
        int oldStart = start;
        int textLength = text.length();

        boolean[] ambiguousYear = {false};

        //(1)解析日期字符串放入CalendarBuilder的实例calb中
        CalendarBuilder calb = new CalendarBuilder();

        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:
                if (start >= textLength || text.charAt(start) != (char)count) {
                    pos.index = oldStart;
                    pos.errorIndex = start;
                    return null;
                }
                start++;
                break;

            case TAG_QUOTE_CHARS:
                while (count-- > 0) {
                    if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
                        pos.index = oldStart;
                        pos.errorIndex = start;
                        return null;
                    }
                    start++;
                }
                break;

            default:
                // Peek the next pattern to determine if we need to obey the number of pattern letters for parsing.
                // It‘s required when parsing contiguous digit text (e.g., "20010704") with a pattern which has no delimiters between fields, like "yyyyMMdd".
                boolean obeyCount = false;

                // In Arabic, a minus sign for a negative number is put after the number. Even in another locale, a minus sign can be put after a number using DateFormat.setNumberFormat().
                // If both the minus sign and the field-delimiter are ‘-‘, subParse() needs to determine whether a ‘-‘ after a number in the given text is a delimiter or is a minus sign for the preceding number.
                // We give subParse() a clue based on the information in compiledPattern.
                boolean useFollowingMinusSignAsDelimiter = false;

                if (i < compiledPattern.length) {
                    int nextTag = compiledPattern[i] >>> 8;
                    if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||
                          nextTag == TAG_QUOTE_CHARS)) {
                        obeyCount = true;
                    }

                    if (hasFollowingMinusSign &&
                        (nextTag == TAG_QUOTE_ASCII_CHAR ||
                         nextTag == TAG_QUOTE_CHARS)) {
                        int c;
                        if (nextTag == TAG_QUOTE_ASCII_CHAR) {
                            c = compiledPattern[i] & 0xff;
                        } else {
                            c = compiledPattern[i+1];
                        }

                        if (c == minusSign) {
                            useFollowingMinusSignAsDelimiter = true;
                        }
                    }
                }
                start = subParse(text, start, tag, count, obeyCount,
                                 ambiguousYear, pos,
                                 useFollowingMinusSignAsDelimiter, calb);
                if (start < 0) {
                    pos.index = oldStart;
                    return null;
                }
            }
        }

        // At this point the fields of Calendar have been set.  Calendar
        // will fill in default values for missing fields when the time
        // is computed.

        pos.index = start;

        Date parsedDate;
        try {
        //(2)使用calb中解析好的日期数据设置calendar
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }
        // An IllegalArgumentException will be thrown by Calendar.getTime()
        // if any fields are out of range, e.g., MONTH == 17.
        catch (IllegalArgumentException e) {
            pos.errorIndex = start;
            pos.index = oldStart;
            return null;
        }

        return parsedDate;
    }

CalendarBuilder#establish方法:

    Calendar establish(Calendar cal) {
        boolean weekDate = isSet(WEEK_YEAR)
                            && field[WEEK_YEAR] > field[YEAR];
        if (weekDate && !cal.isWeekDateSupported()) {
            // Use YEAR instead
            if (!isSet(YEAR)) {
                set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
            }
            weekDate = false;
        }

        //(3)重置日期对象cal的属性值
        cal.clear();

        //(4) 使用calb中中属性设置cal
        // Set the fields from the min stamp to the max stamp so that
        // the field resolution works in the Calendar.
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    cal.set(index, field[MAX_FIELD + index]);
                    break;
                }
            }
        }

        if (weekDate) {
            int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
            int dayOfWeek = isSet(DAY_OF_WEEK) ?
                                field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
            if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
                if (dayOfWeek >= 8) {
                    dayOfWeek--;
                    weekOfYear += dayOfWeek / 7;
                    dayOfWeek = (dayOfWeek % 7) + 1;
                } else {
                    while (dayOfWeek <= 0) {
                        dayOfWeek += 7;
                        weekOfYear--;
                    }
                }
                dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
            }
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
        }

        //(5)返回设置好的cal对象
        return cal;
    }

Calendar#clear()方法:

代码(3)重置Calendar对象里面的属性值,如下代码:

    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;
    }

代码(4)使用calb中解析好的日期数据设置cal对象
代码(5) 返回设置好的cal对象

代码(3)、(4)、(5)这几步骤一起操作不具有原子性,当A线程操作了(3)、(4),当将要执行(5)返回结果之前,如果B线程执行(3)会导致线程A的结果错误。

那么多线程下如何保证SimpleDateFormat的安全性呢?

1)每个线程使用时,都new一个SimpleDateFormat的实例,这保证每个线程都用各自的Calendar实例。

    public static void main(String[] args) {
        String[] waitingFormatTimeItems = { "2019-08-06", "2019-08-07", "2019-08-08" };
        for (int i = 0; i < waitingFormatTimeItems.length; i++) {
            final int i2 = i;
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

                    for (int j = 0; j < 100; j++) {
                        String str = waitingFormatTimeItems[i2];
                        String str2 = null;
                        Date parserDate = null;try {
                            parserDate = sdf.parse(str);
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                        str2 = sdf.format(parserDate);

                        System.out.println("i: " + i2 + "\tj: " + j + "\tThreadName: " + Thread.currentThread().getName() + "\t" + str + "\t" + str2);
                        if (!str.equals(str2)) {
                            throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2);
                        }
                    }
                }
            });
            thread.start();
        }
    }

这种方式缺点:每个线程都 new 一个对象,并且使用后由于没有其它引用,都需要被回收,开销比较大。

2)经过分析最终导致SimpleDateFormat的线程不安全原因是步骤(3)、(4)、(5)不是一个原子性操作,那么就可以对其进行同步,让(3)、(4)、(5)成为原子操作,可以使用ReetentLock。Synchronized等进行同步。

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) {
        String[] waitingFormatTimeItems = { "2019-08-06", "2019-08-07", "2019-08-08" };
        for (int i = 0; i < waitingFormatTimeItems.length; i++) {
            final int i2 = i;
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 100; j++) {
                        String str = waitingFormatTimeItems[i2];
                        String str2 = null;
                        Date parserDate = null;
                        synchronized (sdf) {
                            try {
                                parserDate = sdf.parse(str);
                            } catch (ParseException e) {
                                e.printStackTrace();
                            }
                            str2 = sdf.format(parserDate);
                        }
                        System.out.println("i: " + i2 + "\tj: " + j + "\tThreadName: " + Thread.currentThread().getName() + "\t" + str + "\t" + str2);
                        if (!str.equals(str2)) {
                            throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2);
                        }
                    }
                }
            });
            thread.start();
        }
    }

使用了同步锁,意味着多线程下会竞争锁,在高并发情况下会导致系统响应性能下降。

3)使用ThreadLocal,这样每个线程只需要使用一个SimpleDateFormat实例,在多线程下比第一种节省了对象的销毁开销,并且不需要对多线程进行同步,代码如下:

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

ThreadLocal包含定义了一个ThreadLocalMap,ThreadLocalMap的key为弱引用的线程(ThreadLocal<?>),要保存的线程局部变量的值为value(Object).

    private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        };
    };

    public static void main(String[] args) {
        String[] waitingFormatTimeItems = { "2019-08-06", "2019-08-07", "2019-08-08" };
        for (int i = 0; i < waitingFormatTimeItems.length; i++) {
            final int i2 = i;
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

                    for (int j = 0; j < 100; j++) {
                        String str = waitingFormatTimeItems[i2];
                        String str2 = null;
                        Date parserDate = null;
                        try {
                            parserDate = threadLocal.get().parse(str);
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                        str2 = threadLocal.get().format(parserDate);
                        System.out.println("i: " + i2 + "\tj: " + j + "\tThreadName: " + Thread.currentThread().getName() + "\t" + str + "\t" + str2);
                        if (!str.equals(str2)) {
                            throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2);
                        }
                    }
                }
            });
            thread.start();
        }
    }

参考:

线程不安全的SimpleDateFormat

原文地址:https://www.cnblogs.com/yy3b2007com/p/11360895.html

时间: 2024-10-28 09:44:31

Java-JUC(十四):SimpleDateFormat是线程不安全的的相关文章

Java多线程(四)、线程池(转)

Java多线程(四).线程池 分类: javaSE综合知识点 2012-09-19 17:46 3943人阅读 评论(1) 收藏 举报 系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统的交互.在这种情况下,使用线程池可以很好的提供性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池. 与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线

java之十四 网络连接

1969年,KenThompson和Dennis Ritchie在MurrayHill,New Jersey的贝尔电话实验室开发了与C语言一致的UNIX.很多年来,UNIX的发展停留在贝尔实验室和一些大学及研究机构,用特意设计的DEC PDP机器运行.到了1978 年,Bill Joy在Cal Berkeley领导了一个项目,给UNIX增添新的特性,例如虚拟内存和全屏显示功能.到了1984年早期,当Bill正准备建立Sun Microsystems,它发明了4.2BSD,即众所周知的Berkel

JAVA并发实现四(守护线程和线程阻塞)

守护线程     Java中有两类线程:User Thread(用户线程).Daemon Thread(守护线程) 用户线程即运行在前台的线程,而守护线程是运行在后台的线程. 守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通.非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程.当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护者,也就没有继续运行程序的必要了.如果有非守护线程仍然存活,VM就不会退出. 守护线程并非只有虚拟机内部提

高并发第十四弹:线程池的介绍及使用

单线程就不说了因为简单,并且 在实际的生产环境中一般必须来说 线程资源都是由线程池提供线程资源的. 线程池的好处 重用存在的线程,减少对象创建.消亡的开销,性能好 可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞. 提供定时执行.定期执行.单线程.并发数控制等功能. 在线程池的类图中,我们最常使用的是最下边的Executors,用它来创建线程池使用线程.那么在上边的类图中,包含了一个Executor框架,它是一个根据一组执行策略的调用调度执行和控制异步任务的框架,

java(十四)反射和正则表达式

反射机制: java反射机制是在运行状态中,对于任意一个类(class文件),都能够知道这个类的所有属性和方法: 对于任意一个对象,都能够调用他的任意一个方法和属性: 这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制. 说简单点:动态获取类中的信息,就是java的反射.可以理解为对类的剖析. 反射技术提高了应用的扩展性,而且应用简单,所以很常用. 想要对一个类文件进行解剖,只要获取该类的字节码文件对象即可.怎么获取呢? 获取字节码对象的方式: package day28;

再回首Java第二十四天

Callable和FutureJava1.5开始,Java提供了Callable接口,Callable接口提供了一个call()方法作为线程的执行体,但call()方法run()方法的功能更强大:?call()方法可以有返回值?call()方法可以声明抛出异常因此我们完全可以提供一个Callable对象作为Thread的target,而该线程的执行体就是该Callable对象的call方法.问题是:Callable对象时JDK1.5开始新增的接口,而它并不是Runnable的子接口,所以Call

Java Nio 十四、Java NIO vs. IO

最后更新时间:2014-06-23 当学习Java NIO和IO的API的时候,一个问题很快的就会出现中我们的脑中: 我什么时候应该使用IO,什么时候应该使用NIO? 在这篇文章中我将会尝试着写出中NIO和IO之间不同的地方,他们的使用场景,以及他们怎么影响你的代码设计. Java NIO和IO的主要不同 下面的表格总结了Java NIO和IO的主要不同.针对这个表格中的不同点我将会给予更加详细的说明. IO NIO 基于流的 基于缓冲区的 堵塞IO 非堵塞IO   Selectors(选择器)

第十四节:线程劫持

本章前面讨论了垃圾回收期算法.但是,这些讨论有一个很大的前提:仅有一个线程运行,在现实世界,经常会出现多个线程同时访问托管堆的情况,或至少有多个线程同时操作托管堆中分配的对象.一个线程引发垃圾回收时,其它线程绝对不能访问其他任何对(包含他们自己线程栈上的引用),这是因为垃圾回收器可能移动对象,更改其内存地址. 因此,当垃圾回收器想要开启一次垃圾回收时,正在执行托管代码的所有线程都必须挂起.CLR使用几种不同的机制来确保安全的挂起线程,使垃圾回收期可以正常执行,之所以存在多种机制,目的是尽量保持线

Java Web(十四) 编写MyBookStore项目的总结

这几天一直没有发博文,原因是在写一个书城的小项目,作为web学习的最后沉淀,接下来就要到框架的学习了.项目最后会分享源码链接.有需要的同学可以拿到自己玩一玩 --WH 一.项目介绍 从网上找的一个培训机构的小项目,名称叫做 书城购物网站 吧,其中就是分前后台,前台用于显示各种类型的书籍商品,可以用于加入购物车,可以购买付款下单,后台就用来对书籍.书类别的增删改,和对订单的处理.一个不是很复杂的项目,但是在没开始编写之前,确实是毫无头绪,无从下手,那是因为没有对该项目有一个大体的认识,不知道其中的

JAVA中十四种常见开发工具及其特点

1.JDK (Java Development Kit)Java开发工具集 SUN的Java不仅提了一个丰富的语言和运行环境,而且还提了一个免费的Java开发工具集(JDK).开发人员和最终用户可以利用这个工具来开发java程序. JDK简单易学,可以通过任何文本编辑器(如:Windows 记事本.UltrEdit.Editplus.FrontPage以及dreamweaver等)编写Java源文件,然后在DOS状况下利通过javac命令将Java源程序编译成字节码,通过Java命令来执行编译后