問
我々は如何にして Java におけるスレッドセーフを保証すべきか。
結論
- synchronized ブロックを使いましょう。
- ロックオブジェクトはprivateインスタンスフィールドで定義しましょう。
- ロックオブジェクトはスレッドセーフの保証対象ごとに定義しましょう。
薀蓄
-
スレッドセーフとは
- スレッド間で可変データを共有する場合において、同期化が保証される状態。
-
同期化の保証とは
- 「原子性の保証」「可視性の保証」の二点を満たすものと定義する。
-
原子性(atomicity)の保証とは
- ある処理の開始から終了までの間、外部からの干渉を排他できること。
-
可視性(visibility)の保証とは
- 時系列に対して最新の値が参照できること。
Java言語仕様
Javaにおいては特に以下の制御構文が、これまでに出てきた要素に関連する。
-
synchronizedブロック
- 引数に同じオブジェクトを指定したブロック間において、操作を同期化する。
- メソッドに対する synchronized 修飾は自インスタンスに対する synchronized ブロックと等価。
// 以下のsynchronizedブロック間の操作を同期化 public void foo() { synchronized (_SOME_OBJECT) { // do something } } public void bar() { synchronized (_SOME_OBJECT) { // do something } }
// 以下は等価 public void foo() { synchronized (this) { // do something } } public void synchronized foo() { // do something }
-
volatile修飾子
- 指定した変数の可視性を保証する。
- スレッドセーフ用途で利用できる場面自体は存在するが、今回は扱わない。
- 例えばint型やboolean型の変数は言語仕様により原子性が保証されている。
実践
以下の実装に対して、foo() と bar() を別々のスレッドから不定期に複数回呼び出すケースを考える
public void foo() {
_object.update(); // _object の内部状態を変化させる処理
}
public void bar() {
if (_object.enabled()) {
_object.getData(); // enabled() が false の場合に呼び出すと例外発生
}
}
synchronizedブロック無し
bar() の処理に原子性の保証が無いため、以下の順でメソッドが呼び出される可能性がある。
_object.enabled() -> _object.update() -> _object.getData()
public void foo() {
_object.update();
}
public void bar() {
if (_object.enabled()) {
_object.getData(); // _object の内部状態は保証されない
}
}
synchronizedブロックあり
ブロック内の処理の間に同期化の保証が加わり、即ち原子性についても保証される。
【 _object.update() 】 -> 【 _object.enabled() -> _object.getData() 】 -> 【 _object.update() 】
ブロック単位で処理の待ち行列に格納されるイメージを持つとわかりやすい。
public void foo() {
synchronized (this) {
_object.update();
}
}
public void bar() {
synchronized (this) {
if (_object.enabled()) {
_object.getData(); // _object の内部状態が保証される
}
}
}
synchronizedブロック一部だけあり
引数に同じオブジェクトを指定したブロック間において、操作を同期化する
synchronized ブロックはあくまで、先に上記のように述べた通りの同期化の保証である。
以下の実装は bar() 内の処理に対する同期化を保証しない。
public void foo() {
_object.update();
}
public void bar() {
synchronized (this) {
if (_object.enabled()) {
_object.getData(); // _object の内部状態は保証されない
}
}
}
別オブジェクトに対するsynchronizedブロック
念のための補足。
引数に異なるオブジェクトを指定したブロック間における操作も同期化は保証されない。
public void foo() {
synchronized (_SOME_OBJECT) {
_object.update();
}
}
public void bar() {
synchronized (this) {
if (_object.enabled()) {
_object.getData(); // _object の内部状態は保証されない
}
}
}
余談
- getterのような readonly な単一処理においても synchronized ブロックを使用すべきである。
- 可視性の保証のため。
- また、オブジェクトによっては読み込み処理に対して原子性が保証されないものがある。
ロックオブジェクトについて
※synchronized ブロックの引数に渡すオブジェクトのこと
デッドロック耐性
デッドロックの発生を最大限抑止するため、実装には以下の原則を適用することが望ましい。
- synchronized ブロックの範囲を最小化する
- ロックオブジェクトの共有範囲を最小化する
ロックオブジェクト選定
-
this
- 利用者に対して public なオブジェクトであるため、原則に対して望ましくない
- 等価である synchronized メソッドも使用すべきではない
- 利用者に対して public なオブジェクトであるため、原則に対して望ましくない
-
privateクラスフィールド
- 利用者からはアクセスできないことが保証される
- インスタンス間で共有されるオブジェクトであるため、原則に対して望ましくない
-
privateインスタンスフィールド
- 利用者からはアクセスできないことが保証される
- かつインスタンス単位で生成されるため、原則に対して最適
また、いずれの場合においてもロックオブジェクトがnullになる可能性があってはならない。
これはfinal修飾子を付与することで保証する。
// 推奨実装
class Shared {
private final Object _LOCK_OBJECT = new Object(); // staticにしない
public void foo() {
synchronized (_LOCK_OBJECT) {
// do something
}
}
public void bar() {
synchronized (_LOCK_OBJECT) {
// do something
}
}
}
class User {
public void sample() {
final Shared object = new Shared();
new Thread(() -> object.foo()).start();
new Thread(() -> object.bar()).start();
}
}
// ロックオブジェクトに this を使用する実装
class Shared {
public void foo() {
synchronized (this) {
// do something
}
}
public void bar() {
synchronized (this) {
// do something
}
}
}
class User {
public void sample() {
final Shared object = new Shared();
// この object インスタンスが Shared 内部実装のロックオブジェクトへの参照となる
// そのため「ロックオブジェクトの共有範囲を最小化する」という原則に反する
}
}
ロックオブジェクト定義
同じく共有範囲最小化の原則により、スレッドセーフを保証したい対象ごとにロックオブジェクトを定義すべきである。
class Shared {
// ロック対象
private final Data _dataA = new Data();
private final Data _dataB = new Data();
private final Data _dateC = new Data();
private final Validator = _validator = new Validator();
// ロックオブジェクト
private final Object _DATA_A_LOCK = new Object();
private final Object _DATA_B_LOCK = new Object();
private final Object _DATA_C_LOCK = new Object();
public void foo() {
synchronized (_DATA_A_LOCK) {
_dataA.update();
}
synchronized (_DATA_B_LOCK) {
_dataB.update();
}
synchronized (_DATA_C_LOCK) {
_validator.execute();
_dataC.update();
}
}
public void bar() {
synchronized (_DATA_A_LOCK) {
_dataA.get();
}
synchronized (_DATA_B_LOCK) {
_dataB.get();
}
synchronized (_DATA_C_LOCK) {
_validator.execute();
_dataC.get();
}
}
}
ひとこと
- synchronizedメソッドを使用しない理由を聞かれたら、是非この記事を突き付けてください。