SimpleDateFormat is not thread-safe
0. 들어가면서
Java
에서 스레드 안정성(thread-safe)을 이야기하면 항상 문제가 있다고 거론되는 클래스들이 있습니다.
java.util.Date
java.text.SimpleDateFormat
이번 포스트에선 간단한 예시 코드를 통해 SimpleDateFormat
클래스를 스레드 안전하게 사용하는 방법에 대해 살펴보겠습니다.
1. Why is SimpleDateFormat class not thread-safe?
원인은 완벽하게 캡슐화(encapsulation) 되지 않은 객체가 여러 스레드에 의해 사용되었기 때문입니다. 스레드 안정성이 깨지는 일은 다음과 같은 조건들에 의해 발생합니다.
- 객체는 상태(state)를 가지고 있습니다.
- 객체의 상태란 클래스의 필드를 의미합니다.
- 객체의 상태를 외부에서 변경할 수 있습니다.
- 캡슐화가 되지 않은 객체가 여러 스레드에 의해 사용됩니다.
applyPattern Method in SimpleDateFormat Class
applyPattern
메소드를 통해 SimpleDateFormat
클래스의 문제점을 살펴보겠습니다.
SimpleDateFormat
클래스는compiledPattern
,pattern
라는 상태를 가집니다.applyPattern
메소드는 내부에서compiledPattern
,pattern
상태를 변경합니다.
public class SimpleDateFormat extends DateFormat {
private String pattern;
private transient char[] compiledPattern;
// ...
public void applyPattern(String pattern) {
applyPatternImpl(pattern);
}
private void applyPatternImpl(String pattern) {
compiledPattern = compile(pattern);
this.pattern = pattern;
}
}
2. Using Not Thread-Safely SimpleDateFormat Class
다음은 다른 스레드의 간섭으로 원치 않는 결과를 얻는 테스트 코드입니다.
ThreadNotSafeSimpleDateFormat
객체를 생성합니다.- 해당 객체에
HH:mm:ss.sss
패턴을 적용합니다. CompletableFuture
클래스를 통해 두 개의 스레드를 실행합니다.- 먼저 실행한 스레드에서 적용 패턴을
yyyy-MM-dd
으로 변경합니다. - 다음 실행한 스레드에서 날짜를 적용된 포맷으로 출력합니다.
- 먼저 실행한 스레드에서 적용 패턴을
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
class ThreadNotSafeSimpleDateFormat {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
void applyPattern(String format) {
simpleDateFormat.applyPattern(format);
}
String getFormattedDate(Date date) {
return simpleDateFormat.format(date);
}
}
public class ThreadNotSafeSimpleDateFormatTest {
static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
public static void main(String[] args) {
Date date = new Date();
ThreadNotSafeSimpleDateFormat threadNotSafeSimpleDateFormat = new ThreadNotSafeSimpleDateFormat();
threadNotSafeSimpleDateFormat.applyPattern("HH:mm:ss.sss");
CompletableFuture<Void> thread1 = CompletableFuture.runAsync(() -> {
threadNotSafeSimpleDateFormat.applyPattern("yyyy-MM-dd");
});
CompletableFuture.runAsync(() -> {
sleep(500);
System.out.printf("result of formatting - %s", threadNotSafeSimpleDateFormat.getFormattedDate(date));
}).join();
thread1.join();
}
}
Test Result
- 먼저 실행된 스레드에서 변경한 포맷으로 결과가 출력됩니다.
result of formatting - 2023-02-12
3. Using Thread-Safely SimpleDateFormat Class
다음은 다른 스레드의 간섭에도 영향을 받지 않고 원하는 결과를 얻는 테스트 코드입니다.
HH:mm:ss.sss
패턴을 적용한ThreadSafeSimpleDateFormat
객체를 생성합니다.CompletableFuture
클래스를 통해 두 개의 스레드를 실행합니다.- 먼저 실행한 스레드에서 적용 패턴을
yyyy-MM-dd
으로 변경합니다. - 다음 실행한 스레드에서 날짜를 적용된 포맷으로 출력합니다.
- 먼저 실행한 스레드에서 적용 패턴을
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
class ThreadSafeSimpleDateFormat {
private final SimpleDateFormat simpleDateFormat;
public ThreadSafeSimpleDateFormat(SimpleDateFormat simpleDateFormat) {
this.simpleDateFormat = simpleDateFormat;
}
static ThreadSafeSimpleDateFormat applyPattern(String format) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
simpleDateFormat.applyPattern(format);
return new ThreadSafeSimpleDateFormat(simpleDateFormat);
}
String getFormattedDate(Date date) {
return simpleDateFormat.format(date);
}
}
public class ThreadSafeSimpleDateFormatTest {
static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
public static void main(String[] args) {
Date date = new Date();
ThreadSafeSimpleDateFormat threadSafeSimpleDateFormat = ThreadSafeSimpleDateFormat.applyPattern("HH:mm:ss.sss");
CompletableFuture<Void> thread1 = CompletableFuture.runAsync(() -> {
threadSafeSimpleDateFormat.applyPattern("yyyy-MM-dd");
});
CompletableFuture.runAsync(() -> {
sleep(500);
System.out.printf("result of formatting - %s", threadSafeSimpleDateFormat.getFormattedDate(date));
}).join();
thread1.join();
}
}
Test Result
- 먼저 실행된 스레드에서 포맷을 변경하였지만, 처음 설정한 포맷에 맞는 결과가 출력됩니다.
result of formatting - 02:08:58.058
4. What is Difference Between Two Tests?
스레드 안전한 방법을 살펴보면 객체의 상태를 변경하는 동작을 수행할 땐 SimpleDateFormat
객체를 새로 생성하여 처리했습니다.
객체의 상태를 바꾸는 행위는 다중 스레드 환경에서 불안정하기 때문에 가능하다면 복제하거나 새로 만드는 것이 좋습니다.
4.1. Another Example of Not Thread-Safely Using
SimpleDateFormat
클래스의 parse
메소드는 수행 중간에 상태가 변경되므로 스레드 사이에 공유되면 위험한 코드입니다.
import java.text.SimpleDateFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleDateFormatParseTest {
public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int index = 0; index < 100; index++) {
executorService.submit(() -> {
try {
System.out.printf("Successfully Parsing - %s%n", simpleDateFormat.parse("2023-02-12T02:35:00"));
} catch (Exception e) {
System.out.printf("Parse Error - %s%n", e.getMessage());
}
});
}
executorService.shutdown();
}
}
Test Result
- 파싱(parsing) 작업이 정상적으로 수행되지 않습니다.
- 같은 값을 파싱하지만 결과가 동일하지 않습니다.
- 일부 스레드에서 에러가 발생합니다.
Successfully Parsing - Thu Feb 12 00:00:00 KST 1970
Parse Error - multiple points
Parse Error - multiple points
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Wed Aug 06 02:35:00 KST 3214
Parse Error - For input string: "3535E235"
Parse Error - For input string: "3535E"
Parse Error - For input string: ""
Parse Error - For input string: ".22302320232023E4.22302320232023E4"
Parse Error - For input string: ""
Successfully Parsing - Thu Feb 12 00:00:00 KST 1970
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
...
4.2. Example of Thread-Safely Using
객체의 상태가 변경되기 때문에 마찬가지로 매 스레드마다 새로 생성하여 사용합니다.
import java.text.SimpleDateFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleDateFormatParseTest {
public static void main(String[] args) {
// SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int index = 0; index < 100; index++) {
executorService.submit(() -> {
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
System.out.printf("Successfully Parsing - %s%n", simpleDateFormat.parse("2023-02-12T02:35:00"));
} catch (Exception e) {
System.out.printf("Parse Error - %s%n", e.getMessage());
}
});
}
executorService.shutdown();
}
}
Test Result
- 정상적으로 파싱 작업이 수행됩니다.
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
Successfully Parsing - Sun Feb 12 02:35:00 KST 2023
...
CLOSING
JDK1.8
부터 제공된 DateTimeFormatter
클래스를 사용하는 것이 좋습니다.
java.time.DateTimeFormatter
java.time.*
패키지에 대한 형식 변환합니다.- 날짜를 텍스트로 변경하거나 텍스트를 날짜로 변경합니다.
댓글남기기