Java メモリモデル(JMM)は、コンストラクタが完了した後、final フィールドへの書き込みがオブジェクト参照を読み取る任意のスレッドに対して可視化されることを保証します。ただし、その参照が構築中にエスケープしなかった場合です。もし this 参照が早期に漏れると(他のスレッドに渡されたり、コンストラクタの戻り前に静的コレクションに保存されたりすると)、コンストラクタの final フィールドへの書き込みと他のスレッドの読み取りとの間にある happens-before エッジが断たれます。その結果、観測スレッドは構築された値ではなく、デフォルト値(ゼロ、false、または null)を見る可能性があり、明示的な不変性が破られます。安全な公開を行うためには、オブジェクトの構築が終了するまで、そのオブジェクトへの参照が漏れないようにする必要があります。これにより、final フィールドに対するフリーズアクションが、任意のスレッドが参照をロードできる前に行われることが保証されます。
私たちは、コンストラクタ中に Service インスタンスがグローバルな ConcurrentHashMap に自己登録する高頻度取引システムでこれに遭遇しました。クラスは、コンストラクタパラメータから初期化された final long instrumentId を定義していましたが、監視スレッドは作成直後にレジストリを照会するときに時折ゼロを読み取っていました。
提案された解決策の一つは、instrumentId を final ではなく volatile として宣言することでした。これは、コア間での即時可視性を強制することを期待していました。このアプローチは、原子性と可視性を保証しましたが、不変性の契約を放棄し、すべての読み取りに対して完全なメモリバリアコストを発生させ、構築後に決して変更されない値に対してスループットを不必要に低下させ、オブジェクトの状態に関する推論を複雑化しました。
別の提案は、すべてのレジストリへのアクセスをコンストラクタロジックをカプセル化する synchronized ブロックで同期することで、ロックがメモリキャッシュをフラッシュするだろうという理論でした。これによりレースコンディションは防がれましたが、グローバルレジストリロックでの重い競合を引き起こし、並行構造を直列ボトルネックに変え、市場データの取り込みに対する厳しいレイテンシ要件を侵害しました。
私たちは、インスタンス化を登録から切り離すファクトリーパターンを選択しました。コンストラクタはプライベートのままにし、ファクトリーメソッドが new Service(id) を完全に呼び出し、その後、完全に構成された参照を ConcurrentHashMap に公開しました。これにより、同期オーバーヘッドなしで JMM の最終フィールドフリーズセマンティクスを活用し、instrumentId が取得時に即座に可視化されることを保証しました。
この変更によりゼロの可視性異常が排除され、サービスの検索に対する期待されるマイクロ秒スケールのレイテンシが回復し、不変な設計意図が維持されました。
なぜ final は、スレッドセーフなコレクションである ConcurrentHashMap で単に参照を公開したとしても可視性を保証しないのですか?
ConcurrentHashMap の put および get 操作によって提供される happens-before 関係は、マップの内部状態の変化の間に順序を確立しますが、コンストラクタの書き込みとマップの公開の間には順序を確立しません。もし this が構築中にエスケープした場合、final フィールドへの書き込みは一つのスレッドで発生し、マップの公開は同時に発生します。これにより、命令の並べ替えを防ぐために必要な happens-before エッジが欠けています。したがって、読み取りスレッドは、コンストラクタの書き込みがメインメモリにフラッシュされる前にマップを介して参照を観察し、デフォルト値を見る可能性があります。
レジストリフィールドをオブジェクトのフィールドの代わりに volatile にすることでこれを修正できるのでしょうか?
レジストリ参照を volatile とマークすることは、レジストリ変数自体への変更が可視化されることを保証するだけであり、その内部に含まれるオブジェクトの状態を可視化します。オブジェクトのフィールドの書き込みと参照が可視化されるタイミングが問題であるため、コンテナにおける volatile では、コンストラクタとオブジェクトの消費者の間に必要な順序を確立することはできません。部分的に構築されたインスタンスを依然として観察することになります。
コンストラクタ内で synchronized を使用すると、安全でない公開を防げますか?
コンストラクタに synchronized を置くか、登録を保護するためにそれを使用することで、他のスレッドがクリティカルセクションに同時に入るのを防ぐことはできますが、登録メソッドがロックの外で動作するスレッドに参照を漏らす場合、this 参照がエスケープするのを防ぐことはできません。JMM は、final フィールドのセマンティクスが保持されるためには、コンストラクタが終了するまでオブジェクトへの参照がエスケープしないことを具体的に要求します。適切な公開順序なしの同期では、その保証を復活させることはできません。