SimpleDateFormat,Calendar 线程非安全的问题

SimpleDateFormat是Java中非常常见的一个类,用来解析和格式化日期字符串。但是SimpleDateFormat在多线程的环境并不是安全的,这个是很容易犯错的部分,接下来讲一下这个问题出现的过程以及解决的思路。

问题描述:
先看代码,用来获取一个月的天数的:

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class DateUtil {

    /**
     * 获取月份天数
     * @param time 201202
     * @return
     */
    public static int getDays(String time) throws Exception {
//        String time = "201202";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
        Date date = sdf.parse(time);
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        int day = c.getActualMaximum(Calendar.DATE);
        return day;
    }

}

可以看到在这个方法里,每次要获取值的时候就先要创建一个SimpleDateFormat的实例,频繁调用这个方法的情况下很耗性能。为了避免大量实例的频繁创建和销毁,我们通常会使用单例模式或者静态变量进行改造,一般会这么改:

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class DateUtil {

        private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");

    /**
     * 获取月份天数
     * @param time 201202
     * @return
     */
    public static int getDays(String time) throws Exception {
//        String time = "201202";
        Date date = sdf.parse(time);
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        int day = c.getActualMaximum(Calendar.DATE);
        return day;
    }

}

此时不管调用多少次这个方法,java虚拟机里只有一个SimpleDateFormat对象,效率和性能肯定要比第一个方法好,这个也是很多程序员选择的方法。但是,在这个多线程的条件下,多个thread共享同一个SimpleDateFormat,而SimpleDateFormat本身又是线程非安全的,这样就很容易出各种问题。

验证问题:
用一个简单的例子验证一下多线程环境下SimpleDateFormat的运行结果:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;

public class DateUtil {
    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date) {
        return dateFormat.format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return dateFormat.parse(dateStr);
    }

    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(1);
        final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"};
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    for (int i = 0; i < 10; i++){
                        try {
                            System.out.println(Thread.currentThread().getName()+ "\t" + parse(strs[i % strs.length]));
                            Thread.sleep(100);
                        } catch (ParseException e) {
                            e.printStackTrace();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
        latch.countDown();
    }
}

看一下运行的结果:

Thread-9    Fri Jan 01 10:24:00 CST 2016
Thread-1    Sat Feb 25 00:48:00 CST 20162017
Thread-5    Sat Feb 25 00:48:00 CST 20162017
Exception in thread "Thread-4" Exception in thread "Thread-6" java.lang.NumberFormatException: For input string: "2002.E20022E"
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
    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:2162)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at DateUtil.parse(DateUtil.java:24)
    at DateUtil$2.run(DateUtil.java:45)

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

查找问题:

首先看一下SimpleDateFormat的源码:

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

可以看到format()方法先将日期存放到一个Calendar对象中,而这个Calendar在SimpleDateFormat中是以成员变量的形式存在的。随后调用subFormat()时会再次用到成员变量Calendar。这就是问题所在。同样,在parse()方法里也会存在相应的问题。
试想,在多线程环境下,如果两个线程都使用同一个SimpleDateFormat实例,那么就有可能存在其中一个线程修改了calendar后紧接着另一个线程也修改了calendar,那么随后第一个线程用到calendar时已经不是它所期待的值了。

避免问题:

那么,如何保证SimpleDateFormat的线程安全呢?
1.每次使用SimpleDateFormat时都创建一个局部的SimpleDateFormat对象,跟一开始的那个方法一样,但是存在性能上的问题,开销较大。
2.加锁或者同步

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

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,多线程并发量大的时候会对性能有一定的影响。
3.使用ThreadLocal

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String format(Date date) {
        return local.get().format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return local.get().parse(dateStr);
    }
}

使用ThreadLocal可以确保每个线程都可以得到一个单独的SimpleDateFormat对象,既避免了频繁创建对象,也避免了多线程的竞争。

原文地址:https://www.cnblogs.com/lyy-2016/p/8638553.html

时间: 2025-01-12 18:47:55

SimpleDateFormat,Calendar 线程非安全的问题的相关文章

JDK中的SimpleDateFormat线程非安全

