SimpleDateFormat线程安全问题

x33g5p2x  于2021-12-06 转载在 其他  
字(12.5k)|赞(0)|评价(0)|浏览(411)

简述SimpleDateFormat

在工作中,我们经常需要将日期在StringDate之间做转化,此时需要使用SimpleDateFormat类。
使用SimpleDateFormat类的parse方法,可以将满足格式要求的字符串转换成Date对象
使用SimpleDateFormat类的format方法,可以将Date类型的对象转换成一定格式的字符串
但是有一点需要特别注意,SimpleDateFormat并非是线程安全的,也就是说在并发环境下,如果考虑不周使用SimpleDateFormat方法可以会出现线程安全方面的问题

线程不安全问题示例

下面的代码都是使用同一个SimpleDateFormat对象

format单线程下调用

代码演示:

public class SimpleDateFormatTest {
    public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) {
        // 1999-12-17 00:00:00 2020-01-01 00:00:00 2021-11-05 23:35:55 2022-07-09 11:11:11
        Date date1 = new Date(2014-1900, 6-1, 12);  // 对应日期时间:2014-06-12 00:00:00
        Date date2 = new Date(1999-1900, 12-1, 17);  // 对应日期时间:1999-12-17 00:00:00
        Date date3 = new Date(2021-1900, 1-1, 1);  // 对应日期时间:2021-01-01 00:00:00
        Date date4 = new Date(2022-1900, 7-1, 9);  // 对应日期时间:2022-07-09 00:00:00
        System.out.println(sdf.format(date1));
        System.out.println(sdf.format(date2));
        System.out.println(sdf.format(date3));
        System.out.println(sdf.format(date4));
    }
}

输出结果:

2014-06-12 00:00:00
1999-12-17 00:00:00
2021-01-01 00:00:00
2022-07-09 00:00:00

Process finished with exit code 0

可以看出单线程情况下是没问题的,接下来我们演示多线程下调用format方法

format多线程下调用

代码演示:

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

public class SimpleDateFormatTest {
    public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) {
        // 1999-12-17 00:00:00 2020-01-01 00:00:00 2021-11-05 23:35:55 2022-07-09 11:11:11
        Date date1 = new Date(2014-1900, 6-1, 12);  // 对应日期时间:2014-06-12 00:00:00
        Date date2 = new Date(1999-1900, 12-1, 17);  // 对应日期时间:1999-12-17 00:00:00
        Date date3 = new Date(2021-1900, 1-1, 1);  // 对应日期时间:2021-01-01 00:00:00
        Date date4 = new Date(2022-1900, 7-1, 9);  // 对应日期时间:2022-07-09 00:00:00
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " - " + sdf.format(date1));
        });
        Thread t2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " - " + sdf.format(date2));
        });
        Thread t3 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " - " + sdf.format(date3));
        });
        Thread t4 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " - " + sdf.format(date4));
        });
        t1.setName("t1"); t2.setName("t2"); t3.setName("t3"); t4.setName("t4");
        t1.start(); t2.start(); t3.start(); t4.start();
    }
}

输出结果:

t2 - 1999-06-12 00:00:00
t1 - 2014-06-12 00:00:00
t3 - 1999-06-12 00:00:00
t4 - 1999-06-12 00:00:00

Process finished with exit code 0

首先说明一下,这个结果在多线程情况下是不确定的
从演示结果我们其实已经看出,输出的结果是不正确的。我们发现t2、t3、t4线程他们对应的时间都是1999-06-12,很明显数据已经错乱了,出现了线程不安全现象

parse单线程下调用

代码演示:

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

public class SimpleDateFormatTest {
    public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) throws ParseException {
        String time1 = "1999-12-17 00:00:00";
        String time2 = "2020-01-01 00:00:00";
        String time3 = "2021-11-05 23:35:55";
        String time4 = "2022-07-09 11:11:11";
        System.out.println(sdf.parse(time1));
        System.out.println(sdf.parse(time2));
        System.out.println(sdf.parse(time3));
        System.out.println(sdf.parse(time4));
    }
}

输出结果:

Fri Dec 17 00:00:00 CST 1999
Wed Jan 01 00:00:00 CST 2020
Fri Nov 05 23:35:55 CST 2021
Sat Jul 09 11:11:11 CST 2022

Process finished with exit code 0

输出结果没有任何问题!

parse多线程下调用

代码演示:

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

