写点什么

【高并发】SimpleDateFormat 类到底为啥不是线程安全的?(附六种解决方案,建议收藏)

作者:冰河
  • 2021 年 11 月 11 日
  • 本文字数:15403 字

    阅读完需:约 51 分钟

【高并发】SimpleDateFormat类到底为啥不是线程安全的?(附六种解决方案,建议收藏)

大家好,我是冰河~~


首先问下大家:你使用的 SimpleDateFormat 类还安全吗?为什么说 SimpleDateFormat 类不是线程安全的?带着问题从本文中寻求答案。


提起 SimpleDateFormat 类,想必做过 Java 开发的童鞋都不会感到陌生。没错,它就是 Java 中提供的日期时间的转化类。这里,为什么说 SimpleDateFormat 类有线程安全问题呢?有些小伙伴可能会提出疑问:我们生产环境上一直在使用 SimpleDateFormat 类来解析和格式化日期和时间类型的数据,一直都没有问题啊!我的回答是:没错,那是因为你们的系统达不到 SimpleDateFormat 类出现问题的并发量,也就是说你们的系统没啥负载!


接下来,我们就一起看下在高并发下 SimpleDateFormat 类为何会出现安全问题,以及如何解决 SimpleDateFormat 类的安全问题。

重现 SimpleDateFormat 类的线程安全问题

为了重现 SimpleDateFormat 类的线程安全问题,一种比较简单的方式就是使用线程池结合 Java 并发包中的 CountDownLatch 类和 Semaphore 类来重现线程安全问题。


有关 CountDownLatch 类和 Semaphore 类的具体用法和底层原理与源码解析在【高并发专题】后文会深度分析。这里,大家只需要知道 CountDownLatch 类可以使一个线程等待其他线程各自执行完毕后再执行。而 Semaphore 类可以理解为一个计数信号量,必须由获取它的线程释放,经常用来限制访问某些资源的线程数量,例如限流等。


好了,先来看下重现 SimpleDateFormat 类的线程安全问题的代码,如下所示。


package io.binghe.concurrent.lab06;
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;
/** * @author binghe * @version 1.0.0 * @description 测试SimpleDateFormat的线程不安全问题 */public class SimpleDateFormatTest01 { //执行总次数 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(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); }}
复制代码


可以看到,在 SimpleDateFormatTest01 类中,首先定义了两个常量,一个是程序执行的总次数,一个是同时运行的线程数量。程序中结合线程池和 CountDownLatch 类与 Semaphore 类来模拟高并发的业务场景。其中,有关日期转化的代码只有如下一行。


simpleDateFormat.parse("2020-01-01");
复制代码


当程序捕获到异常时,打印相关的信息,并退出整个程序的运行。当程序正确运行后,会打印“所有线程格式化日期成功”。


运行程序输出的结果信息如下所示。


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)
Process finished with exit code 1
复制代码


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


接下来,我们就看下,SimpleDateFormat 类为何不是线程安全的。

SimpleDateFormat 类为何不是线程安全的?

那么,接下来,我们就一起来看看真正引起 SimpleDateFormat 类线程不安全的根本原因。


通过查看 SimpleDateFormat 类的源码,我们得知:SimpleDateFormat 是继承自 DateFormat 类,DateFormat 类中维护了一个全局的 Calendar 变量,如下所示。


/**  * 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;
复制代码


从注释可以看出,这个 Calendar 对象既用于格式化也用于解析日期时间。接下来,我们再查看 parse()方法接近最后的部分。


@Overridepublic Date parse(String text, ParsePosition pos){    ################此处省略N行代码##################    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()方法,如下所示。


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;    }
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; } } }
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 对象本身不支持线程安全。


那么,得知了 SimpleDateFormat 类不是线程安全的,以及造成 SimpleDateFormat 类不是线程安全的原因,那么如何解决这个问题呢?接下来,我们就一起探讨下如何解决 SimpleDateFormat 类在高并发场景下的线程安全问题。

解决 SimpleDateFormat 类的线程安全问题

解决 SimpleDateFormat 类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考,大家也可以在评论区给出更多的解决方案。

1.局部变量法

最简单的一种方式就是将 SimpleDateFormat 类对象定义成局部变量,如下所示的代码,将 SimpleDateFormat 类对象定义在 parse(String)方法的上面,即可解决问题。


package io.binghe.concurrent.lab06;
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;
/** * @author binghe * @version 1.0.0 * @description 局部变量法解决SimpleDateFormat类的线程安全问题 */public class SimpleDateFormatTest02 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20;
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(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); }}
复制代码


此时运行修改后的程序,输出结果如下所示。


所有线程格式化日期成功
复制代码


至于在高并发场景下使用局部变量为何能解决线程的安全问题,会在【JVM 专题】的 JVM 内存模式相关内容中深入剖析,这里不做过多的介绍了。


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

2.synchronized 锁方式

将 SimpleDateFormat 类对象定义成全局静态变量,此时所有线程共享 SimpleDateFormat 类对象,此时在调用格式化时间的方法时,对 SimpleDateFormat 对象进行同步即可,代码如下所示。


