“ Java DateFormat不是线程安全的”这会导致什么?


143

每个人都对Java DateFormat并不是线程安全的警告,我从理论上理解了这个概念。

但是我无法想象由此导致的实际问题。假设我在一个类中有一个DateFormat字段,并且在多线程环境中该类的不同方法(格式化日期)中使用了相同的字段。

这会导致:

  • 任何异常,例如格式异常
  • 数据差异
  • 还有其他问题吗?

另外,请解释原因。


1
这就是它导致的结果:stackoverflow.com/questions/14309607/…–
caw

现在是2020年。运行我的测试(并行)发现,当另一个线程尝试格式化日期时,会随意返回一个线程的日期。我花了两周时间研究它的依赖关系,直到在格式化程序中发现构造函数实例化了日历,然后将日历配置为采用我们格式化的日期。他们的脑海中仍然是1990年吗?谁知道。
弗拉德·帕特里谢夫

Answers:


262

让我们尝试一下。

这是多个线程使用共享程序的程序SimpleDateFormat

程序

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

    final DateFormat format = new SimpleDateFormat("yyyyMMdd");

    Callable<Date> task = new Callable<Date>(){
        public Date call() throws Exception {
            return format.parse("20101022");
        }
    };

    //pool with 5 threads
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<Date>> results = new ArrayList<Future<Date>>();

    //perform 10 date conversions
    for(int i = 0 ; i < 10 ; i++){
        results.add(exec.submit(task));
    }
    exec.shutdown();

    //look at the results
    for(Future<Date> result : results){
        System.out.println(result.get());
    }
}

运行几次,您将看到:

例外情况

这里有一些例子:

1。

Caused by: java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
    at java.lang.Long.parseLong(Long.java:431)
    at java.lang.Long.parseLong(Long.java:468)
    at java.text.DigitList.getLong(DigitList.java:177)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1298)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)

2。

Caused by: java.lang.NumberFormatException: For input string: ".10201E.102014E4"
    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)

3。

Caused by: java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1084)
    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:1936)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)

错误的结果

Sat Oct 22 00:00:00 BST 2011
Thu Jan 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Thu Oct 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

正确的结果

Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

在多线程环境中安全使用DateFormat的另一种方法是使用 ThreadLocal变量保存DateFormat 对象,这意味着每个线程将拥有自己的副本,而无需等待其他线程释放它。这是这样的:

public class DateFormatTest {

  private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyyMMdd");
    }
  };

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

这是一篇不错的文章,其中有更多详细信息。


1
我喜欢这个答案:-)
Sundararaj Govindasamy

我认为令开发人员如此沮丧的原因是,乍一看,它似乎应该是“面向功能”的函数调用。例如对于相同的输入,我期望相同的输出(即使有多个线程调用它)。我认为答案归结于Java开发人员在编写原始日期时间逻辑时对FOP并不了解。因此,最后,我们只说“没有其他理由,只是错了”。
Lezorte

30

我希望数据损坏-例如,如果您同时解析两个日期,则一个呼叫可能被另一个数据污染。

很难想象这是怎么发生的:解析通常涉及维护到目前为止您所读内容的一定数量的状态。如果两个线程都在同一状态下践踏,则会遇到问题。例如,DateFormat公开一个calendar类型为type 的字段Calendar,并查看其中的代码SimpleDateFormat,其中一些方法调用,calendar.set(...)而另一些方法调用calendar.get(...)。这显然不是线程安全的。

我还没有研究过为什么不是线程安全的确切细节DateFormat,但是对我来说,足以知道如果没有同步它不安全的-不安全的确切方式甚至可能在版本之间发生变化。

就我个人而言,我将使用来自Joda Time的解析器,因为它们线程安全的-并且Joda Time是一个更好的日期和时间API:


1
1 jodatime和声纳执行其用法:mestachs.wordpress.com/2012/03/17/...
mestachs

18

如果您使用的是Java 8,则可以使用DateTimeFormatter

从模式创建的格式化程序可以根据需要多次使用,它是不可变的并且是线程安全的。

