Skip to content

SimpleDateFormat线程安全问题

1、重现SimpleDateFormat类的线程安全问题

面试都会说SimpateDateFormat线程不安全,那为什么不安全,为了重现SimpleDateFormat类的线程安全问题,一种比较简单的方式就是使用线程池结合Java并发包中的CountDownLatch类和Semaphore类来重现线程安全问题。

java
package com.batch.controller;

import java.text.SimpleDateFormat;
import java.util.concurrent.*;

/**
 * @Author: zouming
 * @Date: 2024/5/29 0:26
 */
public class SimpleDateFormatDemo {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < EXECUTE_COUNT; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        simpleDateFormat.parse("2024-05-29");
                    } catch (InterruptedException e) {
                        System.err.println("线程被中断:" + Thread.currentThread().getName());
                    } catch (Exception e) {
                        System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("主程序线程被中断");
        } finally {
            executorService.shutdown();
            if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("线程池未能在规定时间内关闭");
            }
            System.out.println("所有线程格式化日期成功");
        }
    }

}

核心方法

java
simpleDateFormat.parse("2024-05-29");

运行结果

java
Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" 线程:pool-1-thread-7 格式化日期失败
线程:pool-1-thread-9 格式化日期失败
线程:pool-1-thread-10 格式化日期失败
Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-6" 线程:pool-1-thread-15 格式化日期失败
线程:pool-1-thread-21 格式化日期失败
Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败
线程:pool-1-thread-11 格式化日期失败
java.lang.ArrayIndexOutOfBoundsException
线程:pool-1-thread-27 格式化日期失败
	at java.lang.System.arraycopy(Native Method)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597)
	at java.lang.StringBuffer.append(StringBuffer.java:367)
	at java.text.DigitList.getLong(DigitList.java:191)线程:pool-1-thread-25 格式化日期失败

	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
线程:pool-1-thread-14 格式化日期失败
	at java.text.DateFormat.parse(DateFormat.java:364)
	at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
线程:pool-1-thread-13 格式化日期失败	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)

	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
线程:pool-1-thread-20 格式化日期失败	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 io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	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:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)

结论

说明,在高并发下使用SimpleDateFormat类格式化日期时抛出了异常,SimpleDateFormat类不是线程安全的!!!

2、SimpleDateFormat类为何不是线程安全的

SimpleDateFormat是继承自DateFormat类,DateFormat类中维护了一个全局的Calendar变量

java
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}.
     * @serial
     
     用于计算日期-时间字段和时间瞬间的 Calendar 实例。此字段用于格式化和解析。
     子类应将此字段初始化为适合与此 DateFormat 关联的区域设置的日历。
     */
    protected Calendar calendar;
 	
    //省略其他代码.....
}

从注释可以看出,这个Calendar对象既用于格式化也用于解析日期时间。SimpleDateFormat 类中 parse()进行格式化

