SimpleDateFormat的线程安全问题

参考文章:https://blog.csdn.net/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);

6. joda-time

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值