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.

Comments

EVALUATION
Here are performance data after the fix.
$ java TestDateSpeed Single Threaded:
Without calling toString took 40 ms
After calling toString took 62 ms
Multi Threaded:
Without calling toString took 54 ms
Without calling toString took 55 ms
After calling toString took 82 ms
After calling toString took 84 ms
Here are 1.4.2 data on the same machine.
$ java TestDateSpeed Single Threaded:
Single Threaded:
Without calling toString took 117 ms
After calling toString took 105 ms
Multi Threaded:
Without calling toString took 163 ms
Without calling toString took 181 ms
After calling toString took 135 ms
After calling toString took 175 ms

2006-11-08

EVALUATION
Clearing cdate slows down other methods like getXXX(). A better fix would be to keep fastTime with cdate synced after normalization.
The TimeZone.getDefaultRef synchronization problem was fixed in 1.5.0_07.

2006-10-19

EVALUATION
cdate must be cleared upon return if it's been set in normalize(). Also, the way to get a CalendarSystem should be improved.