"Java DateFormat은 스레드 안전하지 않습니다"이것이 무엇을 야기합니까?


143

모든 사람들은 스레드가 안전하지 않은 Java DateFormat에 대해주의하고 이론적으로 개념을 이해합니다.

그러나 이로 인해 우리가 직면 할 수있는 실제 문제를 시각화 할 수 없습니다. 예를 들어, 클래스에 DateFormat 필드가 있고 멀티 스레드 환경에서 클래스의 다른 메소드 (서식 날짜)에 동일하게 사용됩니다.

이것이 원인입니까?

  • 형식 예외와 같은 예외
  • 데이터 불일치
  • 다른 문제?

또한 이유를 설명하십시오.



지금은 2020 년입니다. 테스트를 병렬로 실행하면 다른 스레드가 날짜를 형식화하려고 할 때 한 스레드의 날짜가 자연스럽게 반환된다는 것을 발견했습니다. 생성자가 달력을 인스턴스화하는 포맷터에서 발견 될 때까지 그리고 달력이 우리가 포맷하는 날짜를 갖도록 구성 될 때까지 몇 주가 걸리는지에 대해 조사해 보았습니다. 아직도 머리에 1990 년입니까? 누가 알아.
블라드 Patryshev

답변:


264

사용해 봅시다.

다음은 여러 스레드가 공유를 사용하는 프로그램입니다 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)

삼.

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

다중 스레드 환경에서 DateFormats를 안전하게 사용하는 또 다른 방법은 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

이것이 개발자에게 너무 실망스러운 이유는 언뜻보기에 '기능 지향적'기능 호출이어야한다고 생각하기 때문입니다. 예를 들어 동일한 입력의 경우 여러 스레드가 호출하더라도 동일한 출력을 기대합니다. 내가 생각하는 대답은 원래 날짜 시간 논리를 작성한 시점에서 FOP에 감사하지 않는 Java 개발자에게 발생합니다. 결국, 우리는 단지 "이것이 왜 틀린 것보다 다른 이유가 없다"고 말합니다.
Lezorte

30

데이터가 손상 될 수 있습니다. 예를 들어 두 날짜를 동시에 구문 분석하는 경우 한 통화가 다른 통화의 데이터로 오염 될 수 있습니다.

구문 분석에는 종종 지금까지 읽은 내용에 대해 일정량의 상태를 유지하는 것이 포함됩니다. 두 스레드가 모두 같은 상태에서 짓밟 히면 문제가 발생합니다. 예를 들어, 유형 DateFormatcalendar필드를 노출하고 의 메소드를 Calendar보고 SimpleDateFormat일부 메소드는 호출 calendar.set(...)하고 다른 메소드는 호출 calendar.get(...)합니다. 이것은 스레드로부터 안전하지 않습니다.

나는이에보고하지 않은 정확한 이유에 대한 자세한 DateFormat스레드로부터 안전하지 않습니다,하지만 나를 위해 그것이 알고 충분 하다 동기화없이 안전 - 비 안전의 정확한 매너도 릴리스간에 변경 될 수 있습니다.

개인적으로 난에서 파서 사용하는 것이 Joda 시간을 가로, 대신 있습니다 스레드 안전 - 그리고 Joda 시간에 시작하는 더 나은 날짜와 시간 API입니다 :)


1
사용을 강제하기 위해 +1 jodatime 및 sonar : 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

존 소총에 의해 언급 한 바와 같이, 당신은 정적 및 사례의 공유 인스턴스 변수는 외부 동기화 (즉, 사용 수행을 모두 할 수 있습니다 synchronized받는 전화의 주위를 DateFormat)


2
나는 그것이 따르는 것을 전혀 보지 못합니다. 필자는 대부분의 유형을 스레드로부터 안전하지 않기 때문에 인스턴스 변수가 스레드로부터 안전하다고 기대하지는 않습니다. 정적 변수에 DateFormat을 저장해서는 안된다고 말하는 것이 더 합리적입니다. 그렇지 않으면 동기화가 필요합니다.
Jon Skeet