public class SimpleDateFormatTest {
    public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) throws ParseException {
        String time1 = "1999-12-17 00:00:00";
        String time2 = "2020-01-01 00:00:00";
        String time3 = "2021-11-05 23:35:55";
        String time4 = "2022-07-09 11:11:11";
        Thread t1 = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName()+" - "+sdf.parse(time1));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName()+" - "+sdf.parse(time2));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        });
        Thread t3 = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName()+" - "+sdf.parse(time3));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        });
        Thread t4 = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName()+" - "+sdf.parse(time4));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        });
        t1.setName("t1"); t2.setName("t2"); t3.setName("t3"); t4.setName("t4");
        t1.start(); t2.start(); t3.start(); t4.start();
    }
}

演示结果:

Exception in thread "t4" Exception in thread "t2" Exception in thread "t3" Exception in thread "t1" 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.coderzpw.threadlocal.SimpleDateFormatTest.lambda$main$2(SimpleDateFormatTest.java:29)
	at java.lang.Thread.run(Thread.java:748)

运行了两次就直接报错了!!!!(#^.^#), (是我想看到的结果,哈哈)

线程不安全问题的原因

SimpleDateFormat 与 DateFormat

首先SimpleDateFormat类 继承于 DateFormat类

public class SimpleDateFormat extends DateFormat {

    // the official serial version ID which says cryptically
    // which version we're compatible with
    static final long serialVersionUID = 4774881970558875024L;

    // the internal serial version which says which version was written
    // - 0 (default) for version up to JDK 1.1.3
    // - 1 for version from JDK 1.1.4, which includes a new field
    static final int currentSerialVersion = 1;
    。。。。

DateFormat类中存在protected属性Calendar成员变量,很明显是让子类继承的

public abstract class DateFormat extends Format {

    /** * The {@link Calendar} instance used for calculating the date-time fields * and the instant of time. This field is used for both formatting and * parsing. * * <p>Subclasses should initialize this field to a {@link Calendar} * appropriate for the {@link Locale} associated with this * <code>DateFormat</code>. * @serial */
    protected Calendar calendar;

通过上面的代码可以看出:SimpleDateFormat继承了DateFormat,在DateFormat中定义了一个protected属性的 Calendar类的对象:calendar。
在SimpleDateFormat转换日期实际是通过Calendar对象来操作的,且calendar这个成员变量既被用于format方法也被用于parse方法

format方法线程不安全原因

让我们点进源码找到format的核心代码

// Called from Format after creating a FieldDelegate
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属于simpleDateFormat对象的一个成员变量,因为simpleDateFormat是被多个线程共享,因此calendar也是被多个线程共享的。大家都知道如果多个线程对同一个对象进行修改操作是会出现线程安全问题的。

场景演示:

  • t1线程执行:调用format方法,执行了calendar.setTime(date),将calendar的time设置为1999-01-01
  • 切换线程,系统上下文切换
  • t2线程执行:调用format方法,执行了calendar.setTime(date),将calendar的time设置为2022-01-01
  • 又切换线程,系统上下文切换
  • t1线程又回来了:此时,calendar的time属性已然不是它所设的值,而是t2线程设置的值:2022-01-01,因此数据就不对应了

如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。

parse方法线程不安全原因

让我们点进源码找到parse的核心代码,这里parse方法源码太长了,我就挑主要的代码粘上

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

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

    boolean[] ambiguousYear = {false};

    。。。。
    Date parsedDate;
        try {
            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();
                }
            }
        }
	。。。。

        return parsedDate;
    }

其中操作calendar的代码是calb.establish(calendar).getTime();
让我们再点进establish这个方法的源码一探究竟

Calendar establish(Calendar cal) {
	。。。

    cal.clear();
    // 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;
            }
        }
    }
    。。。
    return cal;
}

其实关键的一段代码是cal.clear();,cal就是传过来的calendar对象,cal.clear();让calendar这个对象内容清空,同样是对这个共享对象进行修改操作,因此也会出现线程安全问题。

场景演示:

  • t1线程执行:已执行过cal.clear(),然后为清空的calendar设置新值
  • 切换线程,系统上下文切换
  • t2线程执行:刚执行完cal.clear()还未设置值
  • 又切换线程,系统上下文切换
  • t1线程又回来了:此时,calendar对象里面的数据已经被清空了,再执行后面的代码操作calendar就会报错了

解决方案

后面我们会自定义一个日期类来封装format、parse这两个方法

1、每次需要都重新创建一个新对象

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

public class DateUtil {

    /** * Date 转成 String * @param date * @return */
    public static String format(Date date) {
        // 每次调用该方法都会 new一个新的SimpleDateFormat
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }

