모든 사람들은 스레드가 안전하지 않은 Java DateFormat에 대해주의하고 이론적으로 개념을 이해합니다.
그러나 이로 인해 우리가 직면 할 수있는 실제 문제를 시각화 할 수 없습니다. 예를 들어, 클래스에 DateFormat 필드가 있고 멀티 스레드 환경에서 클래스의 다른 메소드 (서식 날짜)에 동일하게 사용됩니다.
이것이 원인입니까?
- 형식 예외와 같은 예외
- 데이터 불일치
- 다른 문제?
또한 이유를 설명하십시오.
모든 사람들은 스레드가 안전하지 않은 Java DateFormat에 대해주의하고 이론적으로 개념을 이해합니다.
그러나 이로 인해 우리가 직면 할 수있는 실제 문제를 시각화 할 수 없습니다. 예를 들어, 클래스에 DateFormat 필드가 있고 멀티 스레드 환경에서 클래스의 다른 메소드 (서식 날짜)에 동일하게 사용됩니다.
이것이 원인입니까?
또한 이유를 설명하십시오.
답변:
사용해 봅시다.
다음은 여러 스레드가 공유를 사용하는 프로그램입니다 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;
}
}
데이터가 손상 될 수 있습니다. 예를 들어 두 날짜를 동시에 구문 분석하는 경우 한 통화가 다른 통화의 데이터로 오염 될 수 있습니다.
구문 분석에는 종종 지금까지 읽은 내용에 대해 일정량의 상태를 유지하는 것이 포함됩니다. 두 스레드가 모두 같은 상태에서 짓밟 히면 문제가 발생합니다. 예를 들어, 유형 DateFormat
의 calendar
필드를 노출하고 의 메소드를 Calendar
보고 SimpleDateFormat
일부 메소드는 호출 calendar.set(...)
하고 다른 메소드는 호출 calendar.get(...)
합니다. 이것은 스레드로부터 안전하지 않습니다.
나는이에보고하지 않은 정확한 이유에 대한 자세한 DateFormat
스레드로부터 안전하지 않습니다,하지만 나를 위해 그것이 알고 충분 하다 동기화없이 안전 - 비 안전의 정확한 매너도 릴리스간에 변경 될 수 있습니다.
개인적으로 난에서 파서 사용하는 것이 Joda 시간을 가로, 대신 있습니다 스레드 안전 - 그리고 Joda 시간에 시작하는 더 나은 날짜와 시간 API입니다 :)
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
대략 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
)
SimpleDateFormat
매우 자주 만드는 것보다 많은 경우에 성능이 더 좋습니다 . 사용 패턴에 따라 다릅니다.
날짜 형식이 동기화되지 않았습니다. 각 스레드마다 별도의 형식 인스턴스를 작성하는 것이 좋습니다. 여러 스레드가 동시에 형식에 액세스하는 경우 외부에서 동기화해야합니다.
즉, DateFormat의 객체가 있고 두 개의 다른 스레드에서 동일한 객체에 액세스하고 해당 객체에서 format 메소드를 호출한다고 가정하면 두 스레드가 동일한 객체에서 동일한 메소드에 동시에 입력되어 원화를 시각화 할 수 있습니다 적절한 결과를 얻지 못함
DateFormat으로 작업 해야하는 경우 어떻게해야합니까?
public synchronized myFormat(){
// call here actual format method
}
데이터가 손상되었습니다. 어제 나는 정적 DateFormat
객체가 있고 format()
JDBC를 통해 읽은 값을 호출하는 멀티 스레드 프로그램에서 그것을 발견했습니다 . 다른 이름 ( SELECT date_from, date_from AS date_from1 ...
)으로 동일한 날짜를 읽는 SQL select 문이 있습니다. 이러한 진술은 다양한 날짜에 5 개의 스레드로 사용되었습니다.WHERE
. 날짜는 "정상"으로 보였지만 가치는 달랐습니다. 모든 날짜는 같은 해의 월과 일만 변경되었습니다.
다른 답변은 그러한 부패를 피하는 방법을 보여줍니다. 나는 DateFormat
정적이 아니었고 이제는 SQL 문을 호출하는 클래스의 멤버입니다. 정적 버전도 동기화하여 테스트했습니다. 둘 다 성능의 차이없이 잘 작동했습니다.
가장 좋은 대답에서 dogbane은 parse
기능 을 사용 하는 방법과 그 결과를 보여줍니다. 아래는 format
기능 을 확인할 수있는 코드입니다 .
실행기 (동시 스레드) 수를 변경하면 다른 결과가 나타납니다. 내 실험에서 :
newFixedThreadPool
5 세트를 루프가 매번 실패합니다.프로세서에 따라 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");
}
이것은 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
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)