This is a good question with a complicated answer. I've split it in pieces for an easier read.
People have said here enough times that under the strict rules of JLS
- you should be able to see the desired behavior. But compilers (I mean C1
and C2
), while they have to respect the JLS
, they can make optimizations. And I will get to this later.
Let's take the first, easy scenario, where there are two non-final
variables and see if we can publish an in-correct object. For this test, I am using a specialized tool that was tailored for this kind of tests exactly. Here is a test using it:
@Outcome(id = "0, 2", expect = Expect.ACCEPTABLE_INTERESTING, desc = "not correctly published")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "not correctly published")
@Outcome(id = "1, 2", expect = Expect.ACCEPTABLE, desc = "published OK")
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "II_Result default values for int, not interesting")
@Outcome(id = "-1, -1", expect = Expect.ACCEPTABLE, desc = "actor2 acted before actor1, this is OK")
@State
@JCStressTest
public class FinalTest {
int x = 1;
Holder h;
@Actor
public void actor1() {
h = new Holder(x, x + 1);
}
@Actor
public void actor2(II_Result result) {
Holder local = h;
// the other actor did it's job
if (local != null) {
// if correctly published, we can only see {1, 2}
result.r1 = local.left;
result.r2 = local.right;
} else {
// this is the case to "ignore" default values that are
// stored in II_Result object
result.r1 = -1;
result.r2 = -1;
}
}
public static class Holder {
// non-final
int left, right;
public Holder(int left, int right) {
this.left = left;
this.right = right;
}
}
}
You do not have to understand the code too much; though the very minimal explanations is this: there are two Actor
s that mutate some shared data and those results are registered. @Outcome
annotations control those registered results and set certain expectations (under the hood things are far more interesting and verbose). Just bare in mind, this is a very sharp and specialized tool; you can't really do the same thing with two threads running.
Now, if I run this, the result in these two:
@Outcome(id = "0, 2", expect = Expect.ACCEPTABLE_INTERESTING....)
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING....)
will be observed (meaning there was an unsafe publication of the Object, that the other Actor/Thread has actually see).
Specifically these are observed in the so-called TC2 suite of tests, and these are actually run like this:
java... -XX:-TieredCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:+StressLCM
-XX:+StressGCM
I will not dive too much of what these do, but here is what StressLCM and StressGCM does and, of course, what TieredCompilation flag does.
The entire point of the test is that:
This code proves that two non-final variables set in the constructor are incorrectly published and that is run on x86
.
The sane thing to do now, since there is a specialized tool in place, change a single field to final
and see it break. As such, change this and run again, we should observe the failure:
public static class Holder {
// this is the change
final int right;
int left;
public Holder(int left, int right) {
this.left = left;
this.right = right;
}
}
But if we run it again, the failure is not going to be there. i.e. none of the two @Outcome
that we have talked above are going to be part of the output. How come?
It turns out that when you write even to a single final variable, the JVM
(specifically C1
) will do the correct thing, all the time. Even for a single field, as such this is impossible to demonstrate. At least at the moment.
In theory you could throw Shenandoah
into this and it's interesting flag : ShenandoahOptimizeInstanceFinals
(not going to dive into it). I have tried running previous example with:
-XX:+UnlockExperimentalVMOptions
-XX:+UseShenandoahGC
-XX:+ShenandoahOptimizeInstanceFinals
-XX:-TieredCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:+StressLCM
-XX:+StressGCM
but this does not work as I hoped it will. What is far worse for my arguments of even trying this, is that these flags are going to be removed in jdk-14.
Bottom-line: At the moment there is no way to break this.