Nhật ký Java DateFormat không phải là chủ đề an toàn Điều này dẫn đến điều gì?


143

Mọi người cảnh báo về Java DateFormat không phải là chủ đề an toàn và tôi hiểu khái niệm này về mặt lý thuyết.

Nhưng tôi không thể hình dung được những vấn đề thực tế mà chúng ta có thể gặp phải do vấn đề này. Giả sử, tôi đã có trường DateFormat trong một lớp và tương tự được sử dụng trong các phương thức khác nhau trong lớp (định dạng ngày) trong môi trường đa luồng.

Điều này sẽ gây ra:

  • bất kỳ ngoại lệ như ngoại lệ định dạng
  • sự khác biệt trong dữ liệu
  • còn vấn đề nào khác không?

Ngoài ra, hãy giải thích tại sao.


1
Đây là những gì nó dẫn đến: stackoverflow.com/questions/14309607/ từ
caw

Bây giờ là năm 2020. Chạy thử nghiệm của tôi (song song) phát hiện ra rằng một ngày từ một luồng được trả lại ngẫu nhiên khi một luồng khác đang cố định dạng một ngày. Mất vài tuần để điều tra xem nó phụ thuộc vào điều gì, cho đến khi tìm thấy trong một trình định dạng rằng hàm tạo khởi tạo lịch và lịch sau đó được định cấu hình để lấy ngày chúng tôi định dạng. Có phải vẫn là năm 1990 trong đầu của họ? Ai biết.
Vlad Patryshev

Câu trả lời:


262

Hãy thử xem.

Đây là một chương trình trong đó nhiều chủ đề sử dụng chia sẻ SimpleDateFormat.

Chương trình :

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

Chạy nó vài lần và bạn sẽ thấy:

Ngoại lệ :

Đây là vài ví dụ:

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)

Kết quả không chính xác :

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

Kết quả đúng :

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

Một cách tiếp cận khác để sử dụng DateFormats một cách an toàn trong môi trường đa luồng là sử dụng một ThreadLocalbiến để giữ DateFormat đối tượng, điều đó có nghĩa là mỗi luồng sẽ có bản sao riêng và không cần phải đợi các luồng khác phát hành. Đây là cách:

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

Đây là một bài viết tốt với nhiều chi tiết hơn.


1
Tôi thích câu trả lời này :-)
Sundararaj Govindasamy

Tôi nghĩ lý do tại sao điều này gây khó chịu cho các nhà phát triển là vì thoạt nhìn, có vẻ như đó phải là một cuộc gọi chức năng 'định hướng theo chức năng'. Ví dụ, cho cùng một đầu vào, tôi mong đợi cùng một đầu ra (ngay cả khi nhiều luồng gọi nó). Câu trả lời tôi tin rằng các nhà phát triển của Java không đánh giá cao FOP tại thời điểm họ viết logic thời gian ban đầu. Vì vậy, cuối cùng, chúng tôi chỉ nói "không có lý do tại sao nó lại như thế này ngoài việc nó sai".
Lezorte

30

Tôi sẽ mong muốn tham nhũng dữ liệu - ví dụ: nếu bạn phân tích hai ngày cùng một lúc, bạn có thể có một cuộc gọi bị ô nhiễm bởi dữ liệu từ người khác.

Thật dễ dàng để tưởng tượng điều này có thể xảy ra như thế nào: phân tích cú pháp thường liên quan đến việc duy trì một lượng trạng thái nhất định như những gì bạn đã đọc cho đến nay. Nếu hai luồng bị chà đạp trên cùng một trạng thái, bạn sẽ gặp vấn đề. Ví dụ, DateFormathiển thị một calendartrường loại Calendarvà xem mã của SimpleDateFormat, một số phương thức gọi calendar.set(...)và phương thức khác gọi calendar.get(...). Điều này rõ ràng là không an toàn chủ đề.

