Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法

Posted 小小工匠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法相关的知识,希望对你有一定的参考价值。


概述

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

这里来揭示它为何是线程不安全的,以及如何避免该问题。


复现问题


import java.text.ParseException;
import java.text.SimpleDateFormat; 

/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/11/21 14:56
 * @mark: show me the code , change the world
 */
public class SimpleDateFormatTest 

    // 1 创建单例实例
    private 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(() -> 
                try 
                    // 3 使用单例日期解析文本
                    System.out.println(sdf.parse("2021-11-19 15:15:00"));
                 catch (ParseException e) 
                    e.printStackTrace();
                
            );
            thread.start();
        

    

    

代码(1)创建了SimpleDateFormat的一个实例

代码(2)创建10个线程,每个线程都共用同一个sdf对象对文本日期进行解析。

多运行几次代码就会抛出java.lang.NumberFormatException异常,增加线程的个数有利于复现该问题

Exception in thread "Thread-0" Exception in thread "Thread-2" Exception in thread "Thread-1" Exception in thread "Thread-6" Exception in thread "Thread-4" Exception in thread "Thread-8" Exception in thread "Thread-9" Exception in thread "Thread-5" Exception in thread "Thread-7" java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	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 com.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
	at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: 20
	at java.text.DigitList.append(DigitList.java:151)
	at java.text.DecimalFormat.subparse(DecimalFormat.java:2278)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2036)
	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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
	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:2089)
	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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	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 com.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: empty String
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
	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:2089)
	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 com.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: empty String
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
	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:2089)
	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 com.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
	at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: 19
	at java.text.DigitList.append(DigitList.java:151)
	at java.text.DecimalFormat.subparse(DecimalFormat.java:2278)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2036)
	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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
	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:2089)
	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 
	.....


源码分析

为了便于分析,首先来看SimpleDateFormat的类图结构

可以看到,每个SimpleDateFormat实例里面都有一个Calendar对象,后面我们就会知道,SimpleDateFormat之所以是线程不安全的,就是因为Calendar是线程不安全的。

Calendar之所以是线程不安全的,是因为其中存放日期数据的变量都是线程不安全的,比如fields、time等。

下面从代码层面来看下parse方法做了什么事情。

    public Date parse(String source) throws ParseException
    
        ParsePosition pos = new ParsePosition(0);
        Date result = parse(source, pos);
        if (pos.index == 0)
            throw new ParseException("Unparseable date: \\"" + source + "\\"" ,
                pos.errorIndex);
        return result;
    



  @Override
    public Date parse(String text, ParsePosition pos)
    
        .......
		// 1 解析日期字符串,将解析好的数据放入CalendarBuilder对象中
        CalendarBuilder calb = new CalendarBuilder();

      	 .......
      	 .......

		Date parsedDate;
        try 
        	// 2 使用calb中解析好的日期数据设置calendar
            parsedDate = calb.establish(calendar).getTime();
            
         
        catch (IllegalArgumentException e) 
            .......
        	.......
            return null;
        
		 .......
      	 .......
        return parsedDate;
    
  • 代码(1)的主要作用是解析日期字符串并把解析好的数据放入 CalendarBuilder的实例calb中。CalendarBuilder是一个建造者模式,用来存放后面需要的数据。

  • 代码(2)使用calb中解析好的日期数据设置calendar。

calb.establish的代码如下

    Calendar establish(Calendar cal) 
        .....
		// 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;
    
  • 代码(3)重置Calendar对象里面的属性值,如下所示。
  public final void clear()
    
        for (int i 以上是关于Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法的主要内容,如果未能解决你的问题,请参考以下文章

Java review-basic4

Java review-basic2

Java review-basic5

java获取 昨天 今天 明天的日期

Java review-basic6

Java review--hashMap