SimpleDateFormatNumber FormatException 格式化错误-2022版保姆级Idea调试jdk源码

源码,发现 SimpleDateFormatNumber 不是线程安全的类。

 
  1. * Date formats are not synchronized.
  2. * It is recommended to create separate format instances for each thread.
  3. * If multiple threads access a format concurrently, it must be synchronized
  4. * externally.
 

因为,SimpleDateFormat继承了DateFormat,在DateFormat中定义了一个protected属性的 Calendar类的对象:calendar。只是因为Calendar累的概念复杂,牵扯到时区与本地化等等,Jdk的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。

 

这样写,在高并发的情况下会出现  java.lang.NumberFormatException: For input string: ""

 
  1. public class DateUtil {
  2.  
  3. private DateUtil(){}
  4.  
  5. private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  6.  
  7. public static Date parse(String date) throws ParseException {
  8. return DATE_FORMAT.parse(date);
  9. }
  10.  
  11. }
 

改成:

 
  1. public class DateUtil {
  2.  
  3. private DateUtil(){}
  4.  
  5. public static Date parse(String date) throws ParseException {
  6. return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(date);
  7. }
  8.  
  9. }
 

或者: 

 
  1. public class DateUtil {
  2.  
  3. private DateUtil(){}
  4.  
  5. private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  6.  
  7. public static Date parse(String date) throws ParseException {
  8. synchronized(DATE_FORMAT){
  9. return DATE_FORMAT.parse(date);
  10. }
  11. }
  12.  
  13. }
 

 

 

