多线程环境下使用 DateFormat,DecimalFormat


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

 

打开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<DateFormat> df = new ThreadLocal<DateFormat>() {

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

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM