FULL PRODUCT VERSION :
java version "1.5.0_06"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_06-b05)
Java HotSpot(TM) Client VM (build 1.5.0_06-b05, mixed mode)
jdk1.5.0_07 and Java SE 1.5.0 Update 9.
ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]
A DESCRIPTION OF THE PROBLEM :
Calling toString() on java.util.Date or any of its subclasses (e.g. java.sql.Timestamp) causes a significant slow down to subsequent methods, including getTime, equals, hashcode, compareTo and variety of others. The slow down is even more significant in multi-threaded applications.
Here is a sample program to show the issue.
The results on calling equals are as follows on my machine:
Single Threaded:
Without calling toString took 187 ms
After calling toString took 5609 ms
Multi Threaded:
Without calling toString took 234 ms
Without calling toString took 234 ms
After calling toString took 249703 ms
After calling toString took 249703 ms
Analysis:
toString() calls a series of deprecated methods (such as getYear()). These deprecated methods set the internal variable Date.cdate, which is then checked in the getTime() method. The single threaded 30x slow down is due to the heavy weight calendar object. The multi-threaded case shows contention at TimeZone.getDefaultRef (which is effectively global):
"Thread-0" prio=6 tid=0x00947008 nid=0x2f8 waiting for monitor entry [0x0bc6f000..0x0bc6fd68]
at java.util.TimeZone.getDefaultRef(TimeZone.java:545)
- waiting to lock <0x06c026b8> (a java.lang.Class)
at java.util.Date.normalize(Date.java:1178)
at java.util.Date.getTimeImpl(Date.java:868)
at java.util.Date.getTime(Date.java:863)
at java.util.Date.equals(Date.java:930)
at test.TestDateSpeed.testEquals(TestDateSpeed.java:40)
Possible solutions:
1) don't call the deprecated methods in Date.toString() and Timestamp.toString(). Instead instantiate a calendar in the method itself and discard it at the end.
OR:
2) don't use Date.cdate in getTime(), just use Date.fasttime.
The problem seems to have been partially addressed in 1.5.0_07:
bash-2.05b$ jdk1.5.0_07/bin/java TestDateSpeed
Single Threaded:
Without calling toString took 53 ms
After calling toString took 3629 ms
Multi Threaded:
Without calling toString took 393 ms
Without calling toString took 397 ms
After calling toString took 4349 ms
After calling toString took 4406 ms
Which is due to TimeZone.getDefaultRef() becoming unsynchronized so that
the huge performance degradation for the mt case is eliminated.
At this point the problem reduces to a question of the overhead in both
cases that is introduced by the toString() call's side-effect of setting
cdate.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
See the included test class.
Call toString() on a Date or Timestamp object before calling equals.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
the performance of the object should not change after calling toString() on it
ACTUAL -
the performance of the object drops by 3000% in single threaded and 10000% in multi-threaded applications:
Single Threaded:
Without calling toString took 187 ms
After calling toString took 5609 ms
Multi Threaded:
Without calling toString took 234 ms
Without calling toString took 234 ms
After calling toString took 249703 ms
After calling toString took 249703 ms
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
package test;
import java.sql.Date;
public class TestDateSpeed implements Runnable
{
private Date firstDate = new Date(1234567890);
private Date secondDate = new Date(1234567890);
private static final int MAX_COUNT = 10000000;
public static void main(String[] args)
{
System.out.println("Single Threaded:");
TestDateSpeed tds = new TestDateSpeed();
tds.run();
System.out.println("Multi Threaded:");
Thread firstThread = new Thread(new TestDateSpeed());
Thread secondThread = new Thread(new TestDateSpeed());
firstThread.start();
secondThread.start();
try
{
firstThread.join();
secondThread.join();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
public void testEquals(String msg)
{
long start = System.currentTimeMillis();
for(int i=0;i<MAX_COUNT;i++)
{
firstDate.equals(secondDate);
}
System.out.println(msg + " took "+(System.currentTimeMillis() - start)+" ms");
}
public void run()
{
testEquals("Without calling toString");
callToStringOnDates();
testEquals("After calling toString");
}
public void callToStringOnDates()
{
firstDate.toString();
secondDate.toString();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
It's nearly impossible to prevent calls to toString() as it is part of the java.lang.Object interface. This really ought to get fixed in the JDK. Calls to any other deprecated methods have the same effect.