java.lang.reflect.TypeVariable getBounds Is Not Thread Safe
Calling java.lang.reflect.TypeVariable getBounds from multiple threads can lead to a race condition and could crash you JVM. See how to reproduce it.
Join the DZone community and get the full member experience.
Join For Freejava.lang.reflect.TypeVariable getBounds is not thread safe. Calling it from multiple threads might even crash your JVM.
The following method shows you the use of getBounds(). The method getBounds is used to get the upper bound(s) of a generic type:
public void testGetBounds() {
Class cl = GenericInterface.class;
TypeVariable typeVariable = cl.getTypeParameters()[0];
typeVariable.getBounds()[0].getTypeName();
}
To make the examples and tests shorter, I do not iterate over the returned array but simply use the first element. Here is the generic interface used in the example:
package com.vmlens.stressTest.util;
public interface GenericInterface<Y extends GenericInterface<Y>> {
}
If you call testGetBounds from multiple threads, calling getBounds leads to a race condition.
The Race Condition
Since the array of TypeVariables returned by getTypeParameters is cached in the volatile field genericInfo, each thread works on the same TypeVariable instance. And the class TypeVariableImpl implementing the TypeVariable interface modifies the not volatile field bounds without synchronization:
package sun.reflect.generics.reflectiveObjects;
// import statements omitted
public class TypeVariableImpl
extends LazyReflectiveObjectGenerator implements TypeVariable {
// upper bounds - evaluated lazily
private Type[] bounds;
public Type[] getBounds() {
// lazily initialize bounds if necessary
if (bounds == null) {
FieldTypeSignature[] fts = getBoundASTs(); // get AST
// allocate result array; note that
// keeping ts and bounds separate helps with threads
Type[] ts = new Type[fts.length];
// iterate over bound trees, reifying each in turn
for ( int j = 0; j < fts.length; j++) {
Reifier r = getReifier();
fts[j].accept(r);
ts[j] = r.getResult();
}
// cache result
bounds = ts;
// could throw away bound ASTs here; thread safety?
}
return bounds.clone(); // return cached bounds
}
// other fields and methods omitted
}
In line 15 and 30 the field bounds is read and in line 27 it is written. If the code is executed in the given order everything is o.k. But if some component reorders the statements, the array is not completely initialized. In pseudo code the method getBounds looks like this:
if instance variable bounds is null
{
set local variable ts to new Array
initialize the array
set instance variable bounds to the local variable ts
}
return instance variable bounds.clone
If the statements get reordered, another thread sees an uninitialised array:
Thread A |
set local variable ts to new Array |
Thread A |
set instance variable bounds to the local variable ts |
Thread B |
if instance variable bounds is null |
Thread B |
Thread B return instance variable bounds.clone // the array is not yet completely initialized |
One such component 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 used 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:
@JCStressTest
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "Default outcome.")
@State
public class TypeVariableGetBounds {
private final Class cl;
public TypeVariableGetBounds() {
try {
cl = (new StressTestClassLoader(TypeVariableGetBounds.class.getClassLoader()))
.loadClass("com.vmlens.stressTest.util.GenericInterface");
} catch (Exception e) {
throw new RuntimeException("Test setup incorrect", e);
}
}
public void callContainsDataRace() {
TypeVariable typeVariable = cl.getTypeParameters()[0];
typeVariable.getBounds()[0].getTypeName();
}
@Actor
public void actor1(IntResult2 r) {
callContainsDataRace();
}
@Actor
public void actor2(IntResult2 r) {
callContainsDataRace();
}
}
Jcstress runs this test multiple times, always calling the method actor1 and actor2 from separate threads. To separate the tests, I use a special classloader, which always reloads a class.
When I call this test on a Raspberry Pi using the test mode tough or stress, I see a crash of the JVM:
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x7665b4c0, pid=16112, tid=1680598112
#
# JRE version: Java(TM) SE Runtime Environment (8.0_65-b17) (build 1.8.0_65-b17)
# Java VM: Java HotSpot(TM) Client VM (25.65-b01 mixed mode linux-arm )
# Problematic frame:
# V [libjvm.so+0x27a4c0]
The JVM error log shows the following:
Stack: [0x6426f000,0x642bf000], sp=0x642bd8b8, free space=314k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [libjvm.so+0x27a4c0]
Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
J 1303 java.lang.Object.clone()Ljava/lang/Object; (0 bytes) @ 0x742ba71c [0x742ba6e0+0x3c]
J 1313 C1 sun.reflect.generics.repository.GenericDeclRepository.getTypeParameters()[Ljava/lang/reflect/TypeVariable; (80 bytes) @ 0x742b8210 [0x742b7f40+0x2d0]
J 1229 C1 com.vmlens.stressTest.tests.TypeVariableGetBounds_jcstress.actor1()Ljava/lang/Void; (109 bytes) @ 0x742a6cb8 [0x742a6bf0+0xc8]
j com.vmlens.stressTest.tests.TypeVariableGetBounds_jcstress$$Lambda$6.call()Ljava/lang/Object;+4
j java.util.concurrent.FutureTask.run()V+42
j java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V+95
j java.util.concurrent.ThreadPoolExecutor$Worker.run()V+5
j java.lang.Thread.run()V+11
v ~StubRoutines::call_stub
It seems that the native array clone method can not cope with an uninitialized array.
So far, I could not reproduce this error on my Intel i5 workstation.
Conclusion
java.lang.reflect.TypeVariable getBounds is not thread safe. Calling it from multiple threads might lead, depending on the java platform you are using, to strange errors.
I found this race condition with VMLens. VMLens traces Java applications and finds race conditions and deadlocks in the traced applications.
Published at DZone with permission of Thomas Krieger, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments