others-how to resolve ArrayIndexOutOfBoundsException when using SimpleDateFormat in android apps ?
Problem
When we use SimpleDateFormat in android apps ,sometimes ,we get this error:
Fatal Exception: java.lang.ArrayIndexOutOfBoundsException: length=13; index=-3
at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2411)
at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2321)
at java.util.Calendar.setTimeInMillis(Calendar.java:1787)
at java.util.Calendar.setTime(Calendar.java:1749)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:981)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:974)
at java.text.DateFormat.format(DateFormat.java:341)
The core error is :
Fatal Exception: java.lang.ArrayIndexOutOfBoundsException: length=13; index=-3
Environment
- android
- java jdk 1.8
Reason
Because Java’s SimpleDateFormat is not thread-safe. SimpleDateFormat is used to format and parse dates in Java and Android environment. One of the most important things to note about SimpleDateFormat class is that it is not thread-safe and causes issues in multi-threaded environments if not used properly.
Reproduce the exception
We can reproduce the error as follows:
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
ExecutorService ex = Executors.newFixedThreadPool(1000);
public void testConcurrentFormatArrayIndexOutOfBoundsException() {
for(;;){
ex.execute(new Runnable() {
public void run() {
try {
sdf.format(new Date(new Random().nextLong()));
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
};
});
}
}
If we run the above code, we would get this error message:
java.lang.ArrayIndexOutOfBoundsException: -1
at java.util.Calendar.getDisplayName(Calendar.java:2114)
at java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1125)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:966)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:936)
at java.text.DateFormat.format(DateFormat.java:345)
at com.bswen.svc.TestCmd1$2.run(TestCmd1.java:51)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
java.lang.ArrayIndexOutOfBoundsException: -1
at java.util.Calendar.getDisplayName(Calendar.java:2114)
at java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1125)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:966)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:936)
at java.text.DateFormat.format(DateFormat.java:345)
at com.bswen.svc.TestCmd1$2.run(TestCmd1.java:51)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Solution
We should ensure that every thread is using its own SimpleDateFormat instance , we choose to use the ThreadLocal class to avoid the concurrent problem of SimpleDateFormat.
static final ThreadLocal<SimpleDateFormat> sdfLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
};
public void testConcurrentFormat() {
for(;;){
ex.execute(new Runnable() {
public void run() {
try {
sdfLocal.get().format(new Date(new Random().nextLong()));
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
};
});
}
}
Run code code again, It works!
One more thing
We can initialize the ThreadLocal object lazily like this:
public class MyDateFormatter {
private ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<>();
public String format(Date date) {
SimpleDateFormat simpleDateFormat = getThreadLocalSimpleDateFormat();
return simpleDateFormat.format(date);
}
private SimpleDateFormat getThreadLocalSimpleDateFormat() {
SimpleDateFormat simpleDateFormat = simpleDateFormatThreadLocal.get();
if(simpleDateFormat == null) {
simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
simpleDateFormatThreadLocal.set(simpleDateFormat);
}
return simpleDateFormat;
}
}