An immutable object, e.g. String
, appears to have the same state for all readers, regardless how its reference is obtained, even with improper synchronization and lack of happens-before relationship.
This is achieved by final
field semantics introduced in Java 5. Data access through a final field has a stronger memory semantics, as defined in jls-17.5.1
In terms of compiler reordering and memory barriers, there are more constraints when dealing with final fields, see JSR-133 Cookbook. The reordering you worried about won't happen.
And yes -- double-checked locking can be done through a final field in a wrapper; no volatile
is required! But this approach is not necessarily faster, because two reads are needed.
Note that this semantics applies to individual final fields, not the entire object as a whole. For example, String
contains a mutable field hash
; nevertheless, String
is considered immutable because its public behavior are only based on final
fields.
A final field can point to a mutable object. For example, String.value
is a char[]
which is mutable. It's impractical to require that an immutable object is a tree of final fields.
final char[] value;
public String(args) {
this.value = createFrom(args);
}
As long as we don't modify the content of value
after constructor exit, it's fine.
We can modify the content of value
in the constructor in any order, it doesn't matter.
public String(args) {
this.value = new char[1];
this.value[0] = 'x'; // modify after the field is assigned.
}
Another example
final Map map;
List list;
public Foo()
{
map = new HashMap();
list = listOf("etc", "etc", "etc");
map.put("etc", list)
}
Any access through the final field will appear to be immutable, e.g. foo.map.get("etc").get(2)
.
Access not through a final field does not -- foo.list.get(2)
is not safe through improper publication, even though it reads the same destination.
Those are the design motivations. Now let's see how JLS formalizes it in jls-17.5.1
A freeze
action is defined at the constructor exit, as apposed to at the assignment of the final field. This allows us to write anywhere inside the constructor to populate internal state.
The usual problem of unsafe publication is the lack of happens-before (hb
) relationship. Even if a read sees a write, it establishes nothing w.r.t other actions. But if a volatile read sees a volatile write, JMM establishes hb
and an order among many actions.
The final
field semantics wants to do the same thing, even with normal reads and writes, that is, even through unsafe publications. To do that, a memory chain (mc
) order is added between any write seen by a read.
A deferences()
order limits the semantics to accesses through the final field.
Let's revisit the Foo
example to see how it works
tmp = new Foo()
[w] write to list at index 2
[f] freeze at constructor exit
shared = tmp; [a] a normal write
// Another Thread
foo = shared; [r0] a normal read
if(foo!=null) // [r0] sees [a], therefore mc(a, r0)
map = foo.map; [r1] reads a final field
map.get("etc").get(2) [r2]
We have
hb(w, f), hb(f, a), mc(a, r1), and dereferences(r1, r2)
therefore w
is visible to r2
.
Essentially, through Foo
wrapper, a map (which is mutable in itself) is published safely though unsafe publication... if that makes sense.
Can we use the wrapper to establish final field semantics then discard it? Like
Foo foo = new Foo(); // [w] [f]
shared_map = foo.map; // [a]
Interestingly, JLS contains enough clauses to exclude such use case. I guess it's weakened so that more inner-thread optimizations are permitted, even with final fields.
Note that if this
is leaked before the freeze action, final field semantics is not guaranteed.
However, we can safely leak this
in a constructor after the freeze action, with constructor chaining.
-- class Bar
final int x;
Bar(int x, int ignore)
{
this.x = x; // assign to final
} // [f] freeze action on this.x
public Bar(int x)
{
this(x, 0);
// [f] is reached!
leak(this);
}
This is safe as far as x
is concerned; freeze action on x
is defined at the exist of the constructor in which x
is assigned. This was probably designed just to safely leak this
.