I manage an open source project and have a user reporting a situation which I think is impossible according to Java's order of initialization of static variables in classes. The value of a static final
class variable is incorrect, apparently resulting from different results of a dependency's static method based on its own static final variable.
I'd like to understand what's happening in order to figure the best workaround. At the moment, I am baffled.
The problem
The main entry point for my project is the class SystemInfo
which has the following constructor:
public SystemInfo() {
if (getCurrentPlatform().equals(PlatformEnum.UNKNOWN)) {
throw new UnsupportedOperationException(NOT_SUPPORTED + Platform.getOSType());
}
}
When run by itself, the problem doesn't reproduce; but when run as part of many tests being executed a larger build (mvn install
) it is consistently reproducible, implying the problem is likely associated with multithreading or multiple forks. (To clarify: I mean the simultaneous initialization of static members in two different classes, and the various JVM-internal locking/synchronization mechanisms associated with this process.)
They receive the following result:
java.lang.UnsupportedOperationException: Operating system not supported: JNA Platform type 2
This exception implies two things are true when SystemInfo
instantiation begins:
- The result of
getCurrentPlatform()
is the enum valuePlatformEnum.UNKNOWN
- The result of
Platform.getOSType()
is 2
However, this situation should be impossible; a value of 2 would return WINDOWS, and unknown would return a value other than 2. Since both variables are both static
and final
they should never simultaneously reach this state.
(User's) MCRE
I have tried to reproduce this on my own and failed, and am relying on a report from a user executing tests in their Kotlin-based (kotest) framework.
The user's MCRE simply invokes this constructor as part of a larger number of tests, running on the Windows operating system:
public class StorageOnSystemJava {
public StorageOnSystemJava(SystemInfo info) {
}
}
class StorageOnSystemJavaTest {
@Test
void run() {
new StorageOnSystemJava(new SystemInfo());
}
}
Underlying code
The getCurrentPlatform()
method simply returns the value of this static final
variable.
public static PlatformEnum getCurrentPlatform() {
return currentPlatform;
}
This is a static final
variable populated as the very first line in the class (so it should be the first thing initialized):
private static final PlatformEnum currentPlatform = queryCurrentPlatform();
where
private static PlatformEnum queryCurrentPlatform() {
if (Platform.isWindows()) {
return WINDOWS;
} else if (Platform.isLinux()) {
// other Platform.is*() checks here
} else {
return UNKNOWN; // The exception message shows the code reaches this point
}
}
This means that during class initialization, all of the Platform.is*()
checks returned false.
However, as indicated above this should not have happened. These are calls to JNA's Platform
class static methods. The first check, which should have returned true
(and does, if called in the constructor or anywhere in code after instantiation) is:
public static final boolean isWindows() {
return osType == WINDOWS || osType == WINDOWSCE;
}
Where osType
is a static final
variable defined thus:
public static final int WINDOWS = 2;
private static final int osType;
static {
String osName = System.getProperty("os.name");
if (osName.startsWith("Linux")) {
// other code
}
else if (osName.startsWith("Windows")) {
osType = WINDOWS; // This is the value being assigned, showing the "2" in the exception
}
// other code
}
From my understanding of the order of initialization, Platform.isWindows()
should always return true
(on a Windows OS). I do not understand how it could possibly return false
when called from my own code's static variable initialization. I've tried both the static method, and a static initialization block immediately following the variable declaration.
Expected order of initialization
- User calls the
SystemInfo
constructor SystemInfo
class initialization begins ("T is a class and an instance of T is created.")- The
static final currentPlatform
variable is encountered by the initializer (first line of class) - The initializer calls the static method
queryCurrentPlatform()
to obtain a result (same result if the value is assigned in a static block immediately following the static variable declaration) - The
Platform.isWindows()
static method is called - The
Platform
class is initialized ("T is a class and a static method of T is invoked.") - The
Platform
class sets theosType
value to 2 as part of initialization - When
Platform
initialization is complete, the static methodisWindows()
returnstrue
- The
queryCurrentPlatform()
sees thetrue
result and sets thecurrentPlatform
variable value (This is not happening as expected!) - After
SystemInfo
class initialization is complete, its constructor executes, showing the conflicting values and throwing the exception.
Workarounds
Some workarounds stop the problem, but I don't understand why they do:
Performing the
Platform.isWindows()
check anytime during the instantiation process (including the constructor) properly returnstrue
and assigns the enum appropriately.- This includes lazy instantiation of the
currentPlatform
variable (removing thefinal
keyword), or ignoring the enum and directly calling JNA'sPlatform
class.
- This includes lazy instantiation of the
Moving the first call to the
static
methodgetCurrentPlatform()
out of the constructor.
These workarounds imply a possible root cause is associated with executing static
methods of multiple classes during class initialization. Specifically:
- During initialization, the
Platform.isWindows()
check apparently returnsfalse
because code reaches theelse
block - After initialization (during instantiation), the
Platform.isWindows()
check returnstrue
. (Since it is based on astatic final
value it should not ever return different results.)
Research
I've thoroughly reviewed multiple tutorials about Java clearly showing the initialization order, as well as these other SO questions and the linked Java Language Specs: