每个人都警告Java DateFormat不是线程安全的,理论上我理解这个概念。
但是由于这个原因,我无法想象出我们可能面临的实际问题。比如,我在类中有一个DateFormat字段,并且在多线程环境中的类(格式化日期)中的不同方法中使用相同的字段。
这会导致:
另外,请解释原因。
答案 0 :(得分:247)
我们试一试。
这是一个程序,其中多个线程使用共享的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)
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)
结果错误:
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;
}
}
这是一个很好的post,其中包含更多细节。
答案 1 :(得分:29)
我希望数据损坏 - 例如如果你同时解析两个日期,你可能会有一个被另一个日期的数据污染的电话。
很容易想象如何发生这种情况:解析通常涉及到目前为止你所阅读的内容保持一定的状态。如果两个线程都在相同的状态下践踏,那么你会遇到问题。例如,DateFormat
公开calendar
类型的Calendar
字段,并查看SimpleDateFormat
的代码,某些方法调用calendar.set(...)
,其他方法调用calendar.get(...)
}}。这显然不是线程安全的。
我没有查看为什么DateFormat
不是线程安全的完全详细信息,但对我而言,它足以知道它是不同步的不安全 - 非安全的确切方式甚至可能在不同版本之间发生变化。
就我个人而言,我会使用Joda Time中的解析器,因为它们 线程安全 - 而Joda Time是一个更好的日期和时间API开始:)
答案 2 :(得分: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
如Jon Skeet所述,如果您执行外部同步(即在synchronized
调用时使用DateFormat
),您可以同时拥有静态和共享实例变量
答案 3 :(得分:10)
如果您使用的是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
答案 4 :(得分:2)
日期格式未同步。建议为每个线程创建单独的格式实例。如果多个线程同时访问格式,则必须同步它 外部。
这意味着假设您有一个DateFormat对象,并且您正在从两个不同的线程访问同一个对象,并且您正在该对象上调用format方法,两个线程将同时在同一个对象上输入同一个对象,这样您就可以可视化它不会产生正确的结果
如果您必须使用DateFormat,那么您应该做些什么
public synchronized myFormat(){
// call here actual format method
}
答案 5 :(得分:1)
数据已损坏。昨天我注意到它在我的多线程程序中,我有静态DateFormat
对象,并通过JDBC读取它的format()
。我有SQL select语句,我用不同的名称(SELECT date_from, date_from AS date_from1 ...
)读取相同的日期。这些陈述在WHERE
clasue中用于5个线程中的各种日期。日期看起来“正常”,但它们的价值不同 - 而所有日期都是从同一年开始,只有月份和日期发生变化。
其他答案向您展示了避免此类腐败的方法。我使DateFormat
不是静态的,现在它是调用SQL语句的类的成员。我测试了同步静态版本。两者都运作良好,表现没有差异。
答案 6 :(得分:1)
Format,NumberFormat,DateFormat,MessageFormat等的规范并非设计为线程安全的。此外,parse方法调用Calendar.clone()
方法,它会影响日历占用空间,因此许多并发解析的线程将更改Calendar实例的克隆。
答案 7 :(得分:1)
在最佳答案中,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");
}
答案 8 :(得分:0)
如果有多个线程操作/访问单个DateFormat实例并且未使用同步,则可能会得到加扰结果。那是因为多个非原子操作可能会改变状态或看到内存不一致。
答案 9 :(得分: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
答案 10 :(得分: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)