首页 > 资讯 > > 正文

世界新动态:Simple Date Format类到底为啥不是线程安全的?

来源:博客园 2023-06-05 13:24:53
摘要:我们就一起看下在高并发下Simple Date Format类为何会出现安全问题,以及如何解决Simple Date Format类的安全问题。

本文分享自华为云社区《【高并发】SimpleDateFormat类到底为啥不是线程安全的?》,作者:冰 河。

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


【资料图】

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

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

重现 Simple Date Format类的线程安全问题

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

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

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

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类中,首先定义了两个常量,一个是程序执行的总次数,一个是同时运行的线程数量。程序中结合线程池和Count Down Latch类与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

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

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

Simple Date Format 类为何不是线程安全的?

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

通过查看Simple Date Format类的源码,我们得知:Simple Date Format是继承自Date Format类,Date Format类中维护了一个全局的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.  *  * 

Subclasses should initialize this field to a {@link Calendar} * appropriate for the {@link Locale} associated with this * DateFormat. * @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 threadLocal = new ThreadLocal(){ @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 threadLocal = new ThreadLocal(); 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库。

joda-timejoda-time2.9.9

引入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是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用。

点击关注,第一时间了解华为云新鲜技术~

x
推荐阅读

世界新动态:Simple Date Format类到底为啥不是线程安全的?

2023-06-05 13:24:53

天天讯息:猪肉概念持续下挫 华统股份等跌超4%

2023-06-05 12:42:04

【就业分析】硕士毕业后如果考选调,考研时应该如何择校?

2023-06-05 11:43:39

变压器容量与功率换算(变压器容量与功率的关系) 报道

2023-06-05 10:56:28

官方预热:摩托罗拉 Razr 40 系列折叠屏手机即将登陆印度

2023-06-05 10:22:41

什么是世界史专业属于学科|全球实时

2023-06-05 09:32:20

全球今亮点!清明上河图高清细节图(清明上河图高清)

2023-06-05 09:00:06

card是什么意思_cake是什么意思

2023-06-05 07:58:10

重大调整,周一生效!4000亿资金提前行动? 观热点

2023-06-05 06:45:31

焦点信息:宋燮(关于宋燮介绍)

2023-06-05 05:12:20
相关新闻

世界新动态:Simple Date Format类到底为啥不是线程安全的?

2023-06-05 13:24:53

天天讯息:猪肉概念持续下挫 华统股份等跌超4%

2023-06-05 12:42:04

【就业分析】硕士毕业后如果考选调,考研时应该如何择校?

2023-06-05 11:43:39

变压器容量与功率换算(变压器容量与功率的关系) 报道

2023-06-05 10:56:28

官方预热:摩托罗拉 Razr 40 系列折叠屏手机即将登陆印度

2023-06-05 10:22:41

什么是世界史专业属于学科|全球实时

2023-06-05 09:32:20

全球今亮点!清明上河图高清细节图(清明上河图高清)

2023-06-05 09:00:06

card是什么意思_cake是什么意思

2023-06-05 07:58:10

重大调整,周一生效!4000亿资金提前行动? 观热点

2023-06-05 06:45:31

焦点信息:宋燮(关于宋燮介绍)

2023-06-05 05:12:20

天天热讯:马云持有阿里巴巴多少股份_马云阿里巴巴股份占多少

2023-06-05 02:49:05

天天快资讯丨泰山海拔高度是多少米高_泰山海拔

2023-06-05 00:37:25

小考试题及答案大全_小考试题_环球看热讯

2023-06-04 22:41:42

什么花在晚上十一点开(晚上十一点开的花) 最新

2023-06-04 21:25:42

windows无法启动print spooler服务1068(windows无法启动print spooler服务)

2023-06-04 20:18:04

制造费用会计(制造费用会计分录)

2023-06-04 19:13:52

乘联会崔东树:中国新能源车出口要建设好欧洲市场的口碑和信誉 减少内耗

2023-06-04 18:16:19

刂字旁的字有哪些字字旁的字(饣字旁的字 饣字旁的字有哪些简介介绍)

2023-06-04 17:21:37

力王监狱之力王完整版国语版_力王之监狱力王国语

2023-06-04 16:09:20

天天看点:飞盘衰落,“小众网红运动”为何难长红?

2023-06-04 15:13:03

分类汇总的操作步骤包括_分类汇总的操作步骤_全球聚焦

2023-06-04 14:18:03

祝福祖国的一句话语_祝福祖国的一句话 焦点速讯

2023-06-04 13:47:17

今日播报!代写遗嘱的法律要件有何种

2023-06-04 12:20:46

feed流投放(feed 流)_当前播报

2023-06-04 11:09:47

巴拉克百科_巴拉克定律

2023-06-04 10:36:37

教师教学创新大赛【一等奖】创新成果报告分享(干货6篇) 天天微头条

2023-06-04 09:46:44

黄震怀揣着对事业的热爱,带领团队不断探索,勇攀技术新高峰 【奋斗者正青春】逐梦星河的航天青年

2023-06-04 08:56:00

国内最先进高铁隧道数智化管片开启试生产

2023-06-04 07:59:44

【独家】WWDC23:苹果可能宣布对“Siri”虚拟助手的重大更改

2023-06-04 06:15:25

每日快看:小孩子早餐吃什么好比较有营养的

2023-06-04 04:57:16

三年级除法竖式练习题百度_三年级除法竖式练习题

2023-06-04 03:28:36

JKL第一视角自己交死亡双招被秒:表情呆滞 猛摸鼻子

2023-06-04 01:34:37

头条焦点:剪映怎么放大镜头特写加回放(剪映怎么放大镜头特写)

2023-06-03 23:43:10

每日体育报:利雅得新月打算6月6日宣布签下梅西 即时看

2023-06-03 21:59:34

别把超额业绩报酬建立在投资者痛苦基础上|全球今热点

2023-06-03 21:08:06

今日播报!大众带t发动机_大众teramont什么发动机

2023-06-03 19:56:07

G7易流助力河北天联实业,财运通提升数字化货运管理能力

2023-06-03 18:51:57

焦点滚动:中日防长在香格里拉对话会期间举行双边会晤

2023-06-03 17:59:14

天天消息!华丰HUAFENG品牌介绍_华丰电子元件

2023-06-03 17:00:07

观热点:00后女生,爆肝10天,盖出武侠小说中9大神仙岛

2023-06-03 16:17:35

环球热头条丨优酷下载视频怎么转换格式_优酷下载视频怎么转换mp4

2023-06-03 15:53:37

我的世界启动器账号和密码免费送 我的世界启动器账号

2023-06-03 14:55:57

环球关注:tf家族成员资料简介12人_TF家族的成员名单

2023-06-03 14:10:57

安吉红星美凯龙世博家居广场_世界今头条

2023-06-03 12:45:57

重磅!7月1日起高明买房可入户!入户攻略|百事通

2023-06-03 11:47:47

天天热资讯!传真号码大全山东_传真号码大全

2023-06-03 11:18:47

原神穿越综漫_求综漫小说 100万字以上主角穿越的世界要多_全球聚焦

2023-06-03 10:43:08

迪士尼乐园有望落户武汉?官方:正在洽谈-全球热讯

2023-06-03 09:25:26

羚锐制药:6月2日融资买入656.78万元,融资融券余额1.72亿元

2023-06-03 08:48:12

大学时代小说(大学时代)_天天简讯

2023-06-03 07:57:56

“灵长类基因组计划”取得重大进展 公布27种灵长类动物基因组数据

2023-06-03 06:53:37

理想L7 Air版:1.5T增程动力+449马力!可以入手吗?

2023-06-03 05:49:58

中华博物书法在线查询_中华博物书法字典 观察

2023-06-03 04:29:04

头条焦点:织金县发布特别严重暴雨警告

2023-06-03 03:31:44

吉力在一起读什么_吉力念什么

2023-06-03 03:27:38

Meta抢发新头显,“截胡”苹果新VR

2023-06-03 03:02:37

暴走大事件第三季在线观看(暴走大事件为什么封了) 简讯

2023-06-03 01:39:43

【全球报资讯】最小“画家”只有三岁!武汉一所大学美术馆举办“童画展”

2023-06-03 01:37:23

微头条丨心较比干多一窍下一句_心较比干多一窍病如西子胜三分

2023-06-03 00:09:58

智媒中心建设项目排版胶印厂房、轮转车间改造装修及智媒中心加建工程PVC塑胶地板采购比选结果公示 最资讯

2023-06-02 23:14:25

天天看热讯:中国科技馆周一闭馆吗?

2023-06-02 22:47:04

鹤壁市鹤山区妇联:手牵手探秘科学 心连心共度佳节-快播报

2023-06-02 22:06:26

3300秒!我国载人登月火箭主力发动机再创新纪录

2023-06-02 21:39:46

中国人民银行行长易纲会见阿根廷经济部长马萨与阿根廷央行行长佩塞一行

2023-06-02 20:34:04

6月2日国内DOTP企业报价下跌

2023-06-02 18:59:02

【世界播资讯】心手相牵 快乐成长——四平中院第二党支部开展“六一”儿童节主题党日活动

2023-06-02 18:50:40

赤龙牙野菜多少钱一斤 赤龙牙 快资讯

2023-06-02 17:16:31

滚动:得陇望蜀的意思_得陇望蜀的意思

2023-06-02 16:53:14

领益智造(002600)6月2日主力资金净卖出5475.28万元 天天快看

2023-06-02 16:14:25

天天视讯!一张图:2023/06/02黄金原油外汇股指"枢纽点+多空占比"一览

2023-06-02 15:27:44

中青旅举办“六一”研学游 助推古北水镇文旅融合|环球实时

2023-06-02 14:50:03

尤菲如月与你有约游戏下载 尤菲如月-快报

2023-06-02 13:15:01

工商银行筑梦中华金条100克价格今天多少一克(2023年06月02日)

2023-06-02 12:59:53

一男子叫嚣“我爸是人大代表,我妈是政协委员”,绍兴警方通报

2023-06-02 12:55:40

万家家瑞债券A基金经理变动:增聘周慧为基金经理

2023-06-02 12:34:56

全面推进依法治国学习心得_如何推进全面依法治国-全球热讯

2023-06-02 11:07:11

送给追“锋”少年的节日礼物_观点

2023-06-02 10:24:55

精灵梦叶罗丽:黑化的封银沙可以被原谅吗?我看未必

2023-06-02 09:37:46

董源善画文言文翻译字词解释 董源善画原文及翻译 当前速看

2023-06-02 08:36:12

树脂瓦尺寸大全 合成树脂瓦规格尺寸

2023-06-02 07:44:06

雅马哈踏板摩托车怎么启动打火的(雅马哈踏板车有脚启动吗)

2023-06-02 07:00:22

队记:目前八村塁的的市场价值在每年1500万到1800万之间

2023-06-02 06:34:03

热点评!股指 阶段性修复窗口来临

2023-06-02 05:17:41

世界滚动:太龙股份:5月31日公司高管苏芳减持公司股份合计75.22万股

2023-06-02 05:03:04

“金色童年 放飞梦想”十里锦城社区安全乐学成长驿站六一儿童节活动

2023-06-02 04:02:06

当前信息:迈凯轮的电动超级跑车将专注于轻量化

2023-06-02 03:15:35

开江县任市镇新街中心小学开展“童心向党,快乐成长”庆“六一”活动 当前简讯

2023-06-02 03:08:50

逼近深国资!粤民投“悄然”增持中国宝安|全球快资讯

2023-06-02 01:18:46

阴吹是怎么引起的_阴吹是怎么造成的

2023-06-02 01:03:07

北京十一学校丰台小学“六一”活动举行

2023-06-01 23:58:33

锤子是什么意思四川话_锤子是什么|动态

2023-06-01 22:53:24

金鹰睿选成长六个月持有期混合二度延长募集期

2023-06-01 22:14:24

每日关注!互联网ETF大涨4.41%,中药ETF全线回调;6月权益新基金将密集发行

2023-06-01 21:39:29

没有健康告知的百万医疗险有哪些?怎么买划算?

2023-06-01 21:31:15

即时焦点:当幸福来敲门百度云_开门大吉之四朵金花来敲门

2023-06-01 19:54:49

信用卡停息挂账怎么协商?银行说的没有停息挂账业务? 世界简讯

2023-06-01 18:58:12

欧派家居:已累计回购公司股份142.96万股 总金额为1.67亿元|环球讯息

2023-06-01 18:50:06

每日消息!星河战队灭绝电浆虫有什么特点

2023-06-01 17:38:46

港姐郭羡妮:“倒贴”3套房下嫁河南农村,婚后被丈夫宠成宝

2023-06-01 16:55:54

世界速看:酒水激战“618”:砸钱即盈利的时代过去了?

2023-06-01 16:07:54