附JDK1.8时间获取

 
  1. //JDK1.8时间获取当前时间
  2. String now = LocalDateTime.now().withNano(0).toString().replace("T", " ");
  3. String now = LocalDate.now()+" "+LocalTime.now().withNano(0).toString();
     

    Idea导入jdk1.8源码

    作为一名在职场混迹多年的老菜鸟,奉劝各位学子,学习一定要趁早。既然知道未来是一定要做的事情,那倒不如现在就做。例如阅读jdk源码,作为一名Java开发工程师,如果想要提高自己的技术,阅读源码这个过程是必不可少的。

    言归正传,本文章主要分为三个部分,第一部分创建项目;第二部分导入源码;第三部分调试源码以及代码跳转;

    创建项目

    • 打开Idea->File->New->Project

      创建好的项目结构如下图:
      在这里插入图片描述

    导入源码

    首先找到源码位置,我们在安装JDK的时候,在安装的目录下面有源码,名字为src.zip。
    mac下的路径为:

    	/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/src.zip
    • 将src.zip复制到刚才创建的JDKSource项目下,复制后的路径为:

      /Users/wuang/IdeaProjects/JDKSource/src/src.zip
    • 在项目的src目录下创建一个名字为source的目录,此目录用来存放src.zip解压后的内容
      在这里插入图片描述

    • 将src.zip解压到source目录下,路径结构如下:
      在这里插入图片描述

    • 此时打开Idea的项目,可以看到项目结构如下:
      在这里插入图片描述

    配置Idea

    将源码导入项目之后,还需要对Idea进行下配置;步骤如下:

    • 选中项目右击鼠标
      在这里插入图片描述

    • 点击Open Module Settings,在SDKs中创建一个自己的jdk,并将刚才解压的源码添加到创建的jdk 中。步骤如下
      在这里插入图片描述

    • 配置项目引用刚才创建的jdk
      在这里插入图片描述
      至此Idea配置已经完成,你就可以打开一个类,例如HaspMap.java。看下它的路径如果是你自己的创建的source下的,那就说明你的步骤是正确的。
      在这里插入图片描述

    调试源码

    如果以上步骤都正确的话,调试源码这块就比较简单了。

    • 首先设置下Idea,允许断点进入classes 。在图中取消勾选java.*和javax.*。
      在这里插入图片描述
    • 在你需要打断点的地方打上断点,测试一下是否可以进来,
      在这里插入图片描述

    如果出现下面这个错误
    在这里插入图片描述
    可以在按照下面的步骤设置
    在这里插入图片描述

    整个过程到此就结束了,天空任鸟飞,海阔凭鱼跃,骚年开启你的源码之旅吧。

    请别再使用 SimpleDateFormat 格式化时间了,DateTimeFormatter 更出色!

    DateTimeFormatter类

    我们先来看看SImpleDateFormat类的部分源码,如图1所示。

    图片
    图1

    接着再来看看DateTimeFormatter类的部分源码,如 图2所示。

    图片
    图2

    由上可知,与SimpleDateFormat不同的是,DateTimeFormatter不但是不变对象,它还是线程安全的。线程的概念我们会在后面涉及到。

    现在我们只需要记住:因为SimpleDateFormat不是线程安全的,使用的时候,只能在方法内部创建新的局部变量。而DateTimeFormatter可以只创建一个实例,到处引用。

    接下来,我们来说一说DateTimeFormatter类的常用方法

    //创建一个格式化程序使用指定的模式
    static DateTimeFormatter ofPattern(String pattern) 
      
    //创建一个格式化程序使用指定的模式和现场。
    static DateTimeFormatter ofPattern(String pattern, Locale locale) 
     
    //使用此格式化程序格式的日期时间对象
    String format(TemporalAccessor temporal) 

    其中,TemporalAccessor是一个接口,其实现类有LocalDate、LocalTime、LocalDateTime、ZonedDateTime等……

    所以我们在使用format方法时,一般传入其实现类的实例化对象即可。

    接下来我们举几个例子。

    范例1:创建DateTimeFormatter

    package edu.blog.test07;
    
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    
    public class DateTimeFormatterTestDemo01 {
        public static void main(String[] args) {
            //自定义输出格式
            DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
            System.out.println(dtf.format(LocalDateTime.now()));
            System.out.println("===================================");
            //自定义格式解析
            LocalDateTime localDateTime = LocalDateTime.parse("2001/07/27 22:22:22", dtf);
            System.out.println(localDateTime);
        }
    }
    
    /*
    结果:
    2021/04/02 23:14:46
    ===================================
    2001-07-27T22:22:22
    */

    由上可知,DateTimeFormatter类格式化字符串的使用方式与SImpleDateFormat一样。

    此外,另一种创建DateTimeFormatter的方法是,传入格式化字符串的同时,同时指定Locale。

    范例2:按照Locale默认习惯格式化

    package edu.blog.test07;
    
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.Locale;
    
    public class DateTimeFormatterTestDemo02 {
        public static void main(String[] args) {
            ZonedDateTime zonedDateTime = ZonedDateTime.now();
            System.out.println(zonedDateTime);
            System.out.println("==============================");
    
            DateTimeFormatter formatter01 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ZZZZ");
            System.out.println(formatter01.format(zonedDateTime));
            System.out.println("==============================");
    
            DateTimeFormatter formatter02 = DateTimeFormatter.ofPattern("yyyy MMM dd EE:HH:mm", Locale.CHINA);
            System.out.println(formatter02.format(zonedDateTime));
            System.out.println("==============================");
    
            DateTimeFormatter formatter03 = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
            System.out.println(formatter03.format(zonedDateTime));
        }
    }
    
    /*
    结果:
    2021-04-02T23:27:59.326+08:00[Asia/Shanghai]
    ==============================
    2021-04-02T23:27:GMT+08:00
    ==============================
    2021 四月 02 星期五:23:27
    ==============================
    Fri, April/02/2021 23:27
    */

    运行本程序,分别以默认方式、中国地区和美国地区对当前时间进行显示,结果如上所述。

    在格式化字符串中,如果需要输出固定字符,可以用’xxx’表示。

    当我们直接调用"System.out.println()"对一个ZonedDateTime或者LocalDateTime实例进行打印的时候,实际上,调用的是它们的toString()方法,默认的toString()方法显示的字符串就是按照ISO 8601格式显示的,我们可以通过DateTimeFormatter预定义的几个静态变量来引用。

    范例3:过DateTimeFormatter预定义静态变量

    package edu.blog.test07;
    
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    
    public class DateTimeFormatterTestDemo03 {
        public static void main(String[] args) {
            LocalDateTime localDateTime = LocalDateTime.now();
            System.out.println(localDateTime);
            System.out.println(DateTimeFormatter.ISO_DATE.format(localDateTime));
            System.out.println(DateTimeFormatter.ISO_DATE_TIME.format(localDateTime));
        }
    }
    
    /*
    结果:
    2021-04-02T23:38:11.707
    2021-04-02
    2021-04-02T23:38:11.707
    */

    总结

    ZonedDateTimeLocalDateTime进行格式化,需要使用DateTimeFormatter类,DateTimeFormatter可以通过格式化字符串和Locale对日期和时间进行定制输出。

     

    高并发之 ——SimpleDateFormat 类的线程安全问题和解决方案

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

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

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

    重现 SimpleDateFormat 类的线程安全问题

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

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

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

     
    1. package io.binghe.concurrent.lab06;
    2.  
    3. import java.text.ParseException;
    4. import java.text.SimpleDateFormat;
    5. import java.util.concurrent.CountDownLatch;
    6. import java.util.concurrent.ExecutorService;
    7. import java.util.concurrent.Executors;
    8. import java.util.concurrent.Semaphore;
    9.  
    10. /**
    11. * @author binghe
    12. * @version 1.0.0
    13. * @description 测试SimpleDateFormat的线程不安全问题
    14. */
    15. public class SimpleDateFormatTest01 {
    16. //执行总次数
    17. private static final int EXECUTE_COUNT = 1000;
    18. //同时运行的线程数量
    19. private static final int THREAD_COUNT = 20;
    20. //SimpleDateFormat对象
    21. private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    22.  
    23. public static void main(String[] args) throws InterruptedException {
    24. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
    25. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
    26. ExecutorService executorService = Executors.newCachedThreadPool();
    27. for (int i = 0; i < EXECUTE_COUNT; i++){
    28. executorService.execute(() -> {
    29. try {
    30. semaphore.acquire();
    31. try {
    32. simpleDateFormat.parse("2020-01-01");
    33. } catch (ParseException e) {
    34. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    35. e.printStackTrace();
    36. System.exit(1);
    37. }catch (NumberFormatException e){
    38. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    39. e.printStackTrace();
    40. System.exit(1);
    41. }
    42. semaphore.release();
    43. } catch (InterruptedException e) {
    44. System.out.println("信号量发生错误");
    45. e.printStackTrace();
    46. System.exit(1);
    47. }
    48. countDownLatch.countDown();
    49. });
    50. }
    51. countDownLatch.await();
    52. executorService.shutdown();
    53. System.out.println("所有线程格式化日期成功");
    54. }
    55. }
     

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

    simpleDateFormat.parse("2020-01-01");

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

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

     
    1. 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 格式化日期失败
    2. 线程:pool-1-thread-9 格式化日期失败
    3. 线程:pool-1-thread-10 格式化日期失败
    4. 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 格式化日期失败
    5. 线程:pool-1-thread-21 格式化日期失败
    6. Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败
    7. 线程:pool-1-thread-11 格式化日期失败
    8. java.lang.ArrayIndexOutOfBoundsException
    9. 线程:pool-1-thread-27 格式化日期失败
    10. at java.lang.System.arraycopy(Native Method)
    11. at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597)
    12. at java.lang.StringBuffer.append(StringBuffer.java:367)
    13. at java.text.DigitList.getLong(DigitList.java:191)线程:pool-1-thread-25 格式化日期失败
    14.  
    15. at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
    16. at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    17. at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    18. 线程:pool-1-thread-14 格式化日期失败
    19. at java.text.DateFormat.parse(DateFormat.java:364)
    20. at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
    21. 线程:pool-1-thread-13 格式化日期失败 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    22. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    23.  
    24. at java.lang.Thread.run(Thread.java:748)
    25. java.lang.NumberFormatException: For input string: ""
    26. at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    27. 线程:pool-1-thread-20 格式化日期失败 at java.lang.Long.parseLong(Long.java:601)
    28. at java.lang.Long.parseLong(Long.java:631)
    29.  
    30. at java.text.DigitList.getLong(DigitList.java:195)
    31. at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
    32. at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
    33. at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    34. at java.text.DateFormat.parse(DateFormat.java:364)
    35. at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47)
    36. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    37. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    38. at java.lang.Thread.run(Thread.java:748)
    39. java.lang.NumberFormatException: For input string: ""
    40. at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    41. at java.lang.Long.parseLong(Long.java:601)
    42. at java.lang.Long.parseLong(Long.java:631)
    43. at java.text.DigitList.getLong(DigitList.java:195)
    44. at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
    45. at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    46. at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    47. at java.text.DateFormat.parse(DateFormat.java:364)
    48.  
    49. Process finished with exit code 1
     

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

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

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

    通过上面的示例程序我们得知,在高并发环境下 SimpleDateFormat 类会抛出异常,导致其在高并发环境下不能良好的发挥作用。那么,SimpleDateFormat 类为何不是线程安全的呢?

    这里,我们就看下源码来进一步了解下。通过如下一行代码进入到 DateFormat 类的源码中。

    simpleDateFormat.parse("2020-01-01");

    打开 DateFormat 类的 parse (String) 方法,如下所示。

     
    1. public Date parse(String source) throws ParseException{
    2. ParsePosition pos = new ParsePosition(0);
    3. Date result = parse(source, pos);
    4. if (pos.index == 0)
    5. throw new ParseException("Unparseable date: \"" + source + "\"" ,
    6. pos.errorIndex);
    7. return result;
    8. }
     

    可以看到,在 DateFormat 类的当前 parse (String) 方法中,再次调用 parse (String, ParsePosition) 方法来格式化日期,继续查看 parse (String, ParsePosition) 方法,如下所示。

    public abstract Date parse(String source, ParsePosition pos);

    发现 parse (String, ParsePosition) 方法在 DateFormat 类中是一个抽象类,具体由子类实现。此时,我们查看此方法的实现,发现此方法正是在 SimpleDateFormat 类中实现的,如下所示。

     
    1. @Override
    2. public Date parse(String text, ParsePosition pos){
    3. checkNegativeNumberExpression();
    4.  
    5. int start = pos.index;
    6. int oldStart = start;
    7. int textLength = text.length();
    8.  
    9. boolean[] ambiguousYear = {false};
    10.  
    11. CalendarBuilder calb = new CalendarBuilder();
    12.  
    13. for (int i = 0; i < compiledPattern.length; ) {
    14. int tag = compiledPattern[i] >>> 8;
    15. int count = compiledPattern[i++] & 0xff;
    16. if (count == 255) {
    17. count = compiledPattern[i++] << 16;
    18. count |= compiledPattern[i++];
    19. }
    20.  
    21. switch (tag) {
    22. case TAG_QUOTE_ASCII_CHAR:
    23. if (start >= textLength || text.charAt(start) != (char)count) {
    24. //严重破坏了线程的安全性
    25. pos.index = oldStart;
    26. //严重破坏了线程的安全性
    27. pos.errorIndex = start;
    28. return null;
    29. }
    30. start++;
    31. break;
    32.  
    33. case TAG_QUOTE_CHARS:
    34. while (count-- > 0) {
    35. if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
    36. //严重破坏了线程的安全性
    37. pos.index = oldStart;
    38. //严重破坏了线程的安全性
    39. pos.errorIndex = start;
    40. return null;
    41. }
    42. start++;
    43. }
    44. break;
    45.  
    46. default:
    47. boolean obeyCount = false;
    48. boolean useFollowingMinusSignAsDelimiter = false;
    49.  
    50. if (i < compiledPattern.length) {
    51. int nextTag = compiledPattern[i] >>> 8;
    52. if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||
    53. nextTag == TAG_QUOTE_CHARS)) {
    54. obeyCount = true;
    55. }
    56.  
    57. if (hasFollowingMinusSign &&
    58. (nextTag == TAG_QUOTE_ASCII_CHAR ||
    59. nextTag == TAG_QUOTE_CHARS)) {
    60. int c;
    61. if (nextTag == TAG_QUOTE_ASCII_CHAR) {
    62. c = compiledPattern[i] & 0xff;
    63. } else {
    64. c = compiledPattern[i+1];
    65. }
    66.  
    67. if (c == minusSign) {
    68. useFollowingMinusSignAsDelimiter = true;
    69. }
    70. }
    71. }
    72. start = subParse(text, start, tag, count, obeyCount,
    73. ambiguousYear, pos,
    74. useFollowingMinusSignAsDelimiter, calb);
    75. if (start < 0) {
    76. //严重破坏了线程的安全性
    77. pos.index = oldStart;
    78. return null;
    79. }
    80. }
    81. }
    82. //严重破坏了线程的安全性
    83. pos.index = start;
    84.  
    85. Date parsedDate;
    86. try {
    87. parsedDate = calb.establish(calendar).getTime();
    88. if (ambiguousYear[0]) {
    89. if (parsedDate.before(defaultCenturyStart)) {
    90. parsedDate = calb.addYear(100).establish(calendar).getTime();
    91. }
    92. }
    93. }
    94. catch (IllegalArgumentException e) {
    95. //严重破坏了线程的安全性
    96. pos.errorIndex = start;
    97. 严重破坏了线程的安全性
    98. pos.index = oldStart;
    99. return null;
    100. }
    101.  
    102. return parsedDate;
    103. }
     

    通过对 SimpleDateFormat 类中的 parse (String, ParsePosition) 方法的分析可以得知,parse (String, ParsePosition) 方法中存在几处为 ParsePosition 类中的索引赋值的操作。

    一旦将 SimpleDateFormat 类定义成全局的静态变量,那么 SimpleDateFormat 类在多个线程间是共享的,这就导致 ParsePosition 类在多个线程间共享。在高并发场景下,一个线程对 ParsePosition 类中的索引进行修改,势必会影响到其他线程对 ParsePosition 类中索引的读操作。这就造成了线程的安全问题。

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

    解决 SimpleDateFormat 类的线程安全问题

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

    1. 局部变量法

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

     
    1. package io.binghe.concurrent.lab06;
    2.  
    3. import java.text.ParseException;
    4. import java.text.SimpleDateFormat;
    5. import java.util.concurrent.CountDownLatch;
    6. import java.util.concurrent.ExecutorService;
    7. import java.util.concurrent.Executors;
    8. import java.util.concurrent.Semaphore;
    9.  
    10. /**
    11. * @author binghe
    12. * @version 1.0.0
    13. * @description 局部变量法解决SimpleDateFormat类的线程安全问题
    14. */
    15. public class SimpleDateFormatTest02 {
    16. //执行总次数
    17. private static final int EXECUTE_COUNT = 1000;
    18. //同时运行的线程数量
    19. private static final int THREAD_COUNT = 20;
    20.  
    21. public static void main(String[] args) throws InterruptedException {
    22. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
    23. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
    24. ExecutorService executorService = Executors.newCachedThreadPool();
    25. for (int i = 0; i < EXECUTE_COUNT; i++){
    26. executorService.execute(() -> {
    27. try {
    28. semaphore.acquire();
    29. try {
    30. SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    31. simpleDateFormat.parse("2020-01-01");
    32. } catch (ParseException e) {
    33. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    34. e.printStackTrace();
    35. System.exit(1);
    36. }catch (NumberFormatException e){
    37. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    38. e.printStackTrace();
    39. System.exit(1);
    40. }
    41. semaphore.release();
    42. } catch (InterruptedException e) {
    43. System.out.println("信号量发生错误");
    44. e.printStackTrace();
    45. System.exit(1);
    46. }
    47. countDownLatch.countDown();
    48. });
    49. }
    50. countDownLatch.await();
    51. executorService.shutdown();
    52. System.out.println("所有线程格式化日期成功");
    53. }
    54. }
     

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

    所有线程格式化日期成功

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

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

    2.synchronized 锁方式

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

     
    1. package io.binghe.concurrent.lab06;
    2.  
    3. import java.text.ParseException;
    4. import java.text.SimpleDateFormat;
    5. import java.util.concurrent.CountDownLatch;
    6. import java.util.concurrent.ExecutorService;
    7. import java.util.concurrent.Executors;
    8. import java.util.concurrent.Semaphore;
    9.  
    10. /**
    11. * @author binghe
    12. * @version 1.0.0
    13. * @description 通过Synchronized锁解决SimpleDateFormat类的线程安全问题
    14. */
    15. public class SimpleDateFormatTest03 {
    16. //执行总次数
    17. private static final int EXECUTE_COUNT = 1000;
    18. //同时运行的线程数量
    19. private static final int THREAD_COUNT = 20;
    20. //SimpleDateFormat对象
    21. private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    22.  
    23. public static void main(String[] args) throws InterruptedException {
    24. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
    25. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
    26. ExecutorService executorService = Executors.newCachedThreadPool();
    27. for (int i = 0; i < EXECUTE_COUNT; i++){
    28. executorService.execute(() -> {
    29. try {
    30. semaphore.acquire();
    31. try {
    32. synchronized (simpleDateFormat){
    33. simpleDateFormat.parse("2020-01-01");
    34. }
    35. } catch (ParseException e) {
    36. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    37. e.printStackTrace();
    38. System.exit(1);
    39. }catch (NumberFormatException e){
    40. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    41. e.printStackTrace();
    42. System.exit(1);
    43. }
    44. semaphore.release();
    45. } catch (InterruptedException e) {
    46. System.out.println("信号量发生错误");
    47. e.printStackTrace();
    48. System.exit(1);
    49. }
    50. countDownLatch.countDown();
    51. });
    52. }
    53. countDownLatch.await();
    54. executorService.shutdown();
    55. System.out.println("所有线程格式化日期成功");
    56. }
    57. }
     

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

     
    1. synchronized (simpleDateFormat){
    2. simpleDateFormat.parse("2020-01-01");
    3. }
     

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

    所有线程格式化日期成功

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

    3.Lock 锁方式

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

     
    1. package io.binghe.concurrent.lab06;
    2.  
    3. import java.text.ParseException;
    4. import java.text.SimpleDateFormat;
    5. import java.util.concurrent.CountDownLatch;
    6. import java.util.concurrent.ExecutorService;
    7. import java.util.concurrent.Executors;
    8. import java.util.concurrent.Semaphore;
    9. import java.util.concurrent.locks.Lock;
    10. import java.util.concurrent.locks.ReentrantLock;
    11.  
    12. /**
    13. * @author binghe
    14. * @version 1.0.0
    15. * @description 通过Lock锁解决SimpleDateFormat类的线程安全问题
    16. */
    17. public class SimpleDateFormatTest04 {
    18. //执行总次数
    19. private static final int EXECUTE_COUNT = 1000;
    20. //同时运行的线程数量
    21. private static final int THREAD_COUNT = 20;
    22. //SimpleDateFormat对象
    23. private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    24. //Lock对象
    25. private static Lock lock = new ReentrantLock();
    26.  
    27. public static void main(String[] args) throws InterruptedException {
    28. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
    29. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
    30. ExecutorService executorService = Executors.newCachedThreadPool();
    31. for (int i = 0; i < EXECUTE_COUNT; i++){
    32. executorService.execute(() -> {
    33. try {
    34. semaphore.acquire();
    35. try {
    36. lock.lock();
    37. simpleDateFormat.parse("2020-01-01");
    38. } catch (ParseException e) {
    39. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    40. e.printStackTrace();
    41. System.exit(1);
    42. }catch (NumberFormatException e){
    43. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    44. e.printStackTrace();
    45. System.exit(1);
    46. }finally {
    47. lock.unlock();
    48. }
    49. semaphore.release();
    50. } catch (InterruptedException e) {
    51. System.out.println("信号量发生错误");
    52. e.printStackTrace();
    53. System.exit(1);
    54. }
    55. countDownLatch.countDown();
    56. });
    57. }
    58. countDownLatch.await();
    59. executorService.shutdown();
    60. System.out.println("所有线程格式化日期成功");
    61. }
    62. }
     

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

     
    1. finally {
    2. lock.unlock();
    3. }
     

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

    所有线程格式化日期成功

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

    4.ThreadLocal 方式

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

     
    1. package io.binghe.concurrent.lab06;
    2.  
    3. import java.text.DateFormat;
    4. import java.text.ParseException;
    5. import java.text.SimpleDateFormat;
    6. import java.util.concurrent.CountDownLatch;
    7. import java.util.concurrent.ExecutorService;
    8. import java.util.concurrent.Executors;
    9. import java.util.concurrent.Semaphore;
    10.  
    11. /**
    12. * @author binghe
    13. * @version 1.0.0
    14. * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
    15. */
    16. public class SimpleDateFormatTest05 {
    17. //执行总次数
    18. private static final int EXECUTE_COUNT = 1000;
    19. //同时运行的线程数量
    20. private static final int THREAD_COUNT = 20;
    21.  
    22. private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
    23. @Override
    24. protected DateFormat initialValue() {
    25. return new SimpleDateFormat("yyyy-MM-dd");
    26. }
    27. };
    28.  
    29. public static void main(String[] args) throws InterruptedException {
    30. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
    31. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
    32. ExecutorService executorService = Executors.newCachedThreadPool();
    33. for (int i = 0; i < EXECUTE_COUNT; i++){
    34. executorService.execute(() -> {
    35. try {
    36. semaphore.acquire();
    37. try {
    38. threadLocal.get().parse("2020-01-01");
    39. } catch (ParseException e) {
    40. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    41. e.printStackTrace();
    42. System.exit(1);
    43. }catch (NumberFormatException e){
    44. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    45. e.printStackTrace();
    46. System.exit(1);
    47. }
    48. semaphore.release();
    49. } catch (InterruptedException e) {
    50. System.out.println("信号量发生错误");
    51. e.printStackTrace();
    52. System.exit(1);
    53. }
    54. countDownLatch.countDown();
    55. });
    56. }
    57. countDownLatch.await();
    58. executorService.shutdown();
    59. System.out.println("所有线程格式化日期成功");
    60. }
    61. }
     

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

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

    所有线程格式化日期成功

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

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

     
    1. package io.binghe.concurrent.lab06;
    2.  
    3. import java.text.DateFormat;
    4. import java.text.ParseException;
    5. import java.text.SimpleDateFormat;
    6. import java.util.concurrent.CountDownLatch;
    7. import java.util.concurrent.ExecutorService;
    8. import java.util.concurrent.Executors;
    9. import java.util.concurrent.Semaphore;
    10.  
    11. /**
    12. * @author binghe
    13. * @version 1.0.0
    14. * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
    15. */
    16. public class SimpleDateFormatTest06 {
    17. //执行总次数
    18. private static final int EXECUTE_COUNT = 1000;
    19. //同时运行的线程数量
    20. private static final int THREAD_COUNT = 20;
    21.  
    22. private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
    23.  
    24. private static DateFormat getDateFormat(){
    25. DateFormat dateFormat = threadLocal.get();
    26. if(dateFormat == null){
    27. dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    28. threadLocal.set(dateFormat);
    29. }
    30. return dateFormat;
    31. }
    32.  
    33. public static void main(String[] args) throws InterruptedException {
    34. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
    35. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
    36. ExecutorService executorService = Executors.newCachedThreadPool();
    37. for (int i = 0; i < EXECUTE_COUNT; i++){
    38. executorService.execute(() -> {
    39. try {
    40. semaphore.acquire();
    41. try {
    42. getDateFormat().parse("2020-01-01");
    43. } catch (ParseException e) {
    44. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    45. e.printStackTrace();
    46. System.exit(1);
    47. }catch (NumberFormatException e){
    48. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    49. e.printStackTrace();
    50. System.exit(1);
    51. }
    52. semaphore.release();
    53. } catch (InterruptedException e) {
    54. System.out.println("信号量发生错误");
    55. e.printStackTrace();
    56. System.exit(1);
    57. }
    58. countDownLatch.countDown();
    59. });
    60. }
    61. countDownLatch.await();
    62. executorService.shutdown();
    63. System.out.println("所有线程格式化日期成功");
    64. }
    65. }
     

    5.DateTimeFormatter 方式

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

     
    1. package io.binghe.concurrent.lab06;
    2.  
    3. import java.time.LocalDate;
    4. import java.time.format.DateTimeFormatter;
    5. import java.util.concurrent.CountDownLatch;
    6. import java.util.concurrent.ExecutorService;
    7. import java.util.concurrent.Executors;
    8. import java.util.concurrent.Semaphore;
    9.  
    10. /**
    11. * @author binghe
    12. * @version 1.0.0
    13. * @description 通过DateTimeFormatter类解决线程安全问题
    14. */
    15. public class SimpleDateFormatTest07 {
    16. //执行总次数
    17. private static final int EXECUTE_COUNT = 1000;
    18. //同时运行的线程数量
    19. private static final int THREAD_COUNT = 20;
    20.  
    21. private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    22.  
    23. public static void main(String[] args) throws InterruptedException {
    24. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
    25. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
    26. ExecutorService executorService = Executors.newCachedThreadPool();
    27. for (int i = 0; i < EXECUTE_COUNT; i++){
    28. executorService.execute(() -> {
    29. try {
    30. semaphore.acquire();
    31. try {
    32. LocalDate.parse("2020-01-01", formatter);
    33. }catch (Exception e){
    34. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    35. e.printStackTrace();
    36. System.exit(1);
    37. }
    38. semaphore.release();
    39. } catch (InterruptedException e) {
    40. System.out.println("信号量发生错误");
    41. e.printStackTrace();
    42. System.exit(1);
    43. }
    44. countDownLatch.countDown();
    45. });
    46. }
    47. countDownLatch.await();
    48. executorService.shutdown();
    49. System.out.println("所有线程格式化日期成功");
    50. }
    51. }
     

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

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

    所有线程格式化日期成功

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

    6.joda-time 方式

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

     
    1. <dependency>
    2. <groupId>joda-time</groupId>
    3. <artifactId>joda-time</artifactId>
    4. <version>2.9.9</version>
    5. </dependency>
     

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

     
    1. package io.binghe.concurrent.lab06;
    2.  
    3. import org.joda.time.DateTime;
    4. import org.joda.time.format.DateTimeFormat;
    5. import org.joda.time.format.DateTimeFormatter;
    6.  
    7. import java.util.concurrent.CountDownLatch;
    8. import java.util.concurrent.ExecutorService;
    9. import java.util.concurrent.Executors;
    10. import java.util.concurrent.Semaphore;
    11.  
    12. /**
    13. * @author binghe
    14. * @version 1.0.0
    15. * @description 通过DateTimeFormatter类解决线程安全问题
    16. */
    17. public class SimpleDateFormatTest08 {
    18. //执行总次数
    19. private static final int EXECUTE_COUNT = 1000;
    20. //同时运行的线程数量
    21. private static final int THREAD_COUNT = 20;
    22.  
    23. private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");
    24.  
    25. public static void main(String[] args) throws InterruptedException {
    26. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
    27. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
    28. ExecutorService executorService = Executors.newCachedThreadPool();
    29. for (int i = 0; i < EXECUTE_COUNT; i++){
    30. executorService.execute(() -> {
    31. try {
    32. semaphore.acquire();
    33. try {
    34. DateTime.parse("2020-01-01", dateTimeFormatter).toDate();
    35. }catch (Exception e){
    36. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
    37. e.printStackTrace();
    38. System.exit(1);
    39. }
    40. semaphore.release();
    41. } catch (InterruptedException e) {
    42. System.out.println("信号量发生错误");
    43. e.printStackTrace();
    44. System.exit(1);
    45. }
    46. countDownLatch.countDown();
    47. });
    48. }
    49. countDownLatch.await();
    50. executorService.shutdown();
    51. System.out.println("所有线程格式化日期成功");
    52. }
    53. }
     

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

     
    1. import org.joda.time.DateTime;
    2. import org.joda.time.format.DateTimeFormat;
    3. 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 是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用

     

posted @ 2022-07-18 10:58  CharyGao  阅读(256)  评论(0编辑  收藏  举报