Tôi đã không nhìn vào chính xác các chi tiết về việc tại sao DateFormatkhông phải là thread-an toàn, nhưng đối với tôi nó là đủ để biết rằng nó không an toàn mà không cần đồng bộ hóa - cách cư xử chính xác của phi an toàn thậm chí có thể thay đổi giữa các phiên bản.

Cá nhân tôi sẽ sử dụng các trình phân tích cú pháp từ Joda Time , vì chúng luồng an toàn - và Joda Time là API ngày và giờ tốt hơn nhiều để bắt đầu với :)


1
+1 jodatime và sonar để thực thi việc sử dụng nó: mestachs.wordpress.com/2012/03/17/ mẹo
mestachs

18

Nếu bạn đang sử dụng Java 8 thì bạn có thể sử dụng DateTimeFormatter.

Một định dạng được tạo từ một mẫu có thể được sử dụng nhiều lần nếu cần, nó là bất biến và an toàn cho luồng.

Mã số:

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

Đầu ra:

2017-04-17

10

Roughly, rằng bạn không nên định nghĩa một DateFormatbiến thể hiện của một đối tượng được truy cập bởi nhiều luồng, hoặc static.

Định dạng ngày không được đồng bộ. Nên tạo các thể hiện định dạng riêng cho từng luồng.

Vì vậy, trong trường hợp của bạn Foo.handleBar(..)được truy cập bởi nhiều luồng, thay vì:

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

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

bạn nên sử dụng:

public class Foo {

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

Ngoài ra, trong mọi trường hợp, không có static DateFormat

Như Jon Skeet đã lưu ý, bạn có thể có cả hai biến đối tượng tĩnh và biến chung trong trường hợp bạn thực hiện đồng bộ hóa bên ngoài (nghĩa là sử dụng synchronizedxung quanh các cuộc gọi đến DateFormat)


2
Tôi không thấy điều đó theo sau cả. Tôi không làm cho hầu hết các loại của tôi an toàn theo luồng, vì vậy tôi cũng không hy vọng các biến đối tượng của chúng sẽ an toàn theo luồng. Sẽ hợp lý hơn khi nói rằng bạn không nên lưu trữ DateFormat trong một biến tĩnh - hoặc nếu có, bạn sẽ cần đồng bộ hóa.
Jon Skeet

1
Điều đó thường tốt hơn - mặc dù sẽ ổn khi có DateFormat tĩnh nếu bạn đã đồng bộ hóa. Điều đó có thể thực hiện tốt trong nhiều trường hợp hơn là tạo ra một cái mới SimpleDateFormatrất thường xuyên. Nó sẽ phụ thuộc vào mô hình sử dụng.
Jon Skeet

1
Bạn có thể giải thích làm thế nào và tại sao cá thể tĩnh có thể gây ra vấn đề trong môi trường đa luồng không?
Alexandr

4
bởi vì nó lưu trữ các phép tính trung gian trong các biến thể hiện và điều đó không an toàn cho
chuỗi

2

Định dạng ngày không được đồng bộ. Nên tạo các thể hiện định dạng riêng cho từng luồng. Nếu nhiều luồng truy cập một định dạng đồng thời, nó phải được đồng bộ hóa bên ngoài.

Điều này có nghĩa là giả sử bạn có một đối tượng DateFormat và bạn đang truy cập cùng một đối tượng từ hai luồng khác nhau và bạn đang gọi phương thức định dạng trên đối tượng đó, cả hai luồng sẽ nhập vào cùng một phương thức cùng một lúc trên cùng một đối tượng để bạn có thể hình dung nó đã thắng Kết quả không đúng

Nếu bạn phải làm việc với DateFormat thì bạn nên làm gì đó

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

1

Dữ liệu bị hỏng. Hôm qua tôi đã nhận thấy nó trong chương trình đa luồng của tôi, nơi tôi có DateFormatđối tượng tĩnh và gọi nó format()cho các giá trị được đọc qua JDBC. Tôi đã có câu lệnh SQL chọn trong đó tôi đọc cùng ngày với các tên khác nhau ( SELECT date_from, date_from AS date_from1 ...). Những tuyên bố như vậy đã được sử dụng trong 5 chủ đề cho các ngày khác nhau trong WHEREclasue. Ngày trông "bình thường" nhưng chúng khác nhau về giá trị - trong khi tất cả các ngày là từ cùng một năm chỉ thay đổi tháng và ngày.

Những câu trả lời khác chỉ cho bạn cách để tránh tham nhũng như vậy. Tôi đã làm cho tôi DateFormatkhông tĩnh, bây giờ nó là một thành viên của một lớp gọi các câu lệnh SQL. Tôi đã thử nghiệm phiên bản tĩnh với đồng bộ hóa. Cả hai đều hoạt động tốt mà không có sự khác biệt trong hiệu suất.


1

Các thông số kỹ thuật của Format, NumberFormat, DateFormat, MessageFormat, v.v. không được thiết kế để an toàn cho chuỗi. Ngoài ra, phương thức phân tích cú pháp gọi Calendar.clone()phương thức và nó ảnh hưởng đến dấu chân lịch nên nhiều luồng phân tích cú pháp đồng thời sẽ thay đổi nhân bản của phiên bản Lịch.

Để biết thêm, đây là các báo cáo lỗi như thế nàyđây , với kết quả của vấn đề an toàn luồng DateFormat.


1

Trong câu trả lời tốt nhất, dogbane đã đưa ra một ví dụ về việc sử dụng parsechức năng và những gì nó dẫn đến. Dưới đây là một mã cho phép bạn kiểm tra formatchức năng.

Lưu ý rằng nếu bạn thay đổi số lượng người thi hành (các luồng đồng thời), bạn sẽ nhận được các kết quả khác nhau. Từ thí nghiệm của tôi:

