SimpleDateFormat与DateTimeFormatter

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

  • 如果我们把SimpleDateFormat定义成static成员变量,那么多个thread之间会共享这个SimpleDateFormat对象, 所以Calendar对象也会共享。
public static SimpleDateFormat formater = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");

System.out.println(formater.format(new Date())+" Exception made...");
  • DateFormat.java
    public final String format(Date date)
    {
        return format(date, new StringBuffer(),
                      DontCareFieldPosition.INSTANCE).toString();
    }

    public abstract StringBuffer format(Date date, StringBuffer toAppendTo,
                                        FieldPosition fieldPosition);
  • SimpleDateFormat.java
    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo,
                               FieldPosition pos)
    {
    //  如此轻易地使用内部变量,肯定不能线程安全
    //  线程都对pos进行写操作,必然会影响其他线程的读操作
        pos.beginIndex = pos.endIndex = 0;
        return format(date, toAppendTo, pos.getFieldDelegate());
    }

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

1.1 复现错误

代码参考

public class DateFormatTest {
    private static SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
    private static String date[] = { "01-Jan-1999", "09-Jan-2000", "08-Jan-2001" , "07-Jan-2002" , "06-Jan-2003" , "05-Jan-2004" , "04-Jan-2005" , "03-Jan-2006" , "02-Jan-2007" };

    public static void main(String[] args) {
        for (int i = 0; i < date.length; i++) {
            final int temp = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            String str1 = date[temp];
                            String str2 = sdf.format(sdf.parse(str1));
                            System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2);
                            if(!str1.equals(str2)){
                                throw new RuntimeException(Thread.currentThread().getName()
                                        + ", Expected " + str1 + " but got " + str2);
                            }
                        }
                    } catch (Exception e) {
                        throw new RuntimeException("parse failed", e);
                    }
                }
            }).start();
        }
    }
}

2.SimpleDateFormat线程不安全的解决方法

2.1 将SimpleDateFormat定义成局部变量:

SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
String str1 = "01-Jan-2010";
String str2 = sdf.format(sdf.parse(str1));

缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。

2.2 加一把线程同步锁:synchronized(lock)

public class SyncDateFormatTest {
    private static SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
    private static String date[] = { "01-Jan-1999", "01-Jan-2000", "01-Jan-2001" };
 
    public static void main(String[] args) {
        for (int i = 0; i < date.length; i++) {
            final int temp = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            synchronized (sdf) {
                                String str1 = date[temp];
                                Date date = sdf.parse(str1);
                                String str2 = sdf.format(date);
                                System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2);
                                if(!str1.equals(str2)){
                                    throw new RuntimeException(Thread.currentThread().getName() 
                                            + ", Expected " + str1 + " but got " + str2);
                                }
                            }
                        }
                    } catch (Exception e) {
                        throw new RuntimeException("parse failed", e);
                    }
                }
            }).start();
        }
    }
}

缺点:性能较差,每次都要等待锁释放后其他线程才能进入

2.3 使用ThreadLocal

每个线程都将拥有自己的SimpleDateFormat对象副本。

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>();
 
    public static Date parse(String str) throws Exception {
        SimpleDateFormat sdf = local.get();
        if (sdf == null) {
            sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
            local.set(sdf);
        }
        return sdf.parse(str);
    }
    
    public static String format(Date date) throws Exception {
        SimpleDateFormat sdf = local.get();
        if (sdf == null) {
            sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
            local.set(sdf);
        }
        return sdf.format(date);
    }
}
public class ThreadLocalDateFormatTest {
    private static String date[] = { "01-Jan-1999", "01-Jan-2000", "01-Jan-2001" };
 
    public static void main(String[] args) {
        for (int i = 0; i < date.length; i++) {
            final int temp = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            String str1 = date[temp];
                            Date date = DateUtil.parse(str1);
                            String str2 = DateUtil.format(date);
                            System.out.println(str1 + "," + str2);
                            if(!str1.equals(str2)){
                                throw new RuntimeException(Thread.currentThread().getName() 
                                        + ", Expected " + str1 + " but got " + str2);
                            }
                        }
                    } catch (Exception e) {
                        throw new RuntimeException("parse failed", e);
                    }
                }
            }).start();
        }
    }
}

3.使用DateTimeFormatter代替SimpleDateFormat

代码参考DateTimeFormatterTest.java
jdk1.8中新增了 LocalDate 与 LocalDateTime等类来解决日期处理方法,同时引入了一个新的类DateTimeFormatter来解决日期格式化问题。
LocalDateTime,DateTimeFormatter两个类都没有线程问题,只要你自己不把它们创建为共享变量就没有线程问题。
可以使用Instant代替 Date,LocalDateTime代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
LocalDate date = LocalDate.parse("2017 06 17", formatter);
System.out.println(formatter.format(date));

4.为什么DateTimeFormatter是线程安全?是怎样改进为线程安全的?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,389评论 4 56
  • 今天张老师给我留言说:“孩子在上课的时候不注意听讲,做试卷的时候也玩,一张试卷只做了一道题,而且前一天的英语...
    松鼠家阅读 126评论 0 1
  • “分开了就会想念”,这大概就是爱一个人的样子吧!从分开的一刻,就开始期待一周以后的相见,可有的时候你却没有放假,于...
    梓陌阳春阅读 117评论 0 0
  • 我的记忆中奶奶是孤独的,今天早上醒来突然回想起她的时候,清晰地浮现出背靠在椅子上的枯瘦老人,神情恍惚,看着我的时候...
    双鱼的Antony阅读 460评论 0 49
  • 第六十一章 新任郡守 事不做绝,话不说满,这一点滕庆比谁都懂。留有退路方能保全,滕肃却从来不知道他父亲会在什...
    缺心眼子125阅读 357评论 0 0