高并发场景下,6种解决SimpleDateFormat类的线程安全问题方法 ...

打印 上一主题 下一主题

主题 573|帖子 573|积分 1719

摘要:解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考。
本文分享自华为云社区《【高并发】更正SimpleDateFormat类线程不安全问题分析的错误》,作者: 冰 河 。
解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考,大家也可以在评论区给出更多的解决方案。
1.局部变量法

最简单的一种方式就是将SimpleDateFormat类对象定义成局部变量,如下所示的代码,将SimpleDateFormat类对象定义在parse(String)方法的上面,即可解决问题。
  1. package io.binghe.concurrent.lab06;
  2. import java.text.ParseException;
  3. import java.text.SimpleDateFormat;
  4. import java.util.concurrent.CountDownLatch;
  5. import java.util.concurrent.ExecutorService;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.Semaphore;
  8. /**
  9. * @author binghe
  10. * @version 1.0.0
  11. * @description 局部变量法解决SimpleDateFormat类的线程安全问题
  12. */
  13. public class SimpleDateFormatTest02 {
  14. //执行总次数
  15. private static final int EXECUTE_COUNT = 1000;
  16. //同时运行的线程数量
  17. private static final int THREAD_COUNT = 20;
  18. public static void main(String[] args) throws InterruptedException {
  19. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
  20. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
  21. ExecutorService executorService = Executors.newCachedThreadPool();
  22. for (int i = 0; i < EXECUTE_COUNT; i++){
  23. executorService.execute(() -> {
  24. try {
  25. semaphore.acquire();
  26. try {
  27. SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
  28. simpleDateFormat.parse("2020-01-01");
  29. } catch (ParseException e) {
  30. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
  31. e.printStackTrace();
  32. System.exit(1);
  33. }catch (NumberFormatException 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. }
复制代码
此时运行修改后的程序,输出结果如下所示。
  1. 所有线程格式化日期成功
复制代码
至于在高并发场景下使用局部变量为何能解决线程的安全问题,会在【JVM专题】的JVM内存模式相关内容中深入剖析,这里不做过多的介绍了。
当然,这种方式在高并发下会创建大量的SimpleDateFormat类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推荐。
2.synchronized锁方式

将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,此时在调用格式化时间的方法时,对SimpleDateFormat对象进行同步即可,代码如下所示。
  1. package io.binghe.concurrent.lab06;
  2. import java.text.ParseException;
  3. import java.text.SimpleDateFormat;
  4. import java.util.concurrent.CountDownLatch;
  5. import java.util.concurrent.ExecutorService;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.Semaphore;
  8. /**
  9. * @author binghe
  10. * @version 1.0.0
  11. * @description 通过Synchronized锁解决SimpleDateFormat类的线程安全问题
  12. */
  13. public class SimpleDateFormatTest03 {
  14. //执行总次数
  15. private static final int EXECUTE_COUNT = 1000;
  16. //同时运行的线程数量
  17. private static final int THREAD_COUNT = 20;
  18. //SimpleDateFormat对象
  19. private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
  20. public static void main(String[] args) throws InterruptedException {
  21. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
  22. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
  23. ExecutorService executorService = Executors.newCachedThreadPool();
  24. for (int i = 0; i < EXECUTE_COUNT; i++){
  25. executorService.execute(() -> {
  26. try {
  27. semaphore.acquire();
  28. try {
  29. synchronized (simpleDateFormat){
  30. simpleDateFormat.parse("2020-01-01");
  31. }
  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. }
复制代码
此时,解决问题的关键代码如下所示。
  1. synchronized (simpleDateFormat){
  2. simpleDateFormat.parse("2020-01-01");
  3. }
复制代码
运行程序,输出结果如下所示。
  1. 所有线程格式化日期成功
复制代码
需要注意的是,虽然这种方式能够解决SimpleDateFormat类的线程安全问题,但是由于在程序的执行过程中,为SimpleDateFormat类对象加上了synchronized锁,导致同一时刻只能有一个线程执行parse(String)方法。此时,会影响程序的执行性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的。
3.Lock锁方式

Lock锁方式与synchronized锁方式实现原理相同,都是在高并发下通过JVM的锁机制来保证程序的线程安全。通过Lock锁方式解决问题的代码如下所示。
  1. package io.binghe.concurrent.lab06;
  2. import java.text.ParseException;
  3. import java.text.SimpleDateFormat;
  4. import java.util.concurrent.CountDownLatch;
  5. import java.util.concurrent.ExecutorService;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.Semaphore;
  8. import java.util.concurrent.locks.Lock;
  9. import java.util.concurrent.locks.ReentrantLock;
  10. /**
  11. * @author binghe
  12. * @version 1.0.0
  13. * @description 通过Lock锁解决SimpleDateFormat类的线程安全问题
  14. */
  15. public class SimpleDateFormatTest04 {
  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. //Lock对象
  23. private static Lock lock = new ReentrantLock();
  24. public static void main(String[] args) throws InterruptedException {
  25. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
  26. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
  27. ExecutorService executorService = Executors.newCachedThreadPool();
  28. for (int i = 0; i < EXECUTE_COUNT; i++){
  29. executorService.execute(() -> {
  30. try {
  31. semaphore.acquire();
  32. try {
  33. lock.lock();
  34. simpleDateFormat.parse("2020-01-01");
  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. }finally {
  44. lock.unlock();
  45. }
  46. semaphore.release();
  47. } catch (InterruptedException e) {
  48. System.out.println("信号量发生错误");
  49. e.printStackTrace();
  50. System.exit(1);
  51. }
  52. countDownLatch.countDown();
  53. });
  54. }
  55. countDownLatch.await();
  56. executorService.shutdown();
  57. System.out.println("所有线程格式化日期成功");
  58. }
  59. }
复制代码
通过代码可以得知,首先,定义了一个Lock类型的全局静态变量作为加锁和释放锁的句柄。然后在simpleDateFormat.parse(String)代码之前通过lock.lock()加锁。这里需要注意的一点是:为防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块中,如下所示。
  1. finally {
  2. lock.unlock();
  3. }
复制代码
运行程序,输出结果如下所示。
  1. 所有线程格式化日期成功
复制代码
此种方式同样会影响高并发场景下的性能,不太建议在高并发的生产环境使用。
4.ThreadLocal方式

使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题,使用ThreadLocal解决线程安全问题的代码如下所示。
  1. package io.binghe.concurrent.lab06;
  2. import java.text.DateFormat;
  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. * @author binghe
  11. * @version 1.0.0
  12. * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
  13. */
  14. public class SimpleDateFormatTest05 {
  15. //执行总次数
  16. private static final int EXECUTE_COUNT = 1000;
  17. //同时运行的线程数量
  18. private static final int THREAD_COUNT = 20;
  19. private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
  20. @Override
  21. protected DateFormat initialValue() {
  22. return new SimpleDateFormat("yyyy-MM-dd");
  23. }
  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. threadLocal.get().parse("2020-01-01");
  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. }
复制代码
通过代码可以得知,将每个线程使用的SimpleDateFormat副本保存在ThreadLocal中,各个线程在使用时互不干扰,从而解决了线程安全问题。
运行程序,输出结果如下所示。
  1. 所有线程格式化日期成功
复制代码
此种方式运行效率比较高,推荐在高并发业务场景的生产环境使用。
另外,使用ThreadLocal也可以写成如下形式的代码,效果是一样的。
  1. package io.binghe.concurrent.lab06;
  2. import java.text.DateFormat;
  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. * @author binghe
  11. * @version 1.0.0
  12. * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题
  13. */
  14. public class SimpleDateFormatTest06 {
  15. //执行总次数
  16. private static final int EXECUTE_COUNT = 1000;
  17. //同时运行的线程数量
  18. private static final int THREAD_COUNT = 20;
  19. private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();
  20. private static DateFormat getDateFormat(){
  21. DateFormat dateFormat = threadLocal.get();
  22. if(dateFormat == null){
  23. dateFormat = new SimpleDateFormat("yyyy-MM-dd");
  24. threadLocal.set(dateFormat);
  25. }
  26. return dateFormat;
  27. }
  28. public static void main(String[] args) throws InterruptedException {
  29. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
  30. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
  31. ExecutorService executorService = Executors.newCachedThreadPool();
  32. for (int i = 0; i < EXECUTE_COUNT; i++){
  33. executorService.execute(() -> {
  34. try {
  35. semaphore.acquire();
  36. try {
  37. getDateFormat().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. }
  47. semaphore.release();
  48. } catch (InterruptedException e) {
  49. System.out.println("信号量发生错误");
  50. e.printStackTrace();
  51. System.exit(1);
  52. }
  53. countDownLatch.countDown();
  54. });
  55. }
  56. countDownLatch.await();
  57. executorService.shutdown();
  58. System.out.println("所有线程格式化日期成功");
  59. }
  60. }
复制代码
5.DateTimeFormatter方式

DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。代码如下所示。
  1. package io.binghe.concurrent.lab06;
  2. import java.time.LocalDate;
  3. import java.time.format.DateTimeFormatter;
  4. import java.util.concurrent.CountDownLatch;
  5. import java.util.concurrent.ExecutorService;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.Semaphore;
  8. /**
  9. * @author binghe
  10. * @version 1.0.0
  11. * @description 通过DateTimeFormatter类解决线程安全问题
  12. */
  13. public class SimpleDateFormatTest07 {
  14. //执行总次数
  15. private static final int EXECUTE_COUNT = 1000;
  16. //同时运行的线程数量
  17. private static final int THREAD_COUNT = 20;
  18. private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
  19. public static void main(String[] args) throws InterruptedException {
  20. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
  21. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
  22. ExecutorService executorService = Executors.newCachedThreadPool();
  23. for (int i = 0; i < EXECUTE_COUNT; i++){
  24. executorService.execute(() -> {
  25. try {
  26. semaphore.acquire();
  27. try {
  28. LocalDate.parse("2020-01-01", formatter);
  29. }catch (Exception e){
  30. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
  31. e.printStackTrace();
  32. System.exit(1);
  33. }
  34. semaphore.release();
  35. } catch (InterruptedException e) {
  36. System.out.println("信号量发生错误");
  37. e.printStackTrace();
  38. System.exit(1);
  39. }
  40. countDownLatch.countDown();
  41. });
  42. }
  43. countDownLatch.await();
  44. executorService.shutdown();
  45. System.out.println("所有线程格式化日期成功");
  46. }
  47. }
复制代码
可以看到,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。
运行程序,输出结果如下所示。
  1. 所有线程格式化日期成功
复制代码
使用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. import org.joda.time.DateTime;
  3. import org.joda.time.format.DateTimeFormat;
  4. import org.joda.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. * @author binghe
  11. * @version 1.0.0
  12. * @description 通过DateTimeFormatter类解决线程安全问题
  13. */
  14. public class SimpleDateFormatTest08 {
  15. //执行总次数
  16. private static final int EXECUTE_COUNT = 1000;
  17. //同时运行的线程数量
  18. private static final int THREAD_COUNT = 20;
  19. private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");
  20. public static void main(String[] args) throws InterruptedException {
  21. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
  22. final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
  23. ExecutorService executorService = Executors.newCachedThreadPool();
  24. for (int i = 0; i < EXECUTE_COUNT; i++){
  25. executorService.execute(() -> {
  26. try {
  27. semaphore.acquire();
  28. try {
  29. DateTime.parse("2020-01-01", dateTimeFormatter).toDate();
  30. }catch (Exception e){
  31. System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
  32. e.printStackTrace();
  33. System.exit(1);
  34. }
  35. semaphore.release();
  36. } catch (InterruptedException e) {
  37. System.out.println("信号量发生错误");
  38. e.printStackTrace();
  39. System.exit(1);
  40. }
  41. countDownLatch.countDown();
  42. });
  43. }
  44. countDownLatch.await();
  45. executorService.shutdown();
  46. System.out.println("所有线程格式化日期成功");
  47. }
  48. }
复制代码
这里,需要注意的是: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;
复制代码
运行程序,输出结果如下所示。
  1. 所有线程格式化日期成功
复制代码
使用joda-time库来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。
解决SimpleDateFormat类的线程安全问题的方案总结

综上所示:在解决解决SimpleDateFormat类的线程安全问题的几种方案中,局部变量法由于线程每次执行格式化时间时,都会创建SimpleDateFormat类的对象,这会导致创建大量的SimpleDateFormat对象,浪费运行空间和消耗服务器的性能,因为JVM创建和销毁对象是要耗费性能的。所以,不推荐在高并发要求的生产环境使用。
synchronized锁方式和Lock锁方式在处理问题的本质上是一致的,通过加锁的方式,使同一时刻只能有一个线程执行格式化日期和时间的操作。这种方式虽然减少了SimpleDateFormat对象的创建,但是由于同步锁的存在,导致性能下降,所以,不推荐在高并发要求的生产环境使用。
ThreadLocal通过保存各个线程的SimpleDateFormat类对象的副本,使每个线程在运行时,各自使用自身绑定的SimpleDateFormat对象,互不干扰,执行性能比较高,推荐在高并发的生产环境使用。
DateTimeFormatter是Java 8中提供的处理日期和时间的类,DateTimeFormatter类本身就是线程安全的,经压测,DateTimeFormatter类处理日期和时间的性能效果还不错(后文单独写一篇关于高并发下性能压测的文章)。所以,推荐在高并发场景下的生产环境使用。
joda-time是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用。
 
点击关注,第一时间了解华为云新鲜技术~

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

涛声依旧在

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表