参考文章:https://blog.youkuaiyun.com/l1028386804/article/details/104397090
文章分析SimpleDateFormat作为公共变量时,高并发场景下,其format()或parse()函数的线程安全问题。
一、原理分析
查看源码,发现SimpleDateFormat的format()方法实际操作的就是Calendar变量。当声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问。
《DateFormat.java》源码
public final String format(Date date) {
return format(date, new StringBuffer(), DontCareFieldPosition.INSTANCE).toString();
}
《SimpleDateFormat.java》源码
/**
* The compiled pattern.
*/
transient private char[] compiledPattern = {260, 514}; // 'A', 'Ȃ'
/**
* Tags for the compiled pattern.
*/
private final static int TAG_QUOTE_ASCII_CHAR = 100;
private final static int TAG_QUOTE_CHARS = 101;
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
pos.beginIndex = pos.endIndex = 0;
return format(date, toAppendTo, pos.getFieldDelegate());
}
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
/// ASCII码
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
///
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
/**
* Returns true if the DateFormatSymbols has been set explicitly or locale is null.
*/
private boolean useDateFormatSymbols() {
return useDateFormatSymbols || locale == null; // useDateFormatSymbols=false, locale="zh-CN"
}
高并发场景下,假设线程A执行完calendar.setTime(date),把时间设置成 “2024-07-26” 后线程被挂起;线程B获得CPU执行权限,也执行到了calendar.setTime(date),把时间设置为 “2024-07-27” 后线程被挂起,线程A继续执行,calendar还会被继续使用subFormat()方法,而这时calendar用的是线程B设置的值了,此时就会引发问题,如时间不对,线程挂死等。
二、编码验证
CountDownLatch 类可以使一个线程等待其他线程各自执行完毕后再执行,这里使主线程等待子线程执行完毕。
Semaphore 类是计数信号量,被获取后必须由获取它的线程释放,可用来限制指定资源的访问线程数,如限流。
package *;
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;
/**
* 测试SimpleDateFormat的线程安全问题
*/
public class SimpleDateFormatTest {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 线程总的执行次数
private static final int EXECUTE_ACCOUNT = 1000;
// 同时执行的线程数量
private static final int THREAD_COUNT = 20;
public static void main(String[] args) throws InterruptedException {
final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_ACCOUNT);
final Semaphore semaphore = new Semaphore(THREAD_COUNT);
/// 定义线程池
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < EXECUTE_ACCOUNT; i++){
executorService.execute(() -> {
try {
// 获取信号
semaphore.acquire();
try {
sdf.parse("2024-07-26");
} 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("所有线程解析日期成功");
}
}
三、解决办法
1. 局部变量法
高并发场景下JVM会频繁创建、销毁SimpleDateFormat对象,影响程序的性能,生产环境不推荐使用;
2. synchronized 加锁
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
synchronized (sdf){
sdf.parse("2024-07-26");
}
} 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);
}
对同一个SimpleDateFormat对象加锁,使同一时刻只能有一个线程执行parse(String)方法。在高并发场景下会影响程序的执行性能,生产环境不推荐使用;
3. Lock 加锁
private static Lock lock = new ReentrantLock();
try {
lock.lock();
simpleDateFormat.parse("2024-07-26");
} 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();
}
同 synchronized 锁方式
4. ThreadLocal
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
try {
threadLocal.get().parse("2024-07-26");
} 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);
}
ThreadLocal通过保存各个线程的SimpleDateFormat类对象的副本,使每个线程在运行时,各自使用自身绑定的SimpleDateFormat对象,互不干扰,执行性能比较高,推荐在高并发的生产环境使用。
TODO: 使用 threadLocal 时,需注意内存溢出问题,要记得释放内存 !
5. DateTimeFormatter
private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
try {
LocalDate.parse("2024-07-26", formatter);
}catch (Exception e){
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(1);
}
或
private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
java.time.LocalDateTime.now().format(formatter);
LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault()).format(formatter);