java
    /* 
 分析字符串中的文本以生成 Date。该方法尝试从 pos 给出的索引开始解析文本。如果解析成功,则 pos 的索引将更新为使用的最后一个字符之后的索引(解析不一定使用字符串末尾的所有字符),并返回解析的日期。更新的 pos 可用于指示下次调用此方法的起点。如果发生错误,则不会更改 pos 的索引,将 pos 的错误索引设置为发生错误的字符的索引,并返回 null。此分析操作使用日历生成 Date。在解析之前,将清除日历的所有日期时间字段,并且日历的日期时间字段的默认值将用于任何缺少的日期时间信息。例如,如果解析操作未给出年份值,则解析日期的年份值为 1970 年,而 GregorianCalendar 则为 1970 年。TimeZone 值可能会被覆盖,具体取决于给定的模式和文本中的时区值。可能需要还原之前通过调用 setTimeZone 设置的任何 TimeZone 值才能执行进一步操作。Params: text – 一个字符串,其中的一部分应该被解析。pos – 具有如上所述的索引和错误索引信息的 ParsePosition 对象。返回:从字符串中解析的日期。如果出错,则返回 null。抛出:NullPointerException – 如果 text 或 pos 为 null。
     */
    @Override
    public Date parse(String text, ParsePosition pos)
    {	
        //检查负数表达式
        checkNegativeNumberExpression();

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

        boolean[] ambiguousYear = {false};

        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;
                    int nextCount = compiledPattern[i] & 0xff;
                    obeyCount = shouldObeyCount(nextTag, nextCount);

                    if (hasFollowingMinusSign &&
                        (nextTag == TAG_QUOTE_ASCII_CHAR ||
                         nextTag == TAG_QUOTE_CHARS)) {

                        if (nextTag != TAG_QUOTE_ASCII_CHAR) {
                            nextCount = compiledPattern[i+1];
                        }

                        if (nextCount == 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 {
            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对象。

接下来,我们再来看看CalendarBuilder.establish()方法

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

`
    // 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);
    }
    return cal;
}

在CalendarBuilder.establish()方法中先后调用了cal.clear()与cal.set(),也就是先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。类似地, format()方法也存在同样的问题。

因此, SimpleDateFormat类不是线程安全的根本原因是:DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全。

3、解决SimpleDateFormat类的线程安全问题

1.局部变量法

java
package com.batch.controller;

import java.text.SimpleDateFormat;
import java.util.concurrent.*;

/**
 * @Author: zouming
 * @Date: 2024/5/29 0:26
 */
public class SimpleDateFormatDemo01 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < EXECUTE_COUNT; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
                        simpleDateFormat.parse("2024-05-29");
                    } catch (InterruptedException e) {
                        System.err.println("线程被中断:" + Thread.currentThread().getName());
                    } catch (Exception e) {
                        System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("主程序线程被中断");
        } finally {
            executorService.shutdown();
            if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("线程池未能在规定时间内关闭");
            }
            System.out.println("所有线程格式化日期成功");
        }
    }

}

这种方式在高并发下会创建大量的SimpleDateFormat类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推荐。

2.synchronized锁方式

java
package com.batch.controller;

import java.text.SimpleDateFormat;
import java.util.concurrent.*;

/**
 * @Author: zouming
 * @Date: 2024/5/29 0:26
 */
public class SimpleDateFormatDemo02 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < EXECUTE_COUNT; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        synchronized (simpleDateFormat){
                            simpleDateFormat.parse("2020-01-01");
                        }
                    } catch (InterruptedException e) {
                        System.err.println("线程被中断:" + Thread.currentThread().getName());
                    } catch (Exception e) {
                        System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("主程序线程被中断");
        } finally {
            executorService.shutdown();
            if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("线程池未能在规定时间内关闭");
            }
            System.out.println("所有线程格式化日期成功");
        }
    }

}

核心代码

java
synchronized (simpleDateFormat){
	simpleDateFormat.parse("2020-01-01");
}

虽然这种方式能够解决SimpleDateFormat类的线程安全问题,但是由于在程序的执行过程中,为SimpleDateFormat类对象加上了synchronized锁,导致同一时刻只能有一个线程执行parse(String)方法。此时,会影响程序的执行性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的

3.Lock锁方式

java
package com.batch.controller;

import java.text.SimpleDateFormat;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: zouming
 * @Date: 2024/5/29 0:26
 */
public class SimpleDateFormatDemo03 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    //Lock对象
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < EXECUTE_COUNT; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        lock.lock();
                        simpleDateFormat.parse("2020-01-01");

                    } catch (InterruptedException e) {
                        System.err.println("线程被中断:" + Thread.currentThread().getName());
                    } catch (Exception e) {
                        System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                        countDownLatch.countDown();
                        lock.unlock();
                    }
                });
            }
            countDownLatch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("主程序线程被中断");
        } finally {
            executorService.shutdown();
            if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("线程池未能在规定时间内关闭");
            }
            System.out.println("所有线程格式化日期成功");
        }
    }

}

此种方式同样会影响高并发场景下的性能,不太建议在高并发的生产环境使用。

4.ThreadLocal方式

使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题。

java
package com.batch.controller;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: zouming
 * @Date: 2024/5/29 0:26
 */
public class SimpleDateFormatDemo04 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    //ThreadLocal
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < EXECUTE_COUNT; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        threadLocal.get().parse("2020-01-01");

                    } catch (InterruptedException e) {
                        System.err.println("线程被中断:" + Thread.currentThread().getName());
                    } catch (Exception e) {
                        System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("主程序线程被中断");
        } finally {
            executorService.shutdown();
            if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("线程池未能在规定时间内关闭");
            }
            threadLocal.remove();
            System.out.println("所有线程格式化日期成功");
        }
    }

}

5.DateTimeFormatter方式

DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作

java
package com.batch.controller;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.*;

/**
 * @Author: zouming
 * @Date: 2024/5/29 0:26
 */
public class SimpleDateFormatDemo05 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //DateTimeFormatter
    private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < EXECUTE_COUNT; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        LocalDate.parse("2020-01-01",formatter);

                    } catch (InterruptedException e) {
                        System.err.println("线程被中断:" + Thread.currentThread().getName());
                    } catch (Exception e) {
                        System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("主程序线程被中断");
        } finally {
            executorService.shutdown();
            if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("线程池未能在规定时间内关闭");
            }
            System.out.println("所有线程格式化日期成功");
        }
    }

}

DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作

6.joda-time方式

joda-time是第三方处理日期时间格式化的类库,是线程安全的

xml
<dependency>
	<groupId>joda-time</groupId>
	<artifactId>joda-time</artifactId>
	<version>2.9.9</version>
</dependency>
java
package com.batch.controller;

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.util.concurrent.*;

/**
 * @Author: zouming
 * @Date: 2024/5/29 0:26
 */
public class SimpleDateFormatDemo06 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //DateTimeFormatter
    private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");


    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < EXECUTE_COUNT; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        DateTime.parse("2020-01-01",dateTimeFormatter).toDate();
                    } catch (InterruptedException e) {
                        System.err.println("线程被中断:" + Thread.currentThread().getName());
                    } catch (Exception e) {
                        System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                        countDownLatch.countDown();
                    }
                });
            }
            countDownLatch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("主程序线程被中断");
        } finally {
            executorService.shutdown();
            if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("线程池未能在规定时间内关闭");
            }
            System.out.println("所有线程格式化日期成功");
        }
    }

}

注意是用的是joda中的日期类

import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter;

使用joda-time库来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。

4、总结

解决SimpleDateFormat类的线程安全问题的方案总结

在解决解决SimpleDateFormat类的线程安全问题的几种方案中,局部变量法由于线程每次执行格式化时间时,都会创建SimpleDateFormat类的对象,这会导致创建大量的SimpleDateFormat对象,浪费运行空间和消耗服务器的性能,因为JVM创建和销毁对象是要耗费性能的。所以,不推荐在高并发要求的生产环境使用

synchronized锁方式和Lock锁方式在处理问题的本质上是一致的,通过加锁的方式,使同一时刻只能有一个线程执行格式化日期和时间的操作。这种方式虽然减少了SimpleDateFormat对象的创建,但是由于同步锁的存在,导致性能下降,所以,不推荐在高并发要求的生产环境使用。

ThreadLocal通过保存各个线程的SimpleDateFormat类对象的副本,使每个线程在运行时,各自使用自身绑定的SimpleDateFormat对象,互不干扰,执行性能比较高,推荐在高并发的生产环境使用。

DateTimeFormatter是Java 8中提供的处理日期和时间的类,DateTimeFormatter类本身就是线程安全的推荐在高并发场景下的生产环境使用。

joda-time是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用