    /** * String 转成 Date * @param strDate * @return * @throws ParseException */
    public static Date parse(String strDate) throws ParseException {
        // 每次调用该方法都会 new一个新的SimpleDateFormat
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(strDate);
    }
}

这种方式确实可以保证线程的安全,因为在每次调用format或者parse方法时,都会创建一个新的SimpleDateFormat对象。就算是多线程并发情况下,他们操作的也不是同一个SimpleDateFormat对象。

2、操作SimpleDateFormat对象时加锁

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

public class DateUtil {

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

    /** * Date 转成 String * @param date * @return */
    public static String format(Date date) {
        // 每次调用该方法时都锁住sdf这个对象
        synchronized (sdf) {
            return sdf.format(date);
        }
    }

    /** * String 转成 Date * @param strDate * @return * @throws ParseException */
    public static Date parse(String strDate) throws ParseException {
        // 每次调用该方法时都锁住sdf这个对象
        synchronized (sdf) {
            return sdf.parse(strDate);
        }
    }
}

在每次调用format或者parse方法时,都会将sdf这个对象锁住,其他线程想要使用这个对象就会被阻塞住,只能等待,直到使用sdf的线程释放掉这个sdf对象。

3、创建一个ThreadLocal对象使SimpleDateFormat线程私有

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

public class DateUtil {

    // ThreadLocal对象是线程私有的
    public static ThreadLocal<SimpleDateFormat> sdfThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    /** * Date 转成 String * @param date * @return */
    public static String format(Date date) {
        // 获取当前线程私有的sdf对象
        SimpleDateFormat sdf = sdfThreadLocal.get();
        return sdf.format(date);
    }

    /** * String 转成 Date * @param strDate * @return * @throws ParseException */
    public static Date parse(String strDate) throws ParseException {
        // 获取当前线程私有的sdf对象
        SimpleDateFormat sdf = sdfThreadLocal.get();
        return sdf.parse(strDate);
    }
}

这里我们创建的是一个ThreadLocal<SimpleDateFormat>类型的对象,ThreadLocal是Java中线程私有的一个类。不了解ThreadLocal的可以看一下我这篇文章【ThreadLocal的简单使用和原理】

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

完善版的DateFormatUtil类

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

/** * @author coderzpw * 线程安全的日期格式化工具类 */
public class DateFormatUtil {

    // 创建一个静态的 ThreadLocal的 pattern:dateFormat 的map映射 (因为ThreadLocal使得dateFormatMap线程独享,因此dateFormatMap是不会有线程问题的)
    public static ThreadLocal<Map<String, SimpleDateFormat>> dateFormatMap = new ThreadLocal<Map<String, SimpleDateFormat>>(){
        @Override
        protected Map<String, SimpleDateFormat> initialValue() {
            return new HashMap<String,SimpleDateFormat>();
        }
    };

    /** * 根据日期格式获取对应的SimpleDateFormat对象 * @param pattern * @return */
    private static SimpleDateFormat getSimpleDateFormat(final String pattern) {
        // 获取ThreadLocal在当前线程初始化的 HashMap<String,SimpleDateFormat>
        Map<String, SimpleDateFormat> map = dateFormatMap.get();
        SimpleDateFormat sdf = map.get(pattern);
        // 如果对应的sdf为null才会去创建一个新的sdf, 这样做确保只创建一次,既当前线程下是单例的
        if (sdf == null) {
            sdf = new SimpleDateFormat(pattern);
            map.put(pattern, sdf);
        }
        return sdf;
    }

    /** * 封装format方法 Date 转 String * @param date * @param pattern * @return */
    public static String format(Date date, String pattern) {
        return getSimpleDateFormat(pattern).format(date);
    }

    /** * 封装format方法 Long 转 String * @param longTime * @param pattern * @return */
    public static String format(long longTime, String pattern) {
        return getSimpleDateFormat(pattern).format(longTime);
    }

    /** * 封装parse方法 String 转 Date * @param strDate * @param pattern * @return */
    public static Date parse(String strDate, String pattern) {
        Date date = null;
        try {
            date = getSimpleDateFormat(pattern).parse(strDate);
        } catch (ParseException e) {
            System.out.println("时间格式不符合要求,无法解析!");
            e.printStackTrace();
        }
        return date;
    }
}

如果有什么问题的话可以在评论区评论,我看到后会第一时间回复!如果这篇文章对你有帮助的话,还希望你点个赞支持一下,谢谢!

相关文章