The Java Memory Model (JMM) guarantees that once a constructor completes, writes to final fields become visible to any thread that reads the object reference, provided that reference did not escape during construction. If the this reference leaks prematurely—by being passed to another thread or stored in a static collection before the constructor returns—the happens-before edge between the constructor’s write to the final field and the other thread’s read is severed. Consequently, the observing thread may witness the default value (zero, false, or null) rather than the constructed value, breaking apparent immutability. Safe publication requires that no reference to the object under construction escapes until construction terminates, ensuring that the freeze action on final fields occurs before any thread can load the reference.
We encountered this in a high-frequency trading system where Service instances registered themselves into a global ConcurrentHashMap during their constructors to facilitate lookup. The class defined a final long instrumentId, initialized from a constructor parameter, yet monitoring threads sporadically read zero when querying the registry immediately after creation.
One proposed solution was to declare instrumentId as volatile instead of final, hoping to force immediate visibility across cores. This approach guaranteed atomicity and visibility but forfeited the immutability contract and incurred a full memory barrier cost on every read, unnecessarily degrading throughput for a value that never changed after construction and complicating reasoning about the object's state.
Another suggestion involved synchronizing all accesses to the registry with synchronized blocks encapsulating the constructor logic, theorizing that locking would flush memory caches. While this prevented race conditions, it introduced heavy contention at the global registry lock, turning a concurrent structure into a serial bottleneck and violating strict latency requirements for market data ingestion.
We chose a factory pattern that decoupled instantiation from registration. The constructor remained private, the factory method invoked new Service(id) completely, and only afterwards published the fully formed reference to the ConcurrentHashMap. This leveraged the JMM’s final-field freeze semantics without synchronization overhead, ensuring that the instrumentId was visible immediately upon retrieval.
The change eliminated the zero-visibility anomalies and restored the expected microsecond-scale latency for service lookup, while preserving the immutable design intent.
Why doesn’t final guarantee visibility if I simply publish the reference through a thread-safe collection like ConcurrentHashMap?
The happens-before relationship provided by ConcurrentHashMap’s put and get operations establishes ordering between the map’s internal state changes, not between the constructor’s writes and the map’s publication. If this escapes during construction, the write to the final field occurs in one thread while the map publication happens concurrently, lacking the happens-before edge needed to prevent instruction reordering. Therefore, the reading thread may observe the reference through the map before the constructor’s writes are flushed to main memory, witnessing the default value.
Can I fix this by making the registry field volatile instead of the object’s fields?
Marking the registry reference volatile only ensures that changes to the registry variable itself are visible, not the internal state of the objects it contains. Since the issue is the timing of the object’s field writes relative to the reference becoming visible, volatile on the container does not establish the necessary ordering between the constructor and the consumer of the object. You would still observe partially constructed instances.
Does using synchronized inside the constructor prevent the unsafe publication?
Placing synchronized on the constructor or using it to guard the registration prevents other threads from entering the critical section concurrently, but it does not prevent the this reference from escaping if the registration method leaks the reference to a thread that operates outside that lock. The JMM specifically requires that no reference to the object escape before the constructor finishes for final-field semantics to hold; synchronization without proper publication ordering cannot resurrect that guarantee.