在JDK中使用SimpleDateFormat的时候都会遇到线程安全的问题,在JDK文档中也说明了该类是线程非安全的,建议对于每个线程都创建一个SimpleDateFormat对象.如下面一个Case中,多个线程去调用SimpleDateFormat中得parse方法: @Test public void testUnThreadSafe() throws Exception { final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM

java===Date,DateFormat,SimpleDateFormat,Calendar类

package 常用类.Date; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /**月份从0-11的整数表示,0是一月,11是12月 * 日期对象-->毫秒值 getTime方法: * 原因:可以通过毫秒值来进行日期运算 * 毫秒值-->日期对象 setTime方法或者构造方法new Date(毫秒

java基础1.5版后新特性 自动装箱拆箱 Date SimpleDateFormat Calendar.getInstance()获得一个日历对象 抽象不要生成对象 get set add System.arrayCopy()用于集合等的扩容

8种基本数据类型的8种包装类 byte Byte short Short int Integer long Long float Float double Double char Character boolean Boolean Integer a=127; Integer b=127;//虚拟机自动装箱时进行了特殊处理,-127~128以下的自动取有过的 System.out.println(a==b);结果为true 如果是Integer a=128; Integer b=128; Sys

【源码】HashMap源码及线程非安全分析

最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了.当然,还是先从炙手可热的HashMap,每次读都会有一些收获.当然,JDK8对HashMap有一次优化. 一.一些参数 我们首先看到的,应该是它的一些基本参数,这对于我们了解HashMap有一定的作用.他们分别是: 参数 说明 capacity 容量,默认为16,最大为2^30 loadFactor 加载因子,默认0.75 threshold resize的阈值,capacity *

Java 中常用的类:包括基本类型的包装类、Date 类、SimpleDateFormat 类、 Calendar 类、 Math 类

JAVA中的包装类 包装类里没有String,它是引用数据类型 基本类型是不能调用方法的,而其包装类具有很多方法 包装类主要提供了两大类方法: 1. 将本类型和其他基本类型进行转换的方法 2. 将字符串和本类型及包装类互相转换的方法 基本类型 对应的包装类 byte Byte short Short int Integer long Long float Float double Double char Character boolean Boolean Integer m=new Intege

CalendarDemo Calendar 类的创建及用法

/** Calendar 类,其主要作用于其方法可以对时间分量进行运算. 它为特定瞬间与一组诸如 YEAR.MONTH.DAY_OF_MONTH.HOUR 等日历字段之间的转换提供了一些方法, 并为操作日历字段提供了一些方法. 它是一个抽象类,其提供了一个工厂方法:Calendar getInstance(). 该方法可以根据当前系统所在地区获取一个适当的Calendar的子类实现. 主要方法: 1.void set(int field,int value) 该方法可以通过对不同的时间分量分别设

黑马程序员-集合框架(Map和Collections)

--Java培训.Android培训.iOS培训..Net培训.期待与您交流!--- 一.概述 Map是一种存储键值对的存储容器,而且保证键的唯一性.提供一种以"键"标识"值"的数据存储方式.接口形式为:Map<K,V>,其中K是此映射所维护的键的类型,V是映射值的类型.其有两个常用子类,HashMap和TreeMap,另有HashTable与HashMap功能类似,是早期版本.三者特点与不同如下: HashMap:JDK1.2版本出现,底层使用哈希表数

Java学习的一些基础笔记

classpath.;%java_home%\lib;%java_home%\lib\tools.jar;D:\Java\;java_homeD:\Program Files\Java\jdk1.8.0_51pathC:\Users\BaseKing-Sunie\AppData\Local\Code\bin;%java_home%\bin;%java_home%\jre\bin;D:\adt-bundle-windows-x86_64_20131020\sdk\tools;D:\adt-bund

Java知识汇集(1)

由于一些原因需要整理一些Java的知识,把整理出来的结果分享一下. 1.三大基本特性 我们以Java的三大基本特性为角度展开 封装.继承.多态 封装: 封装是把过程和数据包围起来,对数据的访问只能通过已定义的接口.面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治.封装的对象,这些对象通过一个受保护的接口访问其他对象.封装主要实现了隐藏细节,对用户提供访问接口,无需关心方法的具体实现. Java的权限控制等级由大到小依次为:public.protected.(default).p