package io.binghe.concurrent.lab06;
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;
/** * @author binghe * @version 1.0.0 * @description 通过Synchronized锁解决SimpleDateFormat类的线程安全问题 */public class SimpleDateFormatTest03 { //执行总次数 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(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { synchronized (simpleDateFormat){ simpleDateFormat.parse("2020-01-01"); } } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); }}
复制代码


此时,解决问题的关键代码如下所示。


synchronized (simpleDateFormat){  simpleDateFormat.parse("2020-01-01");}
复制代码


运行程序,输出结果如下所示。


所有线程格式化日期成功
复制代码


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

3.Lock 锁方式

Lock 锁方式与 synchronized 锁方式实现原理相同,都是在高并发下通过 JVM 的锁机制来保证程序的线程安全。通过 Lock 锁方式解决问题的代码如下所示。


package io.binghe.concurrent.lab06;
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;
/** * @author binghe * @version 1.0.0 * @description 通过Lock锁解决SimpleDateFormat类的线程安全问题 */public class SimpleDateFormatTest04 { //执行总次数 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(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { lock.lock(); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }finally { lock.unlock(); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); }}
复制代码


通过代码可以得知,首先,定义了一个 Lock 类型的全局静态变量作为加锁和释放锁的句柄。然后在 simpleDateFormat.parse(String)代码之前通过 lock.lock()加锁。这里需要注意的一点是:为防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到 finally 代码块中,如下所示。


finally {  lock.unlock();}
复制代码


运行程序,输出结果如下所示。


所有线程格式化日期成功
复制代码


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

4.ThreadLocal 方式

使用 ThreadLocal 存储每个线程拥有的 SimpleDateFormat 对象的副本,能够有效的避免多线程造成的线程安全问题,使用 ThreadLocal 解决线程安全问题的代码如下所示。


package io.binghe.concurrent.lab06;
import java.text.DateFormat;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;
/** * @author binghe * @version 1.0.0 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题 */public class SimpleDateFormatTest05 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20;
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(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { threadLocal.get().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); }}
复制代码


通过代码可以得知,将每个线程使用的 SimpleDateFormat 副本保存在 ThreadLocal 中,各个线程在使用时互不干扰,从而解决了线程安全问题。


运行程序,输出结果如下所示。


所有线程格式化日期成功
复制代码


此种方式运行效率比较高,推荐在高并发业务场景的生产环境使用。


另外,使用 ThreadLocal 也可以写成如下形式的代码,效果是一样的。


package io.binghe.concurrent.lab06;
import java.text.DateFormat;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;
/** * @author binghe * @version 1.0.0 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题 */public class SimpleDateFormatTest06 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20;
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
private static DateFormat getDateFormat(){ DateFormat dateFormat = threadLocal.get(); if(dateFormat == null){ dateFormat = new SimpleDateFormat("yyyy-MM-dd"); threadLocal.set(dateFormat); } return dateFormat; }
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(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { getDateFormat().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); }}
复制代码

5.DateTimeFormatter 方式

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


package io.binghe.concurrent.lab06;
import java.time.LocalDate;import java.time.format.DateTimeFormatter;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;
/** * @author binghe * @version 1.0.0 * @description 通过DateTimeFormatter类解决线程安全问题 */public class SimpleDateFormatTest07 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20;
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(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { LocalDate.parse("2020-01-01", formatter); }catch (Exception e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); }}
复制代码


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


运行程序,输出结果如下所示。


所有线程格式化日期成功
复制代码


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

6.joda-time 方式

joda-time 是第三方处理日期时间格式化的类库,是线程安全的。如果使用 joda-time 来处理日期和时间的格式化,则需要引入第三方类库。这里,以 Maven 为例,如下所示引入 joda-time 库。


<dependency>  <groupId>joda-time</groupId>  <artifactId>joda-time</artifactId>  <version>2.9.9</version></dependency>
复制代码


引入 joda-time 库后,实现的程序代码如下所示。


package io.binghe.concurrent.lab06;
import org.joda.time.DateTime;import org.joda.time.format.DateTimeFormat;import org.joda.time.format.DateTimeFormatter;
import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;
/** * @author binghe * @version 1.0.0 * @description 通过DateTimeFormatter类解决线程安全问题 */public class SimpleDateFormatTest08 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20;
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(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { DateTime.parse("2020-01-01", dateTimeFormatter).toDate(); }catch (Exception e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); }}
复制代码


这里,需要注意的是:DateTime 类是 org.joda.time 包下的类,DateTimeFormat 类和 DateTimeFormatter 类都是 org.joda.time.format 包下的类,如下所示。


import org.joda.time.DateTime;import org.joda.time.format.DateTimeFormat;import org.joda.time.format.DateTimeFormatter;
复制代码


运行程序,输出结果如下所示。


所有线程格式化日期成功
复制代码


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

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

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


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


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


DateTimeFormatter 是 Java 8 中提供的处理日期和时间的类,DateTimeFormatter 类本身就是线程安全的,经压测,DateTimeFormatter 类处理日期和时间的性能效果还不错(后文单独写一篇关于高并发下性能压测的文章)。所以,推荐在高并发场景下的生产环境使用。


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


好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,我是冰河,我们下期见~~

发布于: 2021 年 11 月 11 日阅读数: 9
用户头像

冰河

关注

公众号:冰河技术 2020.05.29 加入

互联网高级技术专家,《深入理解分布式事务:原理与实战》,《海量数据处理与大数据技术实战》和《MySQL技术大全:开发、优化与运维实战》作者,mykit-data与mykit-transaction-message框架作者。【冰河技术】作者。

评论

发布
暂无评论
【高并发】SimpleDateFormat类到底为啥不是线程安全的?(附六种解决方案,建议收藏)