1
그게 더 일반적입니다 - 당신이 경우 정적 DateFormat의를 가질 괜찮을 것이지만 했다 동기화합니다. 새로운 것을 SimpleDateFormat매우 자주 만드는 것보다 많은 경우에 성능이 더 좋습니다 . 사용 패턴에 따라 다릅니다.
Jon Skeet

1
정적 인스턴스가 멀티 스레드 환경에서 문제를 일으킬 수있는 방법과 이유를 설명해 주시겠습니까?
Alexandr

4
중간 변수를 인스턴스 변수에 저장하기 때문에 스레드 안전하지 않습니다
Bozho

2

날짜 형식이 동기화되지 않았습니다. 각 스레드마다 별도의 형식 인스턴스를 작성하는 것이 좋습니다. 여러 스레드가 동시에 형식에 액세스하는 경우 외부에서 동기화해야합니다.

즉, DateFormat의 객체가 있고 두 개의 다른 스레드에서 동일한 객체에 액세스하고 해당 객체에서 format 메소드를 호출한다고 가정하면 두 스레드가 동일한 객체에서 동일한 메소드에 동시에 입력되어 원화를 시각화 할 수 있습니다 적절한 결과를 얻지 못함

DateFormat으로 작업 해야하는 경우 어떻게해야합니까?

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

1

데이터가 손상되었습니다. 어제 나는 정적 DateFormat객체가 있고 format()JDBC를 통해 읽은 값을 호출하는 멀티 스레드 프로그램에서 그것을 발견했습니다 . 다른 이름 ( SELECT date_from, date_from AS date_from1 ...)으로 동일한 날짜를 읽는 SQL select 문이 있습니다. 이러한 진술은 다양한 날짜에 5 개의 스레드로 사용되었습니다.WHERE . 날짜는 "정상"으로 보였지만 가치는 달랐습니다. 모든 날짜는 같은 해의 월과 일만 변경되었습니다.

다른 답변은 그러한 부패를 피하는 방법을 보여줍니다. 나는 DateFormat정적이 아니었고 이제는 SQL 문을 호출하는 클래스의 멤버입니다. 정적 버전도 동기화하여 테스트했습니다. 둘 다 성능의 차이없이 잘 작동했습니다.


1

Format, NumberFormat, DateFormat, MessageFormat 등의 사양은 스레드로부터 안전하도록 설계되지 않았습니다. 또한 구문 분석 메소드는 메소드를 호출하며 Calendar.clone()캘린더 풋 프린트에 영향을 미치므로 많은 스레드가 동시에 구문 분석하면 캘린더 인스턴스의 복제가 변경됩니다.

자세한 내용은 thisthis 와 같은 버그 보고서 이며 DateFormat 스레드 안전 문제의 결과입니다.


1

가장 좋은 대답에서 dogbane은 parse기능 을 사용 하는 방법과 그 결과를 보여줍니다. 아래는 format기능 을 확인할 수있는 코드입니다 .

실행기 (동시 스레드) 수를 변경하면 다른 결과가 나타납니다. 내 실험에서 :

  • 두고 newFixedThreadPool5 세트를 루프가 매번 실패합니다.
  • 1로 설정하면 루프가 항상 작동합니다 (분명히 모든 작업이 하나씩 실행되므로)
  • 2로 설정하면 루프의 작동 확률은 약 6 %에 불과합니다.

프로세서에 따라 YMMV가 추측됩니다.

format함수는 다른 스레드로부터 시간 포맷 불능. 내부 format기능은 기능 calendar시작시 설정된 객체를 사용 하기 때문 format입니다. 그리고 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는 왜 이런 일이 발생했는지 물었습니다.
Adam

0

이로 인해 ArrayIndexOutOfBoundsException

잘못된 결과 외에도 때때로 충돌이 발생합니다. 기계의 속도에 따라 다릅니다. 내 랩톱에서는 평균 10 만 건의 호출에서 한 번 발생합니다.

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)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.