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;
    }
}