SimpleDateFormat非线程安全


前言

提示:在SimpleDateFormat类中官方其实已经标注了,该类非线程安全:

 * Date formats are not synchronized.
 * It is recommended to create separate format instances for each thread.
 * If multiple threads access a format concurrently, it must be synchronized
 * externally.
 * 日期格式不同步。建议为每个线程创建单独的格式实例。如果多个线程同时访问一种格式,则必须对其进行   	同步。
public class SimpleDateFormat extends DateFormat {


}

一、SimpleDateFormat 是什么?

SimpleDateFormat 是java中非常常用的类,可以对日期字符串进行解析,但是如果写法有问题,将会出现线程安全问题,因为SimpleDateFormat 类不是线程安全的,在多线程环境下会出现数据脏读甚至报错的情况。

二、示例

1.自定义DateUtils.java

public class DateUtils {
	private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
	private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>() {
		@Override
		protected SimpleDateFormat initialValue() {
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
			return sdf;
		}
	};

	// 错误示范
	public static Date parseString(String datetime) throws Exception {
		return simpleDateFormat.parse(datetime);
	}

	// 通过同步synchronized解决该问题,缺陷影响执行速率
	public static Date parseString1(String datetime) throws Exception {
		synchronized (simpleDateFormat) {
			return simpleDateFormat.parse(datetime);
		}
	}

	// 每次都创建新的SimpleDateFormat实例,缺陷:开销大
	public static Date parseString2(String datetime) throws Exception {
		return new SimpleDateFormat("yyyy-MM-dd").parse(datetime);
	}

	// 通过ThreadLocal解决,最佳方式
	public static Date parseString3(String datetime) throws Exception {
		return threadLocal.get().parse(datetime);
	}

	// 第三方工具:hutool类
	public static Date parseString4(String datetime) throws Exception {
		DateTime result = DateUtil.parse(datetime, "yyyy-MM-dd");
		return result;
	}
}

2.多线程测试异常情况

public class Test1 {
	public static void main(String[] args) {
		ExecutorService executorService = Executors.newFixedThreadPool(100);
		for (int i = 0; i < 50; i++) {
			executorService.execute(() -> {
				try {
					System.out.println(DateUtils.parseString("2020-05-01"));
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		executorService.shutdown();
		try {
			executorService.awaitTermination(100, TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}
}

报错:类型异常、数据脏读等问题!

三、非线程安全问题分析

提示:一般为了方便使用把SimpleDateFormat定义成一个static变量,而静态变量是线程共享的(即核心Calendar也是共享,无论是format还是parse方法),所以线程a就会访问到线程b的时间,从而导致时间的误差。

从上面代码看1,2,3,4,5操作不是原子性,当多个线程调用parse方法适合,比如A执行了(3)(4),也就是设置了cal对象,在执行代码(5) 前线程B 执行了代码(3) 清空了cal对象,由于多个线程使用的是一个Calendar对象,所以线程A执行(5) 的时候返回的是被线程B清空后的对象。
public Date parse(String text, ParsePosition pos){
        //(1)解析日期字符串text放入CalendarBuilder的实例calb中,
        .....
        Date parsedDate;
        try {
        	//(2)使用calb中把日期数据设置到calendar
            parsedDate = calb.establish(calendar).getTime();
            ...
        }
        return parsedDate;
}


class CalendarBuilder {
	Calendar establish(Calendar cal) {
	   ...
	   //(3)重置日期对象cal的属性值
	   cal.clear();
	   //(4) 使用calb中中属性设置cal
	   ...
	   //(5)返回设置好的cal对象
	   return cal;
	}
}
问题原因与parse方法一样,由于多线程公用一个calendar对象。
public class SimpleDateFormat extends DateFormat {
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        
        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) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;

            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;

            default:
            	//核心:将calendar进行解析获取的字符串拼接到toAppendTo中
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
    }
}

四、通过以下方式可解决

提示:以下4中方法,在DateUtils.java已标注优点和缺点。

  1. 通过synchronized同步参考parseString1方法)
  2. 每个线程执行时都创建新的SimpleDateFormat实例(参考parseString2方法)
  3. 通过ThreadLocal解决,最佳方式(参考parseString3方法)
  4. 通过第三方工具,测试中我以hutool为例(参考parseString4方法)
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值