SimpleDateFormat 线程安全的问题解析

1,使用Static 关键字容易出现的问题

SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用, 并且, 观察 sdf.parse() 方法,你会发现有如下的调用:

Date parse() {

  calendar.clear(); // 清理calendar

  ... // 执行一些操作, 设置 calendar 的日期什么的

  calendar.getTime(); // 获取calendar的时间

}

2 ,使用 sdf.Parse() 后 出现线程不安全的问题。
这里会导致的问题就是, 如果 线程A 调用了 sdf.parse(), 并且进行了 calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用sdf.parse()并顺利i结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


public class DateFormatTest extends Thread {
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    private String name;
    private String dateStr;
    private boolean sleep;

    public DateFormatTest(String name, String dateStr, boolean sleep) {
        this.name = name;
        this.dateStr = dateStr;
        this.sleep = sleep;
    }

    @Override
    public void run() {

        Date date = null;

        if (sleep) {
            try {
                TimeUnit.MILLISECONDS.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            date = sdf.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        System.out.println(name + " : date: " + date);
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executor = Executors.newCachedThreadPool();

        // A 会sleep 2s 后开始执行sdf.parse()
        executor.execute(new DateFormatTest("A", "1991-09-13", true));
        // B 打了断点,会卡在方法中间
        executor.execute(new DateFormatTest("B", "2013-09-13", false));

        executor.shutdown();
    }
}

****3. 解决方案

最简单的解决方案我们可以把static去掉,这样每个新的线程都会有一个自己的sdf实例,从而避免线程安全的问题

然而,使用这种方法,在高并发的情况下会大量的new sdf以及销毁sdf,这样是非常耗费资源的

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

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重复的sdf
        if (tl == null) {
            synchronized (lockObj) {
                tl = sdfMap.get(pattern);
                if (tl == null) {
                    // 只有Map中还没有这个pattern的sdf才会生成新的sdf并放入map
                    System.out.println("put new sdf of pattern " + pattern + " to map");

                    // 这里是关键,使用ThreadLocal<SimpleDateFormat>替代原来直接new SimpleDateFormat
                    tl = new ThreadLocal<SimpleDateFormat>() {

                        @Override
                        protected 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
     * 
     * @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);
    }

}

使用第三方的日期处理函数:
比如 JODA 来避免这些问题,你也可以使用 commons-lang 包中的 FastDateFormat 工具类。

加一把线程同步锁:synchronized(lock) 性能较差,每次都要等待锁释放后其他线程才能进入

public class SyncDateFormatTest {
private static SimpleDateFormat sdf = new SimpleDateFormat(“dd-MMM-yyyy”, Locale.US);
private static String date[] = { “01-Jan-1999”, “01-Jan-2000”, “01-Jan-2001” };

public static void main(String[] args) {
    for (int i = 0; i < date.length; i++) {
        final int temp = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        synchronized (sdf) {
                            String str1 = date[temp];
                            Date date = sdf.parse(str1);
                            String str2 = sdf.format(date);
                            System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2);
                            if(!str1.equals(str2)){
                                throw new RuntimeException(Thread.currentThread().getName() 
                                        + ", Expected " + str1 + " but got " + str2);
                            }
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException("parse failed", e);
                }
            }
        }).start();
    }
}

}

使用ThreadLocal: 每个线程都将拥有自己的SimpleDateFormat对象副本。(比较好)

public class DateUtil {
private static ThreadLocal local = new ThreadLocal();

public static Date parse(String str) throws Exception {
    SimpleDateFormat sdf = local.get();
    if (sdf == null) {
        sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
        local.set(sdf);
    }
    return sdf.parse(str);
}

public static String format(Date date) throws Exception {
    SimpleDateFormat sdf = local.get();
    if (sdf == null) {
        sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
        local.set(sdf);
    }
    return sdf.format(date);
}

}

测试代码:
public class ThreadLocalDateFormatTest {
private static String date[] = { “01-Jan-1999”, “01-Jan-2000”, “01-Jan-2001” };

public static void main(String[] args) {
    for (int i = 0; i < date.length; i++) {
        final int temp = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        String str1 = date[temp];
                        Date date = DateUtil.parse(str1);
                        String str2 = DateUtil.format(date);
                        System.out.println(str1 + "," + str2);
                        if(!str1.equals(str2)){
                            throw new RuntimeException(Thread.currentThread().getName() 
                                    + ", Expected " + str1 + " but got " + str2);
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException("parse failed", e);
                }
            }
        }).start();
    }
}

}

参考文档:https://my.oschina.net/leejun2005/blog/152253
https://www.cnblogs.com/zemliu/archive/2013/08/29/3290585.html
https://blog.csdn.net/zdp072/article/details/41044059

猜你喜欢

转载自blog.csdn.net/fight_man8866/article/details/81507996