SimpleDateFormat is not thread-safe
0. 들어가면서
Java에서 스레드 안정성(thread-safe)을 이야기하면 항상 문제가 있다고 거론되는 클래스들이 있습니다.
java.util.Datejava.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.DateTimeFormatterjava.time.*패키지에 대한 형식 변환합니다.- 날짜를 텍스트로 변경하거나 텍스트를 날짜로 변경합니다.
댓글남기기