码:

LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String text = date.format(formatter);
System.out.println(text);

输出:

2017-04-17

10

粗略地说,您不应该将DateFormat被许多线程访问的对象的实例变量定义为或static

日期格式不同步。建议为每个线程创建单独的格式实例。

因此,以防您Foo.handleBar(..)被多个线程访问,而不是:

public class Foo {
    private DateFormat df = new SimpleDateFormat("dd/mm/yyyy");

    public void handleBar(Bar bar) {
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

您应该使用:

public class Foo {

    public void handleBar(Bar bar) {
        DateFormat df = new SimpleDateFormat("dd/mm/yyyy");
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

另外,在所有情况下, static DateFormat

正如Jon Skeet所指出的那样,如果您执行外部同步(例如,在synchronized周围使用调用DateFormat),则可以同时拥有静态实例变量和共享实例变量。


2
我完全看不到。我也不使我的大多数类型都是线程安全的,因此我也不希望它们的实例变量也不一定是线程安全的。更合理地说,您不应该将DateFormat存储在静态变量中-否则,您将需要同步。
乔恩·斯基特

1
通常更好-尽管如果您进行同步,则可以使用静态DateFormat 。在很多情况下,这可能比SimpleDateFormat经常创建一个新的更好。这将取决于使用模式。
乔恩·斯基特

1
您能否解释一下为什么静态实例会在多线程环境中引起问题?
2012年

4
因为它会将中间计算结果存储在实例变量中,所以它不是线程安全的
Bozho 2012

2

日期格式不同步。建议为每个线程创建单独的格式实例。如果多个线程同时访问一种格式,则必须在外部进行同步。

这意味着假设您有一个DateFormat对象,并且您正在从两个不同的线程访问同一对象,并且正在对该对象调用format方法,那么这两个线程将在同一时间在同一对象上同时输入同一个方法,因此您可以直观地看到它结果不正确

如果必须使用DateFormat,那么应该怎么做

public synchronized myFormat(){
// call here actual format method
}

1

数据已损坏。昨天,我在多线程程序中注意到了这一点,该程序中有静态DateFormat对象,并format()通过JDBC 调用它的值。我有SQL select语句,在其中读取具有不同名称的相同日期(SELECT date_from, date_from AS date_from1 ...)。这样的语句在5个线程中使用了不同的日期WHERE。日期看起来“正常”,但它们的价值有所不同-虽然所有日期都来自同一年,但日期和月份都发生了变化。

其他答案向您展示了避免此类损坏的方法。我使我DateFormat不是静态的,现在它是调用SQL语句的类的成员。我还测试了带有同步的静态版本。两者都运行良好,性能没有差异。


1

Format,NumberFormat,DateFormat,MessageFormat等的规范并不是为了线程安全而设计的。同样,parse方法调用Calendar.clone()method,它影响日历的大小,因此许多同时进行解析的线程将更改Calendar实例的克隆。

更多信息是这些错误报告,例如thisthis,以及DateFormat线程安全问题的结果。


1

在最好的答案中,dogbane给出了使用parse函数及其结果的示例。下面是让您检查format功能的代码。

请注意,如果更改执行程序(并发线程)的数量,则会得到不同的结果。根据我的实验:

  • 离开newFixedThreadPool设定为5,循环会失败每次。
  • 设置为1,循环将始终有效(显然,因为所有任务实际上都是一个接一个地运行)
  • 设置为2,循环只有6%的工作机会。

我猜YMMV取决于您的处理器。

format函数通过格式化来自其他线程的时间而失败。这是因为内部format函数正在使用calendarformat函数开始时设置的对象。而calendar对象是一个属性SimpleDateFormat类。叹...

/**
 * Test SimpleDateFormat.format (non) thread-safety.
 *
 * @throws Exception
 */
private static void testFormatterSafety() throws Exception {
    final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    final Calendar calendar1 = new GregorianCalendar(2013,1,28,13,24,56);
    final Calendar calendar2 = new GregorianCalendar(2014,1,28,13,24,56);
    String expected[] = {"2013-02-28 13:24:56", "2014-02-28 13:24:56"};

    Callable<String> task1 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "0#" + format.format(calendar1.getTime());
        }
    };
    Callable<String> task2 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "1#" + format.format(calendar2.getTime());
        }
    };

