SimpleDateFormat在多线程下存在并发安全问题。

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

  1. /**
  2. *
  3. * 重现SimpleDateFormat类的线程安全问题
  4. *
  5. * @author 二十
  6. * @since 2021/9/12 10:05 下午
  7. */
  8. public class SdfTest {
  9. /**执行总次数*/
  10. private static final int EXECUTE_COUNT=1000;
  11. /**并发线程数*/
  12. private static final int THREAD_COUNT=100;
  13. private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  14. @Test
  15. public void test()throws Exception{
  16. final Semaphore semaphore =new Semaphore(THREAD_COUNT);
  17. final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
  18. ExecutorService executor = Executors.newCachedThreadPool();
  19. for (int i = 0; i < EXECUTE_COUNT; i++) {
  20. executor.execute(()->{
  21. try {
  22. semaphore.acquire();
  23. sdf.parse("2021-09-15");
  24. }catch (Exception e){
  25. System.out.println(Thread.currentThread().getName()+"格式化时间失败!");
  26. }finally {
  27. semaphore.release();
  28. cdl.countDown();
  29. }
  30. });
  31. }
  32. cdl.await();
  33. executor.shutdown();
  34. }
  35. }

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

2.SimpleDateFormat为什么不是线程安全的?

  1. public Date parse(String source) throws ParseException
  2. {
  3. ParsePosition pos = new ParsePosition(0);
  4. //调用了parse方法,最终的实现类在SimpleDateFormat类中
  5. Date result = parse(source, pos);
  6. if (pos.index == 0)
  7. throw new ParseException("Unparseable date: \"" + source + "\"" ,
  8. pos.errorIndex);
  9. return result;
  10. }

点进去parse(),这是一个抽象的方法。

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

最终的实现类还是在SimpleDateFormat类中。

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

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

一旦将SimpleDateFormat类定义成全局静态变量,那么SimpleDateFormat类在多线程之间是共享的,这就会导致ParsePosition类在多线程之间共享。

在高并发场景下,一个线程对ParsePosition类中的索引进行修改,一定会影响到其他线程对ParsePosition类中索引的读。这就造成了线程的安全问题。

那么在确定了SimpleDateFormat线程不安全的原因以后,如何来解决这个问题。

3.SimpleDateFormat类线程安全的解决

3.1 局部变量法

  1. public class SdfTest {
  2. /**执行总次数*/
  3. private static final int EXECUTE_COUNT=1000;
  4. /**并发线程数*/
  5. private static final int THREAD_COUNT=100;
  6. @Test
  7. public void test()throws Exception{
  8. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  9. final Semaphore semaphore =new Semaphore(THREAD_COUNT);
  10. final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
  11. ExecutorService executor = Executors.newCachedThreadPool();
  12. for (int i = 0; i < EXECUTE_COUNT; i++) {
  13. executor.execute(()->{
  14. try {
  15. semaphore.acquire();
  16. sdf.parse("2021-09-15");
  17. }catch (Exception e){
  18. System.out.println(Thread.currentThread().getName()+"格式化时间失败!");
  19. }finally {
  20. semaphore.release();
  21. cdl.countDown();
  22. }
  23. });
  24. }
  25. cdl.await();
  26. executor.shutdown();
  27. }
  28. }

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

3.2 synchronized

  1. public class SdfTest {
  2. /**执行总次数*/
  3. private static final int EXECUTE_COUNT=1000;
  4. /**并发线程数*/
  5. private static final int THREAD_COUNT=100;
  6. private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");;
  7. @Test
  8. public void test()throws Exception{
  9. final Semaphore semaphore =new Semaphore(THREAD_COUNT);
  10. final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
  11. ExecutorService executor = Executors.newCachedThreadPool();
  12. for (int i = 0; i < EXECUTE_COUNT; i++) {
  13. executor.execute(()->{
  14. try {
  15. semaphore.acquire();
  16. synchronized (sdf){
  17. sdf.parse("2021-09-15");
  18. }
  19. }catch (Exception e){
  20. System.out.println(Thread.currentThread().getName()+"格式化时间失败!");
  21. }finally {
  22. semaphore.release();
  23. cdl.countDown();
  24. }
  25. });
  26. }
  27. cdl.await();
  28. executor.shutdown();
  29. }
  30. }

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

3.3 Lock锁方式

  1. public class SdfTest {
  2. /**
  3. * 执行总次数
  4. */
  5. private static final int EXECUTE_COUNT = 1000;
  6. /**
  7. * 并发线程数
  8. */
  9. private static final int THREAD_COUNT = 100;
  10. private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  11. private static Lock lock = new ReentrantLock();
  12. @Test
  13. public void test() throws Exception {
  14. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
  15. final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
  16. ExecutorService executor = Executors.newCachedThreadPool();
  17. for (int i = 0; i < EXECUTE_COUNT; i++) {
  18. executor.execute(() -> {
  19. try {
  20. semaphore.acquire();
  21. lock.lock();
  22. try {
  23. sdf.parse("2021-09-15");
  24. } finally {
  25. lock.unlock();
  26. }
  27. } catch (Exception e) {
  28. System.out.println(Thread.currentThread().getName() + "格式化时间失败!");
  29. } finally {
  30. semaphore.release();
  31. cdl.countDown();
  32. }
  33. });
  34. }
  35. cdl.await();
  36. executor.shutdown();
  37. }
  38. }

通过代码得知:首先定义了Lock类型的全局静态变量作为加锁和释放锁的句柄。然后再SimpleDateFormat.parse()代码执行之前加锁。

这里需要注意的一点是:为了防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块。

此种方式在并发下同样影响性能,不太推荐在高并发生产中使用。

3.4ThreadLocal方式

  1. public class SdfTest {
  2. /**
  3. * 执行总次数
  4. */
  5. private static final int EXECUTE_COUNT = 1000;
  6. /**
  7. * 并发线程数
  8. */
  9. private static final int THREAD_COUNT = 100;
  10. private static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
  11. @Test
  12. public void test() throws Exception {
  13. final Semaphore semaphore = new Semaphore(THREAD_COUNT);
  14. final CountDownLatch cdl = new CountDownLatch(EXECUTE_COUNT);
  15. ExecutorService executor = Executors.newCachedThreadPool();
  16. for (int i = 0; i < EXECUTE_COUNT; i++) {
  17. executor.execute(() -> {
  18. try {
  19. semaphore.acquire();
  20. threadLocal.get().parse("2021-09-15");
  21. } catch (Exception e) {
  22. System.out.println(Thread.currentThread().getName() + "格式化时间失败!");
  23. } finally {
  24. threadLocal.remove();
  25. semaphore.release();
  26. cdl.countDown();
  27. }
  28. });
  29. }
  30. cdl.await();
  31. executor.shutdown();
  32. }
  33. }

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

此种方式运行效率比较高,推荐在高并发场景下使用。

3.5 DateTimeFormatter

jdk8线程安全的时间日期API。