多线程环境下使用 DateFormat

想必大家对SimpleDateFormat并不陌生。不过,你是否知道,SimpleDateFormat不是线程安全的(thread safe)。这意味着,下面的代码在多线程环境下运行结果并非如我们所愿 - 有时候,它输出正确的日期,有时候会输出错误的(例如.Tue Aug 11 00:00:00 CST 48201),有些时候甚至会抛出NumberFormatException!!!(当然,在单线程环境是,这段代码是完全没有问题的)

package com.uppower.test;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateFormatTest {
    private final DateFormat format = new SimpleDateFormat("yyyyMMdd");

    public Date convert(String source) throws ParseException {
        return format.parse(source);
    }
}

package com.uppower.test;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Test {
    
    public static void main(String[] args) throws Exception {
        final DateFormatTest t =new DateFormatTest();
        
        Callable task =new Callable(){
            public Date call()throws Exception {
                return t.convert("20100811");
            }
        };
         
        //尝试5个线程的情况
        ExecutorService exec = Executors.newFixedThreadPool(5);
        List> results = new ArrayList>();
         
        //实现100次日期转换
        for(int i =0; i <100; i++){
            results.add(exec.submit(task));
        }
        
        exec.shutdown();
         
        //查看结果
        for(Future result : results){
            System.out.println(result.get());
        }
    }

}

运行结果:

Wed Aug 11 00:00:00 CST 2010
Tue Aug 11 00:00:00 CST 48201
Wed Aug 11 00:00:00 CST 2010
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.NumberFormatException: For input string: "E.1118E1"
at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:222)
at java.util.concurrent.FutureTask.get(FutureTask.java:83)
at com.uppower.test.DateFormatTest.main(DateFormatTest.java:44)
Caused by: java.lang.NumberFormatException: For input string: "E.1118E1"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1224)
at java.lang.Double.parseDouble(Double.java:510)
at java.text.DigitList.getDouble(DigitList.java:151)
at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.uppower.test.DateFormatTest.convert(DateFormatTest.java:20)
at com.uppower.test.DateFormatTest$1.call(DateFormatTest.java:28)
at com.uppower.test.DateFormatTest$1.call(DateFormatTest.java:1)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
at java.util.concurrent.FutureTask.run(FutureTask.java:138)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)


接下来,我们看看为什么DateFormat不是线程安全的。

我们就要打开JDK的源码,在format方法里,有这样一段代码:

calendar.setTime(date);

其中,calendar是DateFormat的protected字段。这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。

想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:

  1. 线程1调用format方法,改变了calendar这个字段。
  2. 中断来了。
  3. 线程2开始执行,它也改变了calendar。
  4. 又中断了。
  5. 线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。
  6. BANG!!! 稍微花点时间分析一下format的实现,我们便不难发现,用到calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。

这个问题背后隐藏着一个更为重要的问题:无状态

无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。

所以,写程序,我们要尽量编写无状态方法。


那么,我们如何在多线程中的使用DateFormat呢?

1. 同步

最简单的方法就是在做日期转换之前,为DateFormat对象加锁。这种方法使得一次只能让一个线程访问DateFormat对象,而其他线程只能等待。

public class DateFormatTest {
    private final DateFormat format = new SimpleDateFormat("yyyyMMdd");
    
    public Date convert(String source) throws ParseException {
        synchronized(format) {
            return format.parse(source);
        }
    }
}

2. 使用ThreadLocal

另外一个方法就是使用ThreadLocal变量去容纳DateFormat对象,也就是说每个线程都有一个属于自己的副本,并无需等待其他线程去释放它。这种方法会比使用同步块更高效。

public class DateFormatTest {

    private final ThreadLocal df = new ThreadLocal() {

        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyyMMdd");
        }
    };

    public Date convert(String source) throws ParseException {
        return df.get().parse(source);
    }
}


3. Joda-Time

Joda-Time 是一个很棒的开源的 JDK 的日期和日历 API 的替代品,其 DateTimeFormat 是线程安全而且不变的。

package com.uppower.test;

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.Date;

public class DateFormatTest {
    private final DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyyMMdd");
        
    public Date convert(String source){
        DateTime d = fmt.parseDateTime(source);
        returnd.toDate();
    }
}

4.使用临时变量(不推荐)
 作为一个专业程序员,我们当然知道,相比于共享一个变量的开销要比每次创建小。创建一个实例来获取日期格式会比较高效,因为系统不需要多次获取本地语言和国家。

public class DateFormatTest {
    public Date convert(String source) throws ParseException {
        DateFormat format = new SimpleDateFormat("yyyyMMdd");
        return format.parse(source);
    }
}

你可能感兴趣的:(并发编程)