    //pool with X threads
    // note that using more then CPU-threads will not give you a performance boost
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<String>> results = new ArrayList<>();

    //perform some date conversions
    for (int i = 0; i < 1000; i++) {
        results.add(exec.submit(task1));
        results.add(exec.submit(task2));
    }
    exec.shutdown();

    //look at the results
    for (Future<String> result : results) {
        String answer = result.get();
        String[] split = answer.split("#");
        Integer calendarNo = Integer.parseInt(split[0]);
        String formatted = split[1];
        if (!expected[calendarNo].equals(formatted)) {
            System.out.println("formatted: " + formatted);
            System.out.println("expected: " + expected[calendarNo]);
            System.out.println("answer: " + answer);
            throw new Exception("formatted != expected");
        /**
        } else {
            System.out.println("OK answer: " + answer);
        /**/
        }
    }
    System.out.println("OK: Loop finished");
}

0

如果有多个线程在操作/访问单个DateFormat实例,并且未使用同步,则可能会得到混乱的结果。那是因为多个非原子操作可能会改变状态或不一致地看到内存。


0

这是我的简单代码,显示DateFormat不是线程安全的。

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

public class DateTimeChecker {
    static DateFormat df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
    public static void main(String args[]){
       String target1 = "Thu Sep 28 20:29:30 JST 2000";
       String target2 = "Thu Sep 28 20:29:30 JST 2001";
       String target3 = "Thu Sep 28 20:29:30 JST 2002";
       runThread(target1);
       runThread(target2);
       runThread(target3);
   }
   public static void runThread(String target){
       Runnable myRunnable = new Runnable(){
          public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() + "  " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
     }
}

由于所有线程都使用相同的SimpleDateFormat对象,因此它将引发以下异常。

Exception in thread "Thread-0" Exception in thread "Thread-2" Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)

但是,如果我们将不同的对象传递给不同的线程,则代码将运行而不会出错。

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

public class DateTimeChecker {
    static DateFormat df;
    public static void main(String args[]){
       String target1 = "Thu Sep 28 20:29:30 JST 2000";
       String target2 = "Thu Sep 28 20:29:30 JST 2001";
       String target3 = "Thu Sep 28 20:29:30 JST 2002";
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target1, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target2, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target3, df);
   }
   public static void runThread(String target, DateFormat df){
      Runnable myRunnable = new Runnable(){
        public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() + "  " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
   }
}

这些就是结果。

Thread-0  Thu Sep 28 17:29:30 IST 2000
Thread-2  Sat Sep 28 17:29:30 IST 2002
Thread-1  Fri Sep 28 17:29:30 IST 2001

OP询问为什么会发生这种情况以及发生了什么。
亚当

0

这将导致 ArrayIndexOutOfBoundsException

除了错误的结果外,它还会使您不时崩溃。这取决于您的机器速度。在我的笔记本电脑中,平均发生100,000次呼叫一次:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<?> future1 = executorService.submit(() -> {
  for (int i = 0; i < 99000; i++) {
    sdf.format(Date.from(LocalDate.parse("2019-12-31").atStartOfDay().toInstant(UTC)));
  }
});

executorService.submit(() -> {
  for (int i = 0; i < 99000; i++) {
    sdf.format(Date.from(LocalDate.parse("2020-04-17").atStartOfDay().toInstant(UTC)));
  }
});

future1.get();

最后一行触发延迟的执行程序异常:

java.lang.ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 13
  at java.base/sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
  at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2394)
  at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2309)
  at java.base/java.util.Calendar.complete(Calendar.java:2301)
  at java.base/java.util.Calendar.get(Calendar.java:1856)
  at java.base/java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1150)
  at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:997)
  at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:967)
  at java.base/java.text.DateFormat.format(DateFormat.java:374)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.