“Java DateFormat bukan threadsafe” apa yang menyebabkan hal ini?


143

Semua orang memperingatkan tentang Java DateFormat tidak aman dan saya mengerti konsepnya secara teoritis.

Tetapi saya tidak dapat memvisualisasikan masalah aktual apa yang dapat kita hadapi karena ini. Katakanlah, saya memiliki bidang DateFormat di kelas dan hal yang sama digunakan dalam metode yang berbeda di kelas (memformat tanggal) di lingkungan multi-utas.

Apakah ini akan menyebabkan:

  • pengecualian seperti pengecualian format
  • perbedaan dalam data
  • ada masalah lain?

Juga, tolong jelaskan mengapa.


1
Inilah yang mengarah ke: stackoverflow.com/questions/14309607/…
caw

Sekarang tahun 2020. Menjalankan tes saya (secara paralel) menemukan bahwa tanggal dari satu utas dikembalikan dengan santai ketika utas lainnya mencoba memformat tanggal. Butuh waktu beberapa minggu untuk menyelidiki apa yang tergantung, sampai ditemukan dalam formatter bahwa konstruktor membuat instance kalender, dan kalender kemudian dikonfigurasi untuk mengambil tanggal yang kita format. Apakah masih tahun 1990 di kepala mereka? Siapa tahu.
Vlad Patryshev

Jawaban:


263

Mari kita coba.

Berikut adalah program di mana banyak utas menggunakan yang dibagikan SimpleDateFormat.

Program :

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

Jalankan ini beberapa kali dan Anda akan melihat:

Pengecualian :

Berikut ini beberapa contoh:

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)

Hasil yang salah :

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

Hasil yang benar :

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

Pendekatan lain untuk menggunakan DateFormats dengan aman di lingkungan multi-utas adalah menggunakan ThreadLocalvariabel untuk memegang DateFormat objek, yang berarti bahwa setiap utas akan memiliki salinannya sendiri dan tidak perlu menunggu utas lain untuk melepaskannya. Begini caranya:

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

Ini adalah posting yang bagus dengan detail lebih lanjut.


1
Saya suka jawaban ini :-)
Sundararaj Govindasamy

Saya pikir alasan mengapa hal ini membuat frustasi bagi pengembang adalah bahwa pada pandangan pertama, sepertinya itu harus menjadi panggilan fungsi 'berorientasi fungsional'. Misalnya untuk input yang sama, saya mengharapkan output yang sama (bahkan jika beberapa utas menyebutnya). Jawabannya saya percaya datang ke pengembang Java tidak memiliki penghargaan untuk FOP pada saat mereka menulis logika waktu asli tanggal. Jadi pada akhirnya, kita hanya mengatakan "tidak ada alasan mengapa seperti ini selain salah".
Lezorte

30

Saya mengharapkan korupsi data - mis. Jika Anda menguraikan dua tanggal pada saat yang sama, Anda dapat membuat satu panggilan terpolusi oleh data dari yang lain.

Sangat mudah untuk membayangkan bagaimana ini bisa terjadi: parsing sering melibatkan mempertahankan sejumlah keadaan seperti apa yang telah Anda baca sejauh ini. Jika dua utas sama-sama menginjak-injak pada kondisi yang sama, Anda akan mendapatkan masalah. Misalnya, DateFormatmemperlihatkan calendarbidang jenis Calendar, dan melihat kode SimpleDateFormat, beberapa metode panggilan calendar.set(...)dan panggilan lain calendar.get(...). Ini jelas bukan thread-safe.

Saya belum melihat ke dalam tepat rincian mengapa DateFormattidak thread-safe, tapi bagi saya itu cukup untuk mengetahui bahwa itu adalah aman tanpa sinkronisasi - sopan santun yang tepat dari non-keamanan bahkan bisa mengubah antara rilis.

Secara pribadi saya akan menggunakan parser dari Joda Waktu bukan, karena mereka adalah thread aman - dan Joda Waktu adalah tanggal dan waktu yang lebih baik banyak API untuk memulai dengan :)


1
+1 jodatime dan sonar untuk menegakkan penggunaannya: mestachs.wordpress.com/2012/03/17/…
mestachs

18

Jika Anda menggunakan Java 8 maka Anda dapat menggunakan DateTimeFormatter.

Pemformat yang dibuat dari suatu pola dapat digunakan sebanyak yang diperlukan, itu tidak dapat diubah dan aman dari benang.

Kode:

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

Keluaran:

2017-04-17

10

Secara kasar, Anda tidak boleh mendefinisikan DateFormatvariabel instance dari objek yang diakses oleh banyak utas, atau static.

Format tanggal tidak disinkronkan. Disarankan untuk membuat instance format terpisah untuk setiap utas.

Jadi, jika Anda Foo.handleBar(..)diakses oleh banyak utas, alih-alih:

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

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

kamu harus menggunakan:

