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.
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.
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;
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.
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.
© 2020 vmlens Legal Notice Privacy Policy