当前位置: 代码迷 >> 综合 >> SimpleDateFormat 线程不安全的原因剖析及解决方案
  详细解决方案

SimpleDateFormat 线程不安全的原因剖析及解决方案

热度:90   发布时间:2023-12-03 15:54:08.0

在项目中经常会用到日期转换工具SimpleDateFormat。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

由于此方法会经常被用到,所以常常会用静态来修饰,以实现复用。但这样其实是有问题的。

举个例子

public class Test {public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 100; i++) {executorService.execute(new Runnable() {@Overridepublic void run() {try {System.out.println(sdf.parse("2019-01-01 01:00:00"));}catch (Exception e){e.printStackTrace();}}});}}
}

来看一下结果,是不是和预期的不一样:

问题分析:

1 SimpleDateFormat初始化

那为什么会出现这个问题呢?来通过simpleDateFormat的parse方法来分析一下:

SimpleDateFormat中没有静态代码块、静态变量,所以在new SimpleDateFormat之前并没有什么操作,接下来就是调用它的构造函数:

该构造函数调用会给Locale个默认值,调用下面的构造方法。看一下这个构造方法:

  • 校验传入的格式化字符串;
  • 初始化Calendar;
  • 进行一些额外的初始化操作;

下面重点看一下第二步:初始化calendar操作:可以看出calender是个成员变量,也就是存在于堆中,被线程共享。

这样就完成了SimpleDateFormat的初始化操作。

2 SimpleDateFormat parse()操作

下面的for循环不作为本文的重点分析。compiledPattern是在上面SimpleDateFormar初始化 第三步中,将对象格式string进行转换的来的,并进行一系列的处理。

下面就是字符串转日期的方法:

进入看一下具体的实现:

在方法中,calendar对象做了clear,set等多个操作。如果是多个线程的话,必然会引起并发问题。所以有了上面的错误。

formate()方法也是一样的。其中维护了一个共享的变量Calendar,所以也会出现并发问题。

解决方法:

那如何去解决该问题呢?

1 SimpleDateFormat 每次使用时重新创建,以牺牲空间的方法换取多线程下的日期转换的安全性。

2对日期转换进行加锁:synchronized(lock),以互斥的方式从根源上解决多线程问题。但是效率低下,不建议使用。

3 使用ThreadLocal;每个线程都拥有自己的SimpleDateFormat对象。

package com.anjz.test.simpleDateFormat;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** 时间工具类* @author ding.shuai* @date 2017年6月10日上午11:31:59*/
public class DateUtil {/*** 锁对象*/private static final Object lockObj = new Object(); /*** 存放不同的日期模板格式的sdf的Map*/private static Map<String, ThreadLocal<SimpleDateFormat>> sdfMap = new HashMap<String, ThreadLocal<SimpleDateFormat>>();/*** 返回一个ThreadLocal的sdf,每个线程只会new一次sdf* * @param pattern* @return*/private static SimpleDateFormat getSdf(final String pattern) {ThreadLocal<SimpleDateFormat> tl = sdfMap.get(pattern);// 此处的双重判断和同步是为了防止sdfMap这个单例被多次put重复的sdfif (tl == null) {synchronized (lockObj) {tl = sdfMap.get(pattern);if (tl == null) {// 只有Map中还没有这个pattern的sdf才会生成新的sdf并放入mapSystem.out.println("put new sdf of pattern " + pattern + " to map");// 这里是关键,使用ThreadLocal<SimpleDateFormat>替代原来直接new SimpleDateFormattl = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {System.out.println("thread: " + Thread.currentThread() + " init pattern: " + pattern);return new SimpleDateFormat(pattern);}};sdfMap.put(pattern, tl);}}}return tl.get();}/*** 使用ThreadLocal<SimpleDateFormat>来获取SimpleDateFormat,这样每个线程只会有一个SimpleDateFormat* 如果新的线程中没有SimpleDateFormat,才会new一个* @param date* @param pattern* @return*/public static String format(Date date, String pattern) {return getSdf(pattern).format(date);}public static Date parse(String dateStr, String pattern) throws ParseException {return getSdf(pattern).parse(dateStr);}
}

4 使用DateTimeFormatter来进行日期转换。

public class Test {public static DateTimeFormatter sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 100; i++) {executorService.execute(new Runnable() {@Overridepublic  void run() {try {System.out.println(LocalDateTime.parse("2019-01-01 01:00:00",sdf));}catch (Exception e){e.printStackTrace();}}});}}
}

 

  相关解决方案