  • Đặt newFixedThreadPoolthành 5 và vòng lặp sẽ thất bại mỗi lần.
  • Đặt thành 1 và vòng lặp sẽ luôn hoạt động (rõ ràng vì tất cả các tác vụ thực sự được chạy từng cái một)
  • Đặt thành 2 và vòng lặp chỉ có khoảng 6% cơ hội làm việc.

Tôi đoán YMMV tùy thuộc vào bộ xử lý của bạn.

Các formatchức năng thất bại bằng cách định dạng thời gian từ một chủ đề khác nhau. Điều này là do formathàm bên trong đang sử dụng calendarđối tượng được thiết lập khi bắt đầu formathàm. Và calendarđối tượng là một tài sản của SimpleDateFormatlớp. Thở dài...

/**
 * 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

Nếu có nhiều luồng thao tác / truy cập vào một cá thể DateFormat và đồng bộ hóa không được sử dụng, có thể nhận được kết quả được xáo trộn. Đó là bởi vì nhiều hoạt động phi nguyên tử có thể thay đổi trạng thái hoặc nhìn thấy bộ nhớ không nhất quán.


0

Đây là mã đơn giản của tôi cho thấy DateFormat không phải là luồng an toàn.

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

Vì tất cả các luồng đang sử dụng cùng một đối tượng SimpleDateFormat, nó sẽ ném ngoại lệ sau.

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)

Nhưng nếu chúng ta chuyển các đối tượng khác nhau cho các luồng khác nhau, mã sẽ chạy mà không gặp lỗi.

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

Đây là những kết quả.

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 hỏi tại sao điều này xảy ra cũng như những gì.
Adam

0

Điều này sẽ gây ra ArrayIndexOutOfBoundsException

Ngoài kết quả không chính xác, nó sẽ cung cấp cho bạn một sự cố theo thời gian. Nó phụ thuộc vào tốc độ máy của bạn; trong máy tính xách tay của tôi, trung bình một lần trong 100.000 cuộc gọi:

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();

dòng cuối cùng sẽ kích hoạt ngoại lệ thực thi bị hoãn:

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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.