java.­math.­BigDecimal toString is not thread safe

August 29, 2017

BigDecimal is an immutable data type. So every method of this class should be thread safe. But this not the case for the method toString. Calling it from multiple threads leads to strange results.

To see this, let us look at the source code:

@Override
    public String toString() {
        String sc = stringCache;
        if (sc == null)
            stringCache = sc = layoutChars(true);
        return sc;
    }
    /**
     * Used to store the canonical string representation, if computed.
     */
    private transient String stringCache;

As we see a non-volatile field stringCache is used to cache the String computed in the method layoutChars. The method layoutChars uses a thread local StringBuffer to compute a String representation of this BigDecimal. In line 3 the instance variable stringCache is read and line 5 written This makes the class BigDecimal mutable and the method toString not thread safe.

The race condition

If the code is executed in the given order everything is o.k. But if some component reorders the statements, the cached String is not completely initialized. In pseudo code the method toString together with layoutChars looks like this:

store stringCache in local Variable sc
if sc is null
{
	call layoutChars
	{
		compute String with thread local StringBuilder
		call StringBuilder toString
		{
			create String
			initialize String in Constructor of class String
	    }
	}
	store result in instance Variable stringCache
}

If the statements get reordered a thread sees an uninitialized String:

Thread A store stringCache in local Variable sc
Thread A if sc is null
Thread A create String
Thread A store result in instance Variable stringCache
Thread B store stringCache in local Variable sc
Thread B Thread B sees an uninitialized String
Thread A compute String with thread local StringBuilder
Thread A initialize String in Constructor of class String

One component which reorders statements is the cache system of the CPU. ARM compatible processors like in smartphones or the Raspberry Pi reorder reads and writes to improve performance, leading to a scenario as described above.

Reproducing the error

To reproduce the error I use jcstress, an open JDK code tool: The Java Concurrency Stress tests (jcstress) is an experimental harness and a suite of tests to aid the research in the correctness of concurrency support in the JVM, class libraries, and hardware.

I use the following test class:

 package com.vmlens.stressTest.tests; import java.math.BigDecimal; import org.openjdk.jcstress.annotations.*; import org.openjdk.jcstress.infra.results.IntResult1; @JCStressTest @Outcome(id = "0", expect = Expect.ACCEPTABLE, desc = "Default outcome.") @State public class BigDecimalToString { private final  BigDecimal testBigDecimal = new BigDecimal("0.56"); @Actor public void actor1(IntResult1 r) { testBigDecimal.toString().length();	 } @Actor public void actor2(IntResult1 r) { testBigDecimal.toString().length(); }	 }

Jcstress runs this test multiple times always calling the method actor1 and actor2 from separate threads. When I call this test on a raspberry pi, I see the following null pointer exception:

 java.lang.NullPointerException at java.lang.String.length(String.java:623) at com.vmlens.stressTest.tests.BigDecimalToString.actor1(BigDecimalToString.java:12) at com.vmlens.stressTest.tests.BigDecimalToString_jcstress.actor1(BigDecimalToString_jcstress.java:145) at java.util.concurrent.FutureTask.run(FutureTask.java:266) 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)

If we look at line 623 of class String we see that the String was indeed not completely initialized. The instance variable value storing the character array is null:
return value.length;

Where did I find the race condition?

I found the race condition while testing geronimo web service with vmlens. vmlens detects race conditions during test runs. vmlens created the following trace during my tests:

 - variable: java.math.BigDecimal.stringCache reading: thread: DefaultThreadPool 197 stack: - java.math.BigDecimal.toString - org.apache.axis.encoding.ser.SimpleSerializer.getValueAsString - org.apache.axis.encoding.ser.SimpleSerializer.serialize - org.apache.axis.encoding.SerializationContext.serializeActual - org.apache.axis.encoding.SerializationContext.serialize - org.apache.axis.encoding.SerializationContext.serialize - org.apache.axis.encoding.ser.BeanSerializer.serialize - org.apache.axis.encoding.SerializationContext.serializeActual - org.apache.axis.encoding.SerializationContext.serialize - org.apache.axis.encoding.SerializationContext.serialize - org.apache.axis.message.RPCParam.serialize - org.apache.axis.message.RPCElement.outputImpl - org.apache.axis.message.MessageElement.output - org.apache.axis.message.SOAPBody.outputImpl - org.apache.axis.message.SOAPEnvelope.outputImpl - org.apache.axis.message.MessageElement.output - org.apache.axis.SOAPPart.writeTo ---- Stack Trace shortened ---- writing: thread: DefaultThreadPool 196 stack: - java.math.BigDecimal.toString - org.apache.axis.encoding.ser.SimpleSerializer.getValueAsString - org.apache.axis.encoding.ser.SimpleSerializer.serialize
---- Stack Trace shortened ----

This shows that toString is used for serializing BigDecimals to SOAP messages.

Conclusion

The caching of the computed String in an instance variable in the method toString makes the class BigDecimal mutable and the method toString not thread safe.This leads to NullPointer exceptions on ARM compatible processors. One usage of the toString method is the serialization of BigDecimals to SOAP messages.

Make your application thread safe

LEARN MORE