public class Foo {

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

Juga, dalam semua kasus, tidak punya static DateFormat

Seperti dicatat oleh Jon Skeet, Anda dapat memiliki variabel instance statis dan variabel bersama jika Anda melakukan sinkronisasi eksternal (mis. Gunakan synchronizedsekitar panggilan ke DateFormat)


2
Saya tidak melihat bahwa itu mengikuti sama sekali. Saya tidak membuat sebagian besar tipe saya thread-safe, jadi saya tidak berharap variabel instan mereka menjadi thread-safe juga, tentu saja. Lebih masuk akal untuk mengatakan bahwa Anda tidak boleh menyimpan DateFormat dalam variabel statis - atau jika Anda melakukannya, Anda perlu sinkronisasi.
Jon Skeet

1
Itu umumnya lebih baik - meskipun tidak masalah jika memiliki DateFormat statis jika Anda melakukan sinkronisasi. Itu mungkin berkinerja lebih baik dalam banyak kasus daripada membuat yang baru SimpleDateFormatsangat sering. Itu akan tergantung pada pola penggunaan.
Jon Skeet

1
Bisakah Anda jelaskan bagaimana dan mengapa instance statis dapat menyebabkan masalah dalam lingkungan multi-threaded?
Alexandr

4
karena ia menyimpan perhitungan menengah dalam variabel instan, dan itu tidak aman
Bozho

2

Format tanggal tidak disinkronkan. Disarankan untuk membuat instance format terpisah untuk setiap utas. Jika beberapa utas mengakses suatu format secara bersamaan, itu harus disinkronkan secara eksternal.

Ini berarti misalkan Anda memiliki objek DateFormat dan Anda mengakses objek yang sama dari dua utas yang berbeda dan Anda memanggil metode format pada objek yang kedua utas tersebut akan masukkan pada metode yang sama pada waktu yang sama pada objek yang sama sehingga Anda dapat memvisualisasikannya memenangkan akan menghasilkan hasil yang tepat

Jika Anda harus bekerja dengan DateFormat, maka Anda harus melakukan sesuatu

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

1

Data rusak. Kemarin saya perhatikan di program multithread saya di mana saya punya DateFormatobjek statis dan memanggil format()nilai-nilainya untuk dibaca melalui JDBC. Saya memiliki pernyataan pilih SQL di mana saya membaca tanggal yang sama dengan nama yang berbeda ( SELECT date_from, date_from AS date_from1 ...). Pernyataan seperti itu digunakan dalam 5 utas untuk berbagai tanggal dalam WHEREpetunjuk. Tanggal tampak "normal" tetapi nilainya berbeda - sementara semua tanggal berasal dari tahun yang sama hanya bulan dan hari yang berubah.

Jawaban lain menunjukkan cara untuk menghindari korupsi seperti itu. Saya membuat saya DateFormattidak statis, sekarang adalah anggota kelas yang memanggil pernyataan SQL. Saya juga menguji versi statis dengan sinkronisasi. Keduanya bekerja dengan baik tanpa perbedaan kinerja.


1

Spesifikasi Format, NumberFormat, DateFormat, MessageFormat, dll. Tidak dirancang untuk aman utas. Juga, metode parse memanggil Calendar.clone()metode dan itu mempengaruhi jejak kalender sehingga banyak urutan penguraian secara bersamaan akan mengubah kloning instance Kalender.

Untuk lebih lanjut, ini adalah laporan bug seperti ini dan ini , dengan hasil masalah keamanan thread DateFormat.


1

Dalam jawaban terbaik, dogbane memberi contoh penggunaan parsefungsi dan apa tujuannya. Di bawah ini adalah kode yang memungkinkan Anda memeriksa formatfungsi.

Perhatikan bahwa jika Anda mengubah jumlah pelaksana (utas bersamaan) Anda akan mendapatkan hasil yang berbeda. Dari eksperimen saya:

  • Biarkan newFixedThreadPooldiatur ke 5 dan loop akan gagal setiap saat.
  • Set ke 1 dan loop akan selalu bekerja (jelas karena semua tugas sebenarnya dijalankan satu per satu)
  • Set ke 2 dan loop hanya memiliki sekitar 6% peluang bekerja.

Saya menduga YMMV tergantung pada prosesor Anda.

The formatFungsi gagal dengan memformat waktu dari benang yang berbeda. Ini karena formatfungsi internal menggunakan calendarobjek yang diatur pada awal formatfungsi. Dan calendarobjeknya adalah properti SimpleDateFormatkelas. Mendesah...

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

Jika ada beberapa utas yang memanipulasi / mengakses instance DateFormat tunggal dan sinkronisasi tidak digunakan, dimungkinkan untuk mendapatkan hasil yang diacak. Itu karena beberapa operasi non-atom dapat mengubah keadaan atau melihat memori tidak konsisten.


0

Ini adalah kode sederhana saya yang menunjukkan DateFormat tidak aman.

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

Karena semua utas menggunakan objek SimpleDateFormat yang sama, ia melempar pengecualian berikut.

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)

Tetapi jika kita melewatkan objek berbeda ke utas berbeda, kode tersebut berjalan tanpa kesalahan.

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

Inilah hasilnya.

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 bertanya mengapa ini terjadi dan juga apa.
Adam

0

Ini akan menyebabkan ArrayIndexOutOfBoundsException

Terlepas dari hasil yang salah, itu akan memberi Anda crash dari waktu ke waktu. Itu tergantung pada kecepatan mesin Anda; di laptop saya, itu terjadi sekali dalam 100.000 panggilan rata-rata:

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

baris terakhir dapat memicu pengecualian eksekutor yang ditunda:

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)
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.