1、重现 SimpleDateFormat 类的线程安全问题
面试中常提到 SimpleDateFormat 线程不安全,为了重现这个问题,可以使用线程池结合 CountDownLatch 和 Semaphore 类。
示例代码
java
package com.batch.controller; import java.text.SimpleDateFormat; import java.util.concurrent.*; /** * @Author: zouming * @Date: 2024/5/29 0:26 */ public class SimpleDateFormatDemo { // 执行总次数 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(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 核心方法:多个线程共享同一个 SimpleDateFormat 对象 simpleDateFormat.parse("2024-05-29"); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }运行结果
运行上述代码会抛出多种异常:
text
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)
结论
在高并发下使用 SimpleDateFormat 类格式化日期时抛出了异常,证明 SimpleDateFormat 类不是线程安全的!
2、SimpleDateFormat 类为何不是线程安全的
SimpleDateFormat 继承自 DateFormat 类,DateFormat 类中维护了一个全局的 Calendar 变量:
java
public abstract class DateFormat extends Format { /** * 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}. * @serial */ protected Calendar calendar; // 省略其他代码... }从注释可以看出,这个 Calendar 对象既用于格式化也用于解析日期时间。
在 SimpleDateFormat 的parse()方法中,会调用CalendarBuilder.establish()方法,该方法中会先调用cal.clear()清除 Calendar 对象中设置的值,再调用cal.set()重新设置新的值:
java
Calendar establish(Calendar cal) { // ... 省略部分代码 cal.clear(); // 先清除 // ... 设置新值 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; } } } // ... 省略部分代码 return cal; }由于 Calendar 内部并没有线程安全机制,并且clear()和set()操作也都不是原子性的,所以当多个线程同时操作一个 SimpleDateFormat 时就会引起 Calendar 的值混乱。类似地,format()方法也存在同样的问题。
因此,SimpleDateFormat 类不是线程安全的根本原因是:DateFormat 类中的 Calendar 对象被多线程共享,而 Calendar 对象本身不支持线程安全。
3、解决 SimpleDateFormat 类的线程安全问题
方案1:局部变量法
java
public class SimpleDateFormatDemo01 { // 执行总次数 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(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 每次使用时创建新的 SimpleDateFormat 对象 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); simpleDateFormat.parse("2024-05-29"); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }缺点:在高并发下会创建大量的 SimpleDateFormat 类对象,影响程序的性能。
方案2:synchronized 锁方式
java
public class SimpleDateFormatDemo02 { // 执行总次数 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(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 使用 synchronized 同步 synchronized (simpleDateFormat) { simpleDateFormat.parse("2020-01-01"); } } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }缺点:同一时刻只能有一个线程执行parse()方法,影响程序的执行性能。
方案3:Lock 锁方式
java
import java.text.SimpleDateFormat; import java.util.concurrent.*; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SimpleDateFormatDemo03 { // 执行总次数 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(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); lock.lock(); // 加锁 simpleDateFormat.parse("2020-01-01"); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); lock.unlock(); // 释放锁 } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }缺点:同样会影响高并发场景下的性能。
方案4:ThreadLocal 方式
java
import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.concurrent.*; public class SimpleDateFormatDemo04 { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; // 使用 ThreadLocal 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(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 从 ThreadLocal 获取 SimpleDateFormat threadLocal.get().parse("2020-01-01"); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } threadLocal.remove(); // 清理 ThreadLocal System.out.println("所有线程格式化日期成功"); } } }优点:每个线程使用自己的 SimpleDateFormat 副本,线程安全且性能较好。
方案5:DateTimeFormatter 方式(Java 8+)
java
import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.concurrent.*; public class SimpleDateFormatDemo05 { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; // DateTimeFormatter(线程安全) 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(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 使用 DateTimeFormatter LocalDate.parse("2020-01-01", formatter); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }优点:DateTimeFormatter 是线程安全的,推荐在 Java 8+ 中使用。
方案6:joda-time 方式
首先添加依赖:
xml
<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.9</version> </dependency>
示例代码:
java
import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.concurrent.*; public class SimpleDateFormatDemo06 { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; // DateTimeFormatter 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(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); DateTime.parse("2020-01-01", dateTimeFormatter).toDate(); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }优点:joda-time 是线程安全的第三方库,性能经过高并发考验。
4、总结
| 解决方案 | 原理 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| 局部变量法 | 每次使用创建新对象 | 简单直接 | 创建大量对象,性能差 | 不推荐 |
| synchronized锁 | 同步访问共享对象 | 线程安全 | 并发性能差 | 不推荐 |
| Lock锁 | 显式锁控制访问 | 更灵活的锁控制 | 并发性能差 | 不推荐 |
| ThreadLocal | 每个线程独立副本 | 线程安全,性能好 | 需要管理内存 | 推荐 |
| DateTimeFormatter | Java 8 线程安全类 | 官方推荐,性能好 | 仅限 Java 8+ | 强烈推荐 |
| joda-time | 第三方线程安全库 | 性能好,功能丰富 | 需要额外依赖 | 推荐 |
最佳实践建议:
Java 8+ 项目:优先使用
DateTimeFormatterJava 8 以下项目:
高并发场景:使用
ThreadLocal或joda-time低并发场景:可使用局部变量法
性能要求高:
joda-time或ThreadLocal代码简洁性:
DateTimeFormatter(Java 8+)
注意:SimpleDateFormat 的线程安全问题主要体现在其内部共享的 Calendar 对象上,因此在多线程环境下必须采取适当的同步措施或使用线程安全的替代方案。