初めに
本記事では実務の中で使用している静的解析ツール(SonarLint)に指摘された警告の修正を入り口に、マルチスレッドについて説明したいと思います。
本記事では、以下の事柄について解説していきます。
- static の意味
- メモリ管理の方法
- メモリ上にプログラムを展開する意味と理由
- atatic とインスタンスの違い
- static の使い所と注意点
- マルチスレッドの基本的な話
- プロセスとは
- スレッドとは
- スレッドセーフとは
- 並行と並列
- 同期制御と排他制御の概論と基本
- 競合とは
- ロックを取得するとは
- デッドロックとは
- デッドロックの4条件
- トランザクション
- アトミック性と可視性
- synchronizedとは
- インクリメントは非アトミックな操作
- synchronized はミューテックス(単一ロック)
- ミューテックスはパフォーマンスには良くない
- synchronized メソッドと synchronized ブロックの違い
- その他の注意点
取り扱わない話
- 同期制御と排他制御の各論
- Thread クラス
- ReentrantLock クラス
- Atomic クラス
- wait()/notify()
- Executorフレームワーク
- 不変オブジェクト
- マルチスレッドにおける不変オブジェクトについては以下の過去記事にて解説してます。
1. SonarLint の警告内容
SonarLint は、コード内の「バグ発見」「脆弱性発見」「メンテナンス困難なコード検出」などをサポートする自動コードレビューツールです。SonarLint の警告内容は以下になります。
[Java] S2696 raises no issue if method is synchronized but not static
// [Java] S2696 メソッドが同期化されているが静的でない場合、問題が発生する。
Make the enclosing method "static" or remove this set.
// メソッドを "static "にするか、このセットを削除してください。Correctly updating a static field from a non-static method is tricky to get right and could easily lead to bugs if there are multiple class instances and/or multiple threads in play. Ideally, static fields are only updated from synchronized static methods.
This rule raises an issue each time a static field is updated from a non-static method.
Noncompliant Code Example
// 静的でないメソッドから静的フィールドを正しく更新するのは難しいことで、複数のクラスインスタンスや複数のスレッドが存在する場合、簡単にバグにつながる可能性があります。理想的には、静的フィールドは同期化された静的メソッドからしか更新されません。
このルールは、静的フィールドが非静的メソッドから更新されるたびに、問題を発生させます。
public class MyClass {
private static int count = 0;
public void doSomething() {
//...
count++; // Noncompliant
}
}
といった内容でした。全く意味がわからないまま、エラー文を元に複数の信頼できるサイトから同じような解決方法を見つけました。以下のコードです。違いはdoSomething()
メソッドにstatic
とsynchronized
修飾子が付いているだけです。
public class MyClass {
private static int count = 0;
public static synchronized void doSomething() {
//...
count++; // compliant
}
}
上記の警告文は些か抽象的でいまいち要領を得ません。より具体的に言うと、
「doSomething()
メソッドはインスタンスメソッドであり、静的フィールドであるcount
を更新している。だが、静的フィールドを更新するには、同期化された静的メソッド内で行う必要があるため、このコードは非準拠である」
-
※インスタンスメソッド = インスタンスに紐づくメソッドで、インスタンス化されないと使えない
- 呼び出し方:インスタンス変数.インスタンスメソッド
- ※static(クラス) メソッド = インスタンスを生成しなくても使える、クラスに紐付いている
- 呼び出し方:クラス.static(クラス) メソッド
ということを言っています。解決するには以下の方法があります。
- フィールドを静的から非静的にする。つまり、
static
を外して単純なインスタンスフィールドにする。 -
doSomething()
メソッドを静的かつ同期的(synchronized)にする(上記準拠コード) - 静的同期的なセッターメソッドを実装する(以下コード)
public class MyClass {
private static int count = 0;
public void doSomething() {
//...
setCount(count + 1);
}
private static synchronized void setCount(int newCount) {
count = newCount;
}
}
本記事では二番目の方法を採用したという前提で話をします。どの方法が最善かはフィールドの使用方法によって異なります。静的なフィールドがプログラムのどこかで変更される可能性がある場合、同期化されたsetterメソッドを使用するほうがよいらしい。一方、フィールドが読み取り専用である場合、静的同期的なメソッドを使用することが良いようです。
今回の SonarLint 警告文は、端的に言えば
- 「static フィールドへのアクセスは synchronized で static なメソッドにのみ設定されるべきである」
という意味です。なぜそうでなくてはいけないのでしょうか?
最も一般的に紹介されるのが、競合状態化(スレッドセーフではない環境)における共有データ(クリティカルセクション)のデータ不整合問題です。カウント10000回をマルチスレッドで実行したら6000回くらいの結果になって、期待通り10000回カウントしないとかですね。
またセキュリティ面にも問題があります。悪意ある攻撃者が故意に同期を取らず、もしくは信頼できるユーザが意図せずにフィールドへアクセスすることが可能であるためです。このような状態では、信頼できるユーザがこのクラスを安全に使用することはできません。このセキュアな観点での意味や、SonarLint 警告文を理解するには、以下の事柄を理解する必要があります。
- static 静的の意味
- マルチスレッドの基本的な話
- 排他制御と可視性
- synchronized 同期的の意味
それでは一つずつ見ていきましょう。
2. プログラム実行には実行内容をメモリ上に展開する必要がある
ほとんどのPCは「ノイマン型」コンピュータです。
よく見るやつです。入力装置はマウスやキーボード、出力装置はディスプレイなどですね。
ノイマン型のコンピュータの特徴は、
-
プログラム内蔵方式
あらかじめ機械語が主記憶装置(メモリ)に内蔵されている。 -
逐次制御方式
主記憶装置(メモリ)に格納されている情報を中央処理装置(CPU)が1つ1つ取り出して実行する。 -
アドレス指定方式でデータを扱う
固定の命令(機械語)は命令部とアドレス部から構成される。アドレス部は命令の対象となるデータがある主記憶装置(メモリ)のアドレスを示す情報を持っており、 アドレスを示す情報はCPU内部のレジスタ(高速な記憶装置)に格納される。 CPUはレジスタにある場所情報をもとにメモリから順次読み出す。 -
固定の命令(機械語)が使える
演算処理や動作などに固定の命令(機械語)を用いることができる。 「実行対象のプログラムをデータとしてメモリ上に展開し、処理演算装置(CPU)はそれを順次読み込んで処理する」
となっています。
つまり、作成したプログラムや第三者が作成したライブラリ、あるいはPC上で動く各種アプリケーションから、サーバ上で動作するWebサービスに至るまでどのようなプログラムも実行時に必ずメモリ上にその内容が展開される仕組みとなっています。メモリとは、プログラムの実行中に取り扱っているデータを一時的に保存する領域です。
実行対象プログラムは、実行前にメモリ上にその内容が展開されます。今回は取り上げませんが、この作業を行なうのはOSの役割です。また、OSとJVMがそれぞれで占有するメモリ空間などの概念もあります。JavaのGCの仕組みを理解するのに重要なので興味があれば調べてみて下さい。
2-1. なぜメモリ上に展開するのか?
プロセッサ(CPU)がメモリにアクセスして、プログラムの実行に必要な情報を取得するためです。ではなぜ、プロセッサは補助記憶装置(ハードディスクやSSDなど)直接アクセスしないのでしょうか?
プロセッサはCPU(Central Processing Unit)のことで、コンピュータ内でデータを処理するために使用されるチップです。M1Mac のプロセッサが Intel プロセッサから Apple シリコンに変更されましたね。Apple シリコンもプロセッサです。
メモリはプロセッサにとって最も早いアクセス可能な記憶装置であり、CPUはメモリと高速に通信することができます。一方、補助記憶装置に対するアクセスは、メモリよりもアクセス時間が遅く、処理速度が低下するため、プログラムやデータはメモリに展開されます。プログラムやデータがメモリに展開されることで、プロセッサは高速かつ効率的にアクセスでき、処理速度が向上するというわけです。
3. static の意味
Java には、プログラムが実行される際に使用される複数のメモリ領域があり、主なメモリ領域には以下のようなものがあります。メモリの管理方法やデータの使用目的によってメモリの空間が定義されています。
- スタック領域
- ヒープ領域
- static 領域
- コンスタントプール
領域 | 説明 | 変数との対応 | 生存期間 |
---|---|---|---|
static領域 | static 変数・メソッドを管理する。静的領域とも呼ばれ、スレッドから共有される。プログラムのどの部分からでも参照することができるため、プログラム全体で共有する変数や定数を格納する。 | クラススコープ内で static キーワード付きで変数宣言を行なう。 | クラス利用をするJavaアプリケーションの開始から終了まで有効 |
静的領域(static領域)は、Javaアプリケーション実行中に領域の大きさ(使用メモリサイズ)が変わらないため、静的領域と呼ばれています。
その他のメモリ空間
領域 | 説明 | 変数との対応 | 生存期間 |
---|---|---|---|
スタック領域 | ローカル変数、パラメータ、戻り値、演算に使われる任意の値などを管理する領域。スタック領域は共有リソースではないため、スレッドセーフ。実際に処理されるデータを格納する領域。「スタック」=積み重ねという名前が表すように、処理対象のデータはFILO(先入れ後出し)方式でデータを管理し、処理が完了したデータはスタックから破棄。 | メソッドスコープあるいはforスコープなどの特定の処理スコープ内で定義する。 | 特定の処理スコープ内だけで有効な変数。変数定義した処理スコープの処理がすべて完了すると破棄。 |
ヒープ領域 | new演算子で生成されたオブジェクトと配列を管理。必要な時に、必要なサイズを指定して領域が確保できる自由度の高いメモリ領域。スレッドで共有される。ただし、確保したメモリは必ず解放する必要がある。Java ではGCにて実行中のプログラムの動作から不要になったと判断した領域を自動的に解放。メモリの解放を明示的に行わなけばならない言語ではメモリリークに注意する必要がある。 | クラススコープ内で変数定義を行なう。 | 対象クラスのインスタンスがnew演算子にて生成されてから、破棄される(明示的に解放するかGC)までの間有効。 |
static領域 | static 変数・メソッドを管理する。静的領域とも呼ばれ、スレッドから共有される。プログラムのどの部分からでも参照することができるため、プログラム全体で共有する変数や定数を格納する。 | クラススコープ内で static キーワード付きで変数宣言を行なう。 | クラス利用をするJavaアプリケーションの開始から終了まで有効 |
コンスタントプール | 定数値や文字列などのリテラルが格納されるメモリ領域で、スレッドから共有される。コンパイル時に定義され、プログラムの実行中に変更することはできない。そのため、定数領域に格納された値は不変であり、プログラムの安全性やパフォーマンスを向上させる。 | クラススコープ内で final キーワード付きでの変数宣言や文字列リテラルでの変数定義を行なう。 | クラス利用をするJavaアプリケーションの開始から終了まで有効 |
以下に static 領域のメモリ空間を簡易的に図式化しました。違いと使い分けを箇条書きします。
※余談: new 演算子にはインスタンス化とコンストラクタによるオブジェクトの初期化という意味がある
インスタンス化の要否
- インスタンスメソッドやフィールドはインスタンス化によってメモリ(ヒープ領域)上に展開しないと参照できません。
- static メソッドやフィールドは static 領域というメモリ上に常に展開されているので、インスタンス化しなくても参照可能です。
3-1. static 領域のフィールド・メソッドはどこからでもアクセスできる
static 領域に定義されたフィールドやメソッドは、条件付きですがどこからでも参照可能です。クラス自体がロードされていれば、インスタンス生成の有無や値に関わらず、static フィールドや static メソッドを参照できます。ただし、アクセス修飾子によってアクセス制限がかけられる場合もあります。
- 同じパッケージ内のクラスから参照
- private でなければ 参照可能
- package private / protected / public は可能
- protected の場合は、同一パッケージまたはサブクラスからのみ参照可能
- private でなければ 参照可能
- 異なるパッケージに属するクラスからの参照
- public であれば参照可能
public class MyClass {
private static int count = 0;
public static void increment() {
count++;
}
}
他のクラスからアクセスする際には、以下のようにクラス名を指定して参照可能です。オブジェクトをインスタンス化する必要はありません。
MyClass.increment();
3-2. アクセスできるフィールドの違い
- static メソッドから static フィールドを使う
- static メソッドからインスタンスフィールドを使う
- インスタンスメソッドから static フィールドを使う
- インスタンスメソッドからインスタンスフィールドを使う
上記の2つ目の選択肢、「static メソッドからインスタンスフィールドを使う」は不可能です。static 領域に定義されたフィールドやメソッドは、条件付きですがどこからでも参照可能ですが、static メソッドからインスタンスフィールドを使うことはできません。
これは static 領域と、ヒープ領域のインスタンスの関係は、別々に独立した領域になっていることに起因します。
インスタンスは「自分のクラスが何クラスか」を知っています。だからthis
で自分自身を指し示すことができます。なので、インスタンス化されたクラスが static なメソッドやフィールドを持つクラスをインポートしていれば、アクセス出来ます。自分自身が依存しているクラスですから当然です。ですが、static 領域はインスタンス管理・依存しているわけではありません。static 領域にとって、同じクラスのインスタンスはたくさんあって、static 領域はそれらインスタンスの判別はできません。参照されることはあっても何から参照されているかは知りようがないのです。これは Singleton やグローバル変数も同じです。static メソッドはクラスに関連付けられているため、クラスに依存関係(import)があればその、クラス内の任意の場所からアクセスできます。インスタンスフィールドはクラスに関連付けられているわけではなく、インスタンスに関連付けられているため、static メソッドからアクセスすることはできません。
3-3. それぞれのフィールドの違い
- static フィールド:インスタンス毎に異なる値に対してアクセスする必要がない場合や、共有・参照されるべき値を保持する
- インスタンスフィールド: インスタンス毎で異なる(必要な)値を保持する
そのため、static メソッドやフィールドは、オブジェクトの状態に依存しないため、ユーティリティクラスやユーティリティメソッドの実装に適しています。また、定数や共有変数を扱う場合にも便利です。
3-4. static メソッドとインスタンスメソッドの使い分け
- static メソッド:インスタンス毎に異なる様に持たせた値に対してアクセスする必要がない、共有・参照しなければならない場合
- インスタンスメソッド:インスタンス毎に異なる様に持たせた値に対してアクセスをしたい場合
- フィールドにアクセスしない場合はケースバイケース
- インスタンス化しなければならない理由があるのであればインスタンスメソッド
- インスタンス化の必要がなければ static メソッドで済ませればいい
- (ex. Integer.parseInt() )
3-5. static メソッド・static フィールドの使い所
- 全てのインスタンスが共有して使えるメンバフィールドやメソッドを定義したい場合
- 状態に依存しない値、定数など
- クラスに関連する共通なメソッドをまとめたい場合
- ex.Integer.parseInt()
上記の図のように、static 領域はヒープ領域とは別のメモリ空間なので、インスタンスフィールドの状態と static フィールドの状態は別々に管理されています。なので、どのインスタンスから参照しても同じ状態を参照するようにしたい場合などが挙げられます。下記にメソッドとフィールド別の使い所を記載します。
3-5-1. static メソッドの使い所
staticメソッドは、クラス全体に関係する共通の処理を行うために使用されます。
- エントリーポイント定義:アプリケーションエントリーポイントとして使用される main()メソッドは、必ず static メソッドで定義されます。その様な言語使用になっています。
- ユーティリティメソッド:インスタンスを必要としない共通の処理を実行する場合に利用されます。例えば、Math クラスの abs メソッドは、引数に渡された数値の絶対値を返します。このような処理は、インスタンス化する必要がないため、static メソッドとして実装されます。
- ファクトリメソッド:インスタンスを作成するためのメソッドを static メソッドとして定義することができます。例えば、Java の Collections クラスには、空のリストやイテレーターなどを作成するための static メソッドが用意されています。
~~~中略~~~
package java.util;
~~~中略~~~
public class Collections {
// Suppresses default constructor, ensuring non-instantiability.
private Collections() {
}
~~~中略~~~
/**
* The empty list (immutable). This list is serializable.
*
* @see #emptyList()
*/
@SuppressWarnings("rawtypes")
public static final List EMPTY_LIST = new EmptyList<>();
/**
* Returns an empty list (immutable). This list is serializable.
*
* <p>This example illustrates the type-safe way to obtain an empty list:
* <pre>
* List<String> s = Collections.emptyList();
* </pre>
*
* @implNote
* Implementations of this method need not create a separate {@code List}
* object for each call. Using this method is likely to have comparable
* cost to using the like-named field. (Unlike this method, the field does
* not provide type safety.)
*
* @param <T> type of elements, if there were any, in the list
* @return an empty immutable list
*
* @see #EMPTY_LIST
* @since 1.5
*/
@SuppressWarnings("unchecked")
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
~~~中略~~~
}
3-5-2. static フィールドの使い所
- 定数:static フィールドを定数として定義することができます。例えば、Java の Math クラスには、πの値を表す static フィールドが定義されています。
- 共有データ:クラスの全てのインスタンスで共有されるデータを static フィールドとして定義することができます。例えば、Java の Singleton パターンでは、インスタンスが一つしか存在しないことを保証するために、private で static なインスタンスを持つように実装されます。環境変数などが定義されたファイルやインスタンスなどの様に一度決まれば変更されない、もしくは変更される場合にそれを参照する対象全てが同期して欲しいものなど
インスタンスにはインスタンスに必要なフィールドしかない(のが理想的なカプセル化)はずです。つまりインスタンスメソッドとはインスタンスフィールドにアクセスをするのが本来の目的の一つです。それらを踏まえると、それぞれのメソッドがアクセスしたいフィールドは異なることが解ります。インスタンスメソッドがインスタンスフィールドを使用していない場合は、そのメソッドをそのクラスに置く必要はありません。インスタンスフィールドとは「インスタンスごとで異なる値・状態をもつ」ためにあります。インスタンスメソッド内で使用していないのであれば、意味がありません。対象ロジックの置き場所を再検討するべきサインです。逆に static なフィールドが特定の単一のオブジェクトからしかアクセスされていない場合は不適切な static フィールドであることを意味しています。static フィールドはみんなから参照されるべき共有値だからです。特定のオブジェクトからしかアクセスされないのであればインスタンスフィールドにするべきです。
また、以下の二点はパフォーマンスの観点から理解しておくべきです。
- static 領域のメソッドやフィールドは常に高速にアクセスできるということ
- static フィールドや static メソッドが多用されると、メモリ(static 領域)使用量が増加し、パフォーマンスが低下する可能性があるため、適切な使用方法を考慮する必要があること
3-6. static メソッドやフィールドは、オブジェクトの状態に依存しない
static メソッドやフィールドは、オブジェクトの状態に依存しないというのはどういう意味でしょうか。
インスタンスメソッドやフィールドは、インスタンスごとに異なる状態を持つため、オブジェクトをインスタンス化する必要があります。それに対して、static 領域にあるメソッドやフィールドは、プログラムの開始から終了まで、常にメモリに展開されています。つまり、オブジェクトのインスタンス化やメソッドの呼び出しに関係なく、プログラムが実行されている間は常にメモリ上に展開されています。
static メソッドやフィールドは、クラス自体に紐づいているため、インスタンス化されたオブジェクトの状態に依存しません。インスタンス化するのはインスタンスごとに異なる値を持つインスタンスフィールドが必要だからです。異なる値に依存する、つまり、状態に依存するのがインスタンスです。それに対して、static フィールドは常に定まった値を取り扱います。static メソッドは同じ引数に対して常に同じ値が返されます。ちなみに、この様な性質を関数型言語の世界では参照透過性と呼びます。インスタンスメソッドやインスタンスフィールドは参照透過性を持ちません。これらの要素は、インスタンスの状態に依存しており、同じ引数を渡しても、オブジェクトの状態が異なる場合は異なる結果を返す可能性があるからです。
static メソッドやフィールドはクラス自体に紐づいているため、非 static なオブジェクトやメソッドなどからはクラス単位で共有されています。MyClass.increment();
という形で呼び出せます。
注意点として、static フィールドや static メソッドはクラス単位で共有されるため、複数のオブジェクトが同じ static フィールドを参照している状態が常に起こり得ます。その場合、一方のオブジェクトが static フィールドの値を変更すると、他方のオブジェクトにも影響が及びます。そのため、static フィールドはスレッドセーフな必要があります。
余談:参照透過性と Stream API
例えば、以下の式は参照透過性を持ちます。
x + y
これは、与えられた同じ x
と y
に対して、必ず同じ結果を返します。
一方、以下の式は参照透過性を持ちません。
getRandomNumber()
これは、呼び出すたびにランダムな値を返すため、同じ引数であっても呼び出すたびに結果が異なります。
参照透過性がある関数や式は、プログラムの理解や変更、テスト、最適化などの面で非常に有用です。また、関数型プログラミングにおいては参照透過性が重視されます。
StreamAPIなどはこの性質が取り入れられています。Stream APIには、ストリーム生成、中間操作、終端操作の3つの段階があります。ストリーム生成ではデータソースからストリームを生成し、中間操作は新しいStreamを返し、終端操作はStreamから結果を取得します。中間操作は入力Streamを変更せず、新しいStreamを生成するため、参照透過性を持ちます。また、終端操作は常に同じ入力に対して同じ結果を返すため、参照透過性を持ちます。このように、Stream APIは関数型プログラミングの考え方に基づいて設計されており、参照透過性が基本的には保証されています。プログラムの中で状態を変更せずにデータを扱うことができ、より安全で保守性の高いコードを書くことができます。
この「スレッドセーフな必要がる」という点が、今回の SonarLint の警告内容の理解に必要な重要知識となります。
3-7. staticメソッドの注意点
3-7-1. 参照共有・アンスレッドセーフ問題
上記でも記述しましたが、static フィールドや static メソッドはクラスに紐づくことから、他のオブジェクトからはクラス単位で共有されます。共有する方法はクラスに依存関係を追加(import)すればいいわけです。複数のオブジェクトが同じ static フィールドを参照している状態が常に起こり得ます。その場合、一方のオブジェクトが static フィールドの値を変更すると、他方のオブジェクトにも影響が及びます。そのため、static フィールドはスレッドセーフな必要があります。スレッドセーフについては後述します。今回の SonarLint 警告の中核はこの問題です。
つまり、1 つのスレッドが static フィールドを変更しているときに、別のスレッドがそのフィールドにアクセスしても、不正な結果にならないようにする必要があります。不正な結果とは、期待した結果以外の結果です。たとえば、count フィールドを 1 回インクリメントする予定が、2 回インクリメントされた場合、これは不正な結果です。これは、1 つのスレッドがフィールドを変更しているときに、別のスレッドがフィールドにアクセスしたために発生する現象です。
マルチスレッドをスレッドセーフにするためには一般的に、synchronized や volatile などの手段を使って、適切に排他制御を行う必要があります。それと同じく static フィールドの値を変更する場合も同じく、他のスレッドとの競合を避けるために、適切な排他制御を行う必要があります。
マルチスレッドについては次の章から説明します。
3-7-2. DIがしづらくテスタビリティが低い(おまけ:本編関係なし)
DI(Dependency Injection)とは、オブジェクトに依存関係を注入する手法です。依存関係とは、オブジェクトが正常に機能するために必要なオブジェクトやデータのことです。DIは「依存性注入」と訳されますがより実態を示しているのは「依存性オブジェクトの注入」ではないかと思います。より平たく言えば、「そのクラスが依存しているオブジェクトの外部からのインスタンス注入」となります。
-
モックやスタブの注入: DIは、依存関係を注入するメカニズムを提供します。これにより、ユニットテスト中にモックやスタブの実装を注入することができます。モックやスタブは、テスト中に予測可能な動作や返り値を提供することができます。これにより、テストケースの制御が容易になり、テストの信頼性を高めることができます。
-
依存関係の分離: DIによって依存関係が明示的に宣言されるため、クラス間の結合度が低くなります。これにより、単体のクラスをユニットテストする際に、そのクラスが依存する他のクラスやリソースの実装詳細を意識する必要がありません。代わりに、依存関係をモックやスタブに置き換えることができます。
-
テストの容易な再構成: DIを使用すると、依存関係の注入が外部で行われるため、テスト中に異なる実装を注入することができます。例えば、本番環境では実際のデータベースを使用するが、テスト環境ではインメモリデータベースを使用するなどの設定が可能です。これにより、テストの再構成が容易になります。
static メソッドは、インスタンスメソッドとは異なり、クラス自体に関連付けられているメソッドであり、インスタンスを生成せずに呼び出すことができるメソッドでもあるため、DIが困難となります。また、static メソッドは、単体テストも困難です。単体テストでは、オブジェクトをモック(代役)に置き換えてテストを行う必要があります。DIはオブジェクトに依存関係を注入する手法であり、モックはテスト中に依存関係をシミュレートするために使用できるオブジェクトです。
モックを生成する際には、DIでテスト対象のインスタンスメソッドがあるインスタンスを生成し依存関係を設定できる状態することでモックの準備が整います。ですが、上記で紹介した様に、インスタンスメソッドはヒープ領域にクラスデータを展開することでアクセスできます。対して、static メソッドは static 領域に展開されます。このようなメモリ領域の関係により、DIがしづらく、よって、モックに置き換えることも面倒というわけです。
DIはデザインパターン
引用元:terasoluna.org 2.4. アプリケーションのレイヤ化
あとは、いろんな議論でユニットテストの普及目的で導入された行ったけど実はそんなに効果がなかったとかいろんな議論がありますが、二年目のひよっこの自分には全く意味わかりません。
3-7-3. static メソッドのユニッテスト方法(おまけ:本編関係なし)
実は Mock で対応してます。対応以前はどうやってたんでしょうかね。多分以下で紹介する一番目のやり方でしょうか?
PowerMock でも出来たみたいですが、JUnit5って実は PowerMock 使えないんですよね、、、。だから、private メソッドのMock化とかも出来ません。そして、今から JUnit 入れるとかであれば JUnit4 をわざわざ選択する必要ってないと思います。いつサポート切れるか分かりません。
- 静的メソッドをラップするインスタンスメソッドを作成する
- モックライブラリを使用して静的メソッドをモックする
以下は「静的メソッドをラップするインスタンスメソッドを作成する」
ぶっちゃけ、やってられんですよねこんなの。テストの為だけにラップするって何?という思いが禁じ得ないのですが、使用すべき場面ってあるのでしょうか??
public class Sample {
private static String getFullName(String firstName, String lastName) {
return String.format("%s %s", firstName, lastName);
}
public String getFullName(String firstName) {
return getFullName(firstName, "");
}
}
以下は「モックライブラリを使用して静的メソッドをモックする」
@Test
public void testSample() {
// Mockito を使用して、Sample クラスのモックオブジェクト作成
try (MockedStatic mocked = mockStatic(Sample.class)) {
// `Math` クラスの `random()` 静的メソッドの期待値を設定
mocked.when(Sample::method).thenReturn("bar");
// 静的メソッドを呼び出す
assertEquals("bar", Sample.method());
// Mockito を使用して、静的メソッドの期待を検証
mocked.verify(Foo::Sample);
// もし、try-with-resources で実装しない場合、明示的に close() を呼ぶ
// 以下の公式サイトでは try-with-resources を推奨
// mocked.close();
}
assertEquals("foo", Foo.Sample());
}
参考:Package org.mockito Class Mockito「48. Mocking static methods (since 3.4.0)」
3-8. 改めて SonarLint の警告内容を見てみましょう
[Java] S2696 メソッドが同期化されているが静的でない場合、問題が発生する。
メソッドを "static "にするか、このセットを削除してください。静的でないメソッドから静的フィールドを正しく更新するのは難しいことで、複数のクラスインスタンスや複数のスレッドが存在する場合、簡単にバグにつながる可能性があります。理想的には、静的フィールドは同期化された静的メソッドからしか更新されません。
このルールは、静的フィールドが非静的メソッドから更新されるたびに、問題を発生させます。
この警告が言っているのは、複数のインスタンスで共有されているような 静的なフィールドが、非静的なメソッド(つまりインスタンスメソッド)から状態を変更されてしまうと、変更前の値を参照したいはずの無関係な他のインスタンスにも影響が出る状態やから、その状態をなんとかせい、と言っているわけですね。
その解決策として、非静的なメソッドを同期的なメソッドに変更するというものでした。つまりsynchronized
をメソッドにつけるというものでした。
この、synchronized
をつけると何が起きるのでしょうか?
- 排他制御が行われる様になる
- 排他制御として
synchronized
はミューテックスである - 複数スレッドが同時に
synchronized
ブロックにアクセスしても、操作は一度に一つのスレッドのみとなりアトミックとなる。 - 複数スレッドが同時に
synchronized
ブロックにアクセスしても、排他制御によりスレッドセーフとなる。 -
synchronized
ブロック内でのクリティカルセクションの変更結果が他のスレッドに対して可視になる。 - ただし、ミューテックスの使用箇所によってはパフォーマンスを低下させる可能性が高い
いやぁ、すごいですねsynchronized
。実はsynchronized
だけでは、実務上で様々な問題を引き起こすので「スレッドセーフ?synchronized
つけといたらいいんでしょ?」みたいな感じで適当にやっていると致命的なパフォーマンス低下や最悪システムの停止につながる様なデッドロックを引き起こす可能性もあります。ただし、本記事ではそこまで説明しません。この解説の粒度でやっていたらとんでもない長さになります。それに実務的知見も全くないので、またいつかやりたいところです。
以降では上記で挙げたsynchronized
キーワードを付けることで起こること、の解説の為に必要な基礎知識について解説します。とんでもなく多いです。量が。申し訳ない。簡潔さと詳細さを取る場合、私は詳細さを取る人間です。
余談:やや不正確な表現だったものの補足
「static 領域にあるメソッドやフィールドは、プログラムの開始から終了まで、常にメモリに展開されています」は、より正確には、JVMは動的にメモリを割り当てるため、static 領域にあるメソッドやフィールドが常にメモリ上に展開されていることは、やや不正確です。
4. マルチスレッドの基本的な話
マルチスレッドには多くの用語が登場するので語彙の定義をまず示したいと思います。
-
※参考
- Effective Java 第3版 Tankobon Softcover by Joshua Bloch (著), 柴田 芳樹 (翻訳)
- Oracle マルチスレッドの概念
- Oracle マルチスレッドに関する用語の定義
- JPCERT Coordination Center「VNA00-J. 共有プリミティブ型変数の可視性を確保する」などなど
- 情報処理用語(装置技術)
- 第1回 マルチスレッドはこんなときに使う
- やり直しJava マルチスレッド
- mac でも ps コマンドでプロセスのツリーを表示したい(--forest)
- The Rust Programming Language 日本語版 恐れるな!並行性
- 並行処理(Concurrency) vs. 並列処理(Parallelism)
- ReadWriteLock (Java Platform SE 8 )
- 第57回 ロック編 ReadWriteLockインターフェース
- Condition (Java Platform SE 8 )
- 第56回 ロック編 Conditionインターフェース
- フリー百科事典『ウィキペディア(Wikipedia)』 デッドロック
- フリー百科事典『ウィキペディア(Wikipedia)』 Lock-freeとWait-freeアルゴリズム
- JPCERT Coordination Center:「VNA05-J. 64ビット値の読み書きはアトミックに行う」
- 『Java の理論と実践: volatile を扱う』を読んで
- [Java] volatile 変数
マルチスレッドプログラミングに関連する用語の定義と詳細を以下に解説します。
4-1. 図表
詳細な解説の前にまず簡単に定義を紹介します。
用語 |
定義 |
|
---|---|---|
プロセス(Process) |
命令(プログラム)の実行単位。実行中プログラムのインスタンスであり、メモリ空間やCPUなどのシステムリソースを割り当てられた単位。 |
|
スレッド(Thread) |
プロセスの軽量な実行単位として登場した「軽量なプロセス」である。スレッドはプロセスに含まれる。あるプロセスの既に作成されたメモリ空間を使用して新しいスレッドが生成されるため、スレッドの方が起動のオーバーヘッドが小さい。プログラム内で処理を行う独立した命令実行の流れを持つ単位で、CPUが処理する最小単位。プロセスと共有されるメモリ空間があるが、独自のスタックとCPUレジスタといった独自のメモリ空間を持つ。 |
|
メインスレッド(Main Thread) |
プログラムの最初に実行されるスレッドであり、プログラムの制御を担当。 |
|
スレッドセーフ(Thread Safe) |
複数のスレッドからメソッド・フィールドや共有データ(インスタンスなど)にアクセスされても問題が起きない状態、もしくは複数スレッドからアクセスされることを想定し排他制御が行われているようなクラスやメソッドを指す。 |
|
並列性(parallelism) |
2 つ以上のスレッドが同時に実行されている状態を表す概念。 |
|
並行性(concurrency) |
2 つ以上のスレッドが進行過程の実行状態にあることを表す概念。一般化された形の並列性で、疑似的に複数の処理が同時に実行されているように見える。 |
|
シングルスレッド化 |
1 プロセス 1 スレッドで動作させること。ある処理を単一のスレッドのみを用いて動作させる環境もしくは手法 |
|
マルチスレッド化 |
1 プロセス複数スレッドで動作させること。複数のスレッドが同時に動作することで、並行処理や並列処理を実現できる。 |
|
クリティカルセクション (critical section) |
複数の処理が同時期に実行されると競合状態を起こす単一の共有データ、コードセクションを指す。つまり、複数のスレッドが同時にアクセスしてはならないデータまたはリソースのこと。 |
|
同期制御 (Synchronization) |
クリティカルセクションへのアクセスを制御するプロセス。複数のプロセスやスレッドが同時に実行される場合に、実行順序やタイミングを制御すること。排他制御を包含する。 |
|
排他制御 (Exclusive Control) |
排他制御とは、クリティカルセクションへの複数プロセス(またはスレッド)が同時に入ることを防ぐ。複数のプログラムスレッドやプロセスが同時に共有リソースにアクセスすることを防ぐために、リソースの使用権限を制御する。 |
4-2. プロセス(Process)
定義
命令(プログラム)の実行単位。プログラム実行時、OSからメモリを割り当てられ、プログラムコード、データ、スタックなどの情報を保持する仮想アドレス空間を持つ。実行中プログラムのインスタンスであり、メモリ空間やCPUなどのシステムリソースを割り当てられた単位。
解説
プログラムが実行されるためにはプログラムのコードやデータ・スタックなどがメモリ上に展開される必要がある(上記で記述済み)。メモリ上にプログラムが展開される(インスタンスや static)ことで、CPUがそれらを実行可能となる、プログラムが実行されている状態を指します。プロセスは、このような実行中のプログラムを表す仮想的な概念。OSが提供する様々な機能やリソースを利用して、プログラム実行環境を構築する。
プロセスは、(複数の)スレッドを含めた複数の命令実行の流れを持つ。新たなプロセスを動作させるためには、CPUやメインメモリ上のアドレス空間などの計算資源(リソース)を割り当てる。これは都度、親プロセスをfork(copyと同義)して、子プロセスとして独自の仮想メモリを割り当てるため親プロセスを頂点としたツリー構造を構築する。そのため各プロセスは独立しており、他のプロセスのメモリに直接アクセスできない。プロセス間でデータを共有するには、共有メモリやパイプ、ソケット通信などのメカニズムを使用する必要がある。
ただし、マルチプロセスであろうが共有データ(DBやファイルなど)への対応はマルチスレッドと同じくデータ不整合・競合状態(競合条件)が発生しないよう、適切な同期処理が必要。上記での話はあくまでプロセスのメモリ空間の話。
4-3. スレッド(Thread)
定義
プロセスの軽量な実行単位として登場した「軽量なプロセス」である。スレッドはプロセスに含まれる。あるプロセスの既に作成されたメモリ空間を使用して新しいスレッドが生成されるため、スレッドの方が起動のオーバーヘッドが小さい。プログラム内で処理を行う独立した命令実行の流れを持つ単位で、CPUが処理する最小単位。プロセスと共有されるメモリ空間があるが、独自のスタックとCPUレジスタといった独自のメモリ空間を持つ。
解説
スレッドはプロセス内で並行して実行できる命令の流れ。プロセスはスレッドの集合であり、同じプロセスに属するスレッド間の通信は簡単に実行できる。なぜなら、それらのスレッドはメモリ空間を含めあらゆるものを共有しているから(共有メモリ形式)。したがって、あるスレッドで生成されたデータを、他のすべてのスレッドがただちに利用できる。同一プロセス内の複数スレッドを同一メモリ空間上で実行でき、メモリ消費量などが軽減できるしスレッドの切り替えに要する時間も、プロセスの切り替えに要する時間よりも短くて済む。スレッド切替にはメモリ空間を切り替える必要がない。同じデータにアクセスしながら並行動作するような複数の処理には、マルチスレッドを使った方がプログラミングは断然楽になる。共有メモリとして利用できるのは ヒープ領域に置かれたインスタンスやクラスフィールドなどになる。
ただし、これがスレッドセーフかどうかを引き起こす原因である。単一プロセスに対するマルチスレッド処理プログラミングにおいて、同じデータを複数のスレッドが同時に書き換えることによる不整合に注意し、同期・排他制御、スレッド間データ共有を防ぐか不変オブジェクトの使用などを行う必要が発生する。
-
スレッドを使用するメリット
- パフォーマンス向上
- 複数のタスクを同時に実行できるため、パフォーマンスを向上させることができる。
- 応答性の向上
- ユーザーの入力にすぐに応答できるため、応答性を向上できる。
- スケーラビリティの向上
- スレッドは、複数のコンピューター上で実行できるため、スケーラビリティを向上させることができる。
- パフォーマンス向上
-
スレッドを使用するデメリット
- 複雑さの増加
- スレッドは複雑になる可能性があるため、管理が難しい場合がある。
- 競合の可能性
- スレッドは競合する可能性があるため、予期しない結果が発生する可能性がある。
- メモリ使用量の増加
- スレッドはメモリを大量に消費する可能性があるため、メモリ使用量が増加する可能性がある。
- 複雑さの増加
4-4. プロセスとスレッドの違い
解説
OSによってプロセスは管理され、プロセスによってスレッドは管理される。スレッドはプロセスの内部での話。プロセスがスレッドを含んでいるという関係で、単一プロセスは複数スレッドを持つことができる。そして複数のスレッドが1プロセス内で実行されることをマルチスレッドと呼ばれる。
また、マルチプロセスはプロセスごとにメモリ空間が独立しているため、あるプロセスから別のプロセスが参照しているメモリに直接アクセスはできない。対してマルチスレッドの場合、あるプロセスの既に作成されたメモリ空間を使用して新しいスレッドが生成される。単一空間内のメモリを共有しながら複数の処理を行なう「共有メモリ方式」である。
プロセスとスレッドの主な違いは、プロセスは独立した実行単位であり、スレッドはプロセス内の依存関係のある実行単位であるということ。
プロセスとスレッドの利点・欠点は以下
- プロセス
- 利点
- セキュリティ:プロセスは相互に隔離されているため、セキュリティが向上する。
- プロセスは相互に隔離されているため、あるプロセスが別のプロセスに害を及ぼすことができない。
- スケーラビリティ:プロセスは複数のサーバーにスケーリングできる。
- 回復力:プロセスがクラッシュしても、他のプロセスには影響しない。
- セキュリティ:プロセスは相互に隔離されているため、セキュリティが向上する。
- 欠点
- 重量級:プロセスはスレッドよりも重いため、起動と実行に時間がかかる。
- 複雑さ:プロセスはスレッドよりも複雑であるため、管理が難しい場合がある。
- 利点
- スレッド
- 利点
- 軽量:スレッドはプロセスよりも軽量であるため、起動と実行が高速。
- 効率:スレッドは並行処理に使用できるため、効率を向上させることができる。
- 欠点:
- 競合の可能性:スレッドは競合する可能性があるため、予期しない結果が発生する可能性がある。
- 競合は、データの損失、予期しない結果、およびアプリケーションのクラッシュを引き起こす可能性がある。
- 複数のスレッドが同じアプリケーションを実行している場合、それらをすべて追跡することが困難になる可能性がある。
- 競合の可能性:スレッドは競合する可能性があるため、予期しない結果が発生する可能性がある。
- 利点
4-5. メインスレッド(Main Thread)
定義
プログラムの最初に実行されるスレッドであり、プログラムの制御を担当。
解説
メインスレッドが終了すると、プログラム全体が終了。
4-6. スレッドセーフ(Thread Safe)
定義
複数のスレッドからメソッド・フィールドや共有データ(インスタンスなど)にアクセスされても問題が起きない状態、もしくは複数スレッドからアクセスされることを想定し排他制御が行われているようなクラスやメソッドを指す。
解説
スレッドセーフでないリソースを複数スレッドからアクセスする場合には同期・排他制御を行わなくてはならない。
- アクセスされるリソースのクラスをスレッドセーフにする
- アクセスするクラスで排他制御を行う
一般には1つ目の方法のほうが、排他制御を行う場所を局所化できるという理由から望ましいと言われる。デッドロックの可能性、パフォーマンスの低下の際の原因検索や切り分けが容易になる。また、排他制御を行うコードを書く場所を凝集できるため、変更容易性も高い。
名著「Effective Java」では以下の様にスレッドセーフレベルが定義されている。
- 不変 (immutable) - このクラスのインスタンスは、定数のように見えます。 外部での同期は必
要ありません。例としては、 String Integer, BigInteger があります (項目15)。- 無条件スレッドセーフ (unconditionally thread-safe) - このクラスのインスタンスは可変です が、すべてのメソッドは、インスタンスが外部同期を必要とすることなく並行して使用できるよう に、十分な内部同期を含んでいます。 例としては Random や ConcurrentHashMap があります。
- 条件付きスレッドセーフ (conditionally thread-safe) - 無条件スレッドセーフと似ていますが、 安全に並行して使用するために、メソッドのいくつかは外部同期を必要とします。例としては、外 部同期を必要とするイテレータを持つ Collections.synchronized ラッパーが返すコレクションがあり、それらのコレクションのイテレータは外部同期を必要とします。
- スレッドセーフでない (not thread-safe) - このクラスのインスタンスは、可変です。並行して 使用するためには、クライアントは個々のメソッド呼び出し (あるいは、一連のメソッド呼び出し) をクライアントが選択している外部同期で囲まなければなりません。例としては、ArrayList や HashMap などの汎用コレクション実装があります。
- スレッド敵対 (thread-hostile) - このクラスは、たとえすべてのメソッドが外部同期で囲まれたとしても、並行した使用では安全ではありません。 一般に、スレッド敵対は、 static のデータを同期なしで変更することに起因しています。
マルチスレッドにおける不変オブジェクトについては以下の過去記事にて解説してます。
4-7. 並列性(parallelism)
定義
2 つ以上のスレッドが同時に実行されている状態を表す概念。
解説
並行性を包含する。違いは実行状態であり、並列はまさしく同時に実行されるタスクの数が複数ある状態を指す。例えば、マルチコアのCPUで複数のスレッドを同時に実行することや、複数のプロセスを同時に実行することが並列処理にあたる。対して、並行はタイムスライス毎にタスクをスイッチし、各タスクは常に実行状態にある。
4-8. 並行性(concurrency)
定義
2 つ以上のスレッドが進行過程の実行状態にあることを表す概念。一般化された形の並列性で、疑似的に複数の処理が同時に実行されているように見える。
解説
複数のプロセスやスレッドが同時に進行しているように見えるが、実際にはシステムリソースが単一のプロセッサ(CPU)やコアで共有され、タイムスライスで分割されて交互に実行されることを指す。タイムスライスとは、プロセッサの時間配分単位であり、あるプロセスが一定時間内に使用できるプロセッサ時間の量を表す。タイムスライスは人間にとってはかなり短く、プロセスやスレッドの切り替えは人間にとっては知覚できない時間感覚なので、プロセスは同時実行されているように感じる。
- 引用元(ちょい編集):並行処理(Concurrency) vs. 並列処理(Parallelism)
並行処理(Concurrency)とは、必ずしも同時に実行する必要はないが、多くのタスクを担当することができるシステム。順番に玉ねぎをきざんでフライパンに放り込み、それが炒め上がるまでの間にトマトを切ったりできます。
並列処理(Parallelism)とは、片手で玉ねぎの入ったフライパンを振りながらもう片方の手でトマトを切るようなもの
4-9. シングルスレッド化
定義
1 プロセス 1 スレッドで動作させること。ある処理を単一のスレッドのみを用いて動作させる環境もしくは手法。
4-10. マルチスレッド化
定義
1 プロセス複数スレッドで動作させること。複数のスレッドが同時に動作することで、並行処理や並列処理を実現できる。
4-11. 並列性と並行性の違い
以下の引用が分かり易すぎたのでそのまま引用しています。
このように、複数のCPUで処理を分担できれば、すべての処理が終了するまでの処理時間は向上するわけであるが、1つのCPUで並行処理をさせると、疑似的に処理を分担しているように見えるだけであるので、実際にはすべての処理が終了するまでの時間は変わらず、むしろ処理切り替えのために悪化する可能性もある。
上図では、処理Aと処理Bが両方とも終了するのは(3)のマルチCPUによるマルチスレッドが一番早く、(1)のシングルCPUによるシングルスレッドの場合と(2)のシングルCPUによるマルチスレッドの場合は(ほぼ)同じとなる。一方、処理Bの終了する時間に着目すると、(1)と(3)の場合がともに早く、(2)が一番遅い。つまり、(2)のシングルCPUによるマルチスレッドがパフォーマンスとしては、一番遅くなってしまうともいえるのである。
4-12. クリティカルセクション (critical section)
定義
複数の処理が同時期に実行されると競合状態を起こす単一の共有データ、コードセクションを指す。つまり、複数のスレッドが同時にアクセスしてはならないデータまたはリソースのこと。
解説
データベースや共有メモリ、ファイル、ネットワーク接続など、様々な形で表現されるリソース全般が対象になる。データの同一性が保証されなくなる可能性がある場合は、クリティカルセクションでは常に排他制御を行なう必要がある。プロセス内の共有データ(クリティカルセクション)に複数のスレッドがアクセスする可能性がある場合は、スレッド間の排他制御を行ない、アトミック性を確保する必要がある。
4-13. 同期制御(Synchronization)
定義
クリティカルセクションへのアクセスを制御する。複数のプロセスやスレッドが同時に実行される場合に、実行順序やタイミングを制御すること。排他制御を包含する。
解説
排他制御を行ううえで最も気を付けなくてはならないデッドロック。デッドロックはアプリケーション内部で排他制御などによる競合が起こり、アプリケーションが止まってしまう(反応がなくなってしまう)状態。そのためスレッド同士がタイミングを計って協調動作しなければならないときがある。このような制御を「同期制御」と呼ぶ。Java における同期制御の実現には以下の方法がある。
- synchronized ブロック(後述)
- synchronized メソッド(後述)
- volatile 変数(使用可能箇所は限定的)
- その変数への書き込みが、その変数の現在の値に依存しない
- その変数が、他の変数との不等式に使われない
- インクリメント演算には使えない
- Compare and Assignment には使えない
- 参考:『Java の理論と実践: volatile を扱う』を読んで
- 参考: [Java] volatile 変数
- Atomic クラス(本記事では解説なし)
4-14. 排他制御 (Exclusive Control)
定義
排他制御とは、クリティカルセクションへの複数プロセス(またはスレッド)が同時に入ることを防ぐ。複数のプログラムスレッドやプロセスが同時に共有リソースにアクセスすることを防ぐために、リソースの使用権限を制御する。
解説
共有データに同時アクセスすると、データ破損や予期しない動作が発生する可能性がある。そのため、プログラムが共有データにアクセスする際には、他のプログラムとの競合を避けるために排他制御を行う必要がある。排他制御には後述するロックを行う必要がある。ロックにはミューテックス、セマフォ、スピンロックなどの手法がある。Java における排他制御及びロックの実現には以下の方法などがある。
- synchronized キーワード
- Lock インターフェイス
排他制御はパフォーマンスの劣化を招く
排他制御を行うと、プログラムの実行パフォーマンスが悪くなる。
- 排他制御の仕組みそのものが原因
- マルチスレッドによる並行処理によってパフォーマンス改善の目指しているが、排他制御ではその並行処理を部分的に並行で動作しないように制御するということを行っている。つまりデータの整合性を保つために、部分的にマルチスレッドによるパフォーマンスの利点をつぶすことになる。
- lock インターフェイスによるパフォーマンス低下
- lock インターフェイストを実行してロックを取得する場合の実行コストは小さくない
基本的には、排他制御によるパフォーマンスの低下をできるだけ少なくするために、まずはロックによる排他制御を行わなくてもよいような設計を検討した方がよい。それでも排他制御を行わなくてはいけない個所においては、できるだけロックする範囲と時間を小さくするとよい。ただし、ロックフリーなアルゴリズムなど様々な方法があり、どの方法でも銀の弾丸はやはりない。
参考:フリー百科事典『ウィキペディア(Wikipedia)』 Lock-freeとWait-freeアルゴリズム
5. アトミック性と可視性
アトミック性と可視性は、Javaのマルチスレッドプログラミングにおいて非常に重要な概念です。同期・排他制御をコードで実現するためには必須の知識となります。
5-1. 図表
詳細な解説の前にまず簡単に定義を紹介します。
用語 |
定義 |
---|---|
競合状態(race condition) |
複数スレッドでの処理が、クリティカルセクションに同時アクセスした場合に、データの不整合が起き、結果的にシステム停止など予期しない処理結果が生じてしまうこと。競合状態になるとシステム全体の実行結果が各スレッドの処理の実行順序に依存する形になり、同じ入力を与えても、プログラム実行のたびに結果が変わる非決定的な動作となる。 |
ロック (lock) |
マルチスレッド環境における排他制御にて各スレッドのアクセス順序を決定する。クリティカルセクションへのアクセス制限を行い、不整合な状態が起こらないよう制御する手法の一つ。このアクセス制限を課す動作を「ロックする」、「ロックを取得する」などと表現する。 |
デッドロック (deadlock) |
排他制御によりロックされたクリティカルセクションに、他のユーザからアクセス要求が出された時、両者は互いに使用中のクリティカルセクションが解放されるのをブロック状態で待つという状況。この状態ではどのユーザも共有データの解放を待ったまま処理が進まずにプログラム停止状態となる。 |
アトミック性(Atomicity) |
異なるスレッドが共通のデータにアクセスするような複数の操作を、同時に一つのスレッドだけが処理し途中で割り込まれることがなく、最後まで完了することを保証する性質。アトミックな操作のことをこれ以上分けることができないとして「不可分操作」と呼ぶ。アトミック性を保証できない場合でのマルチスレッド環境では競合が発生しうる。 |
可視性(Visibility) |
複数のスレッドが共有する変数が、スレッド間でどのように見えるかを示す。あるスレッドが共有変数に書き込みをした場合、他のスレッドがその変数の値を読み込むときには、その書き込みが見えない事がある。可視性が保証するために、スレッド間での明示的な同期が必要になる。 |
5-2. 競合状態(race condition)
定義
複数スレッドでの処理が、クリティカルセクションに同時アクセスした場合に、データの不整合が起き、結果的にシステム停止など予期しない処理結果が生じてしまうこと。競合状態になるとシステム全体の実行結果が各スレッドの処理の実行順序に依存する形になり、同じ入力を与えても、プログラム実行のたびに結果が変わる非決定的な動作となる。
解説
競合状態を解防止するには、対象となるクリティカルセクションの独占権を保証すること。つまり適切な排他制御が必要となる。リソース独占のために、ロックという処理を行いアトミック性(後述)を保証する。非決定的な動作は非アトミックな操作と同義。ロックを正しく扱わないと、デッドロックを起こしえる。デッドロックは、お互いにロックされたリソースの解放を待ってしまい、処理が進まなくなってしまうこと。ロックを同じ順序で取得するように設計すれば、予防できると言われている。競合し整合性が失われたデータがDBなどにより永続化された場合、どのデータが間違っていて、いつどこで整合性が失われたのか調査するのは非常に難しい。
Twitter API の突然の仕様変更などでユーザーがサービスを使用できないみたいな混乱が多々あったのは記憶に新しいところ。そんな中で見かけたのだけど、その様な状態を予想して、細かい区間でログやキャッシュを残して後々サルベージしやすい様にして、ユーザがアクセスできなかった間の記録をサルベージしている企業もあったけどどんなアーキテクチャで実現してたんだろう。
5-3. ロック (lock)
定義
マルチスレッド環境における排他制御にて各スレッドのアクセス順序を決定する。クリティカルセクションへのアクセス制限を行い、不整合な状態が起こらないよう制御する手法の一つ。このアクセス制限を課す動作を「ロックする」、「ロックを取得する」などと表現する。
解説
あるスレッドがロックしたクリティカルセクションへは、基本的には他のスレッドによる利用は妨げられる。実際には、完全に利用をさせないロックは性能低下が著しいため、複数の主体が取得可能なロックや、他者の読み出しのみ許可するなど、複数のモード(レベル)のロックを用意し必要に応じて使い分ける。(ミューテックス・セマフォ・共有ロック・占有ロックなど)
ロックは、スレッドがクリティカルセクションにアクセスする前にロックを取得し、アクセスが完了したらロックを解放することによって機能する。スレッドがクリティカルセクションをロック解除すると、他のスレッドがロックを取得できるようになる。これにより、複数スレッドがクリティカルセクションにアクセスしようとしても、一度に一つのスレッドしかアクセスできないようにすることができる。
5-3-1. 簡易解説:ロック関連
単一ロック(Mutex)
定義
クリティカルセクションへのアクセスを保護するために使用される同期手法。クリティカルセクションを1つのスレッドのみがアクセスできるようにする。
解説
セマフォはクリティカルセクションの数を制御するために使用され、ミューテックスはクリティカルセクションへのアクセスを保護するために使用される。
ミューテックスは、ロックとアンロックの2つの基本操作があり、1つのスレッドがミューテックスをロックすると、他のスレッドはそのミューテックスをロックすることができない。ミューテックスを使用することにより、クリティカルセクションに対する同期が可能になり、競合状態やデータ競合などの問題を回避することができる。単一のスレッドしかロックを取得できないロック。
セマフォ(Semaphore)
定義
クリティカルセクションの数を制御するために使用される同期手法。クリティカルセクションを使用できるスレッドの数を制御するために使用。カウンティングセマフォとバイナリセマフォの2種類がある。
解説
セマフォはクリティカルセクションの数を制御するために使用され、ミューテックスはクリティカルセクションへのアクセスを保護するために使用される。
カウンティングセマフォは、特定のクリティカルセクションの利用可能な数を表す非負整数値であり、複数のスレッドやプロセスが同時にアクセスできる。初期値を3と設定すれば、三つのスレッドまでがクリティカルセクションにアクセス可能となり、4つ目のスレッドがアクセスしてきた場合、そのスレッドは順番待ちになる。バイナリセマフォは、利用可能なクリティカルセクションが1つしかない場合に使用される。0または1の値を取り、1の場合は利用可能であり、0の場合は利用不可である。バイナリセマフォは、ミューテックスとほぼ等価。
バイナリセマフォとミューテックスの違い
参考:フリー百科事典『ウィキペディア(Wikipedia) :セマフォ
- ミューテックスは2つの実行単位(プロセスやスレッド)が同時にクリティカルセクションにアクセスするのを防止する
- 多くは「所有者」の概念がある。ミューテックスをロックしたスレッドのみがそれをアンロックすることができる。ミューテックスは実行単位などの実行実体と結びつく。
- バイナリセマフォは単一のクリティカルセクションへのアクセスを制限する
- セマフォはクリティカルセクションと結びついている。
ミューテックスはリソースを排他的に使用する。一度に1つの実行単位だけがリソースを使用できることを意味します。
対してリソースへのアクセスを制限することは、複数の実行単位がリソースを使用できますが、一度に使用できる実行単位の数を制限することを意味します。
データベースは共有リソースと見なせます。ミューテックスを使用してデータベースを排他的にロックすると、データベースに同時にアクセスできるスレッドは1つだけです。セマフォを使用してデータベースへのアクセスを制限すると、データベースに同時にアクセスできるスレッドは、セマフォのカウンタ値によって決まります。たとえば、カウンタ値が3の場合、3つのスレッドが同時にデータベースにアクセスできます。
気をつけなくてはいけないのが、ミューテックスです。むやみやたらにミューテックスを使用すると、マルチスレッドの利点である効率性を大きく損なう可能性があります。スレッド数が多い処理に単一のスレッドしかアクセスを許さないのであれば、そこがボトルネックになってしまうからです。
読み取りロック(共有ロック) / 書き込みロック(専有ロック)
定義
共有データに対して、複数の読み取り専用アクセスを許可するが、共有データを変更する場合は、排他的なアクセス(一度に一つのスレッドのみ)を許可する。
解説
複数スレッドが共有データを同時に読み取り、書き込む場合、競合状態の発生が懸念される。読み取りロックは、複数スレッドが同時にデータを読み取ることは許可するが、データ変更は許可しないため、競合状態を回避する。書き込みロックは、データを変更するスレッドが存在する場合にのみロックを解除し、他スレッドが同時にデータを書き込むことを防ぐ。一般的な同期プリミティブ書き込み側がいつまでもロックを獲得できない事態を避けるために、書き込みが読み取りに優先するように実装することも考える。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
条件変数 (condition variable)
定義
状態が変化するまでスレッドをブロックする機能。スレッドが特定の条件を満たすまで待機する。
解説
あるスレッドがある変数の値を待つように設定された条件変数に対して待機することができる。別のスレッドがその変数の値を変更した場合、条件変数を通じて待機しているスレッドに通知する。これにより、スレッドが相互に連携して処理を行うことができ、競合状態を避けながら効率的にクリティカルセクションを共有できる。通常、ミューテックスやセマフォなどの同期オブジェクトと併用して使用される。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
5-4. デッドロック(Deadlock)
定義
排他制御によりロックされたクリティカルセクションに、他のユーザからアクセス要求が出された時、両者は互いに使用中のクリティカルセクションが解放されるのをブロック状態で待つという状況。この状態ではどのユーザも共有データの解放を待ったまま処理が進まずに停止状態となる。
解説
複数のスレッドがロックを獲得しようと競合し、どのスレッドもロックを獲得できない状態。スレッドはロックを解放せず、プログラムが停止する。デッドロックの回避には『単一ロック(Mutex)』『ロック順序を守る』『タイムアウト機能』などがあるが、デッドロックが発生した場合に備えデッドロック検知・自動回復機能の実装も重要。
引用:フリー百科事典『ウィキペディア(Wikipedia)』 デッドロック
上記の図はスレッド・クリティカルセクション・ロックオブジェクト・デッドロックを示している。
- 移動する青玉
- →スレッドもしくはプロセス(実行単位)
- 真ん中の大玉
- →クリティカルセクション(複数の実行単位がアクセスする共有リソース)
- スレッドがクリティカルセクションに入るときに出てくる黄玉
- →ロックオブジェクトとその取得
- ロックの取得が行われると他のスレッドはクリティカルセクションへのアクセスをブロックされる
- スレッドがクリティカルセクションから出ていく時に戻っていく黄玉
- →ロックオブジェクトの解放
- ロックが解放されると、次のスレッドがロックを取得
- クリティカルセクションへアクセスし処理が終わればロックを解放
- 最後
- 四つのスレッドが同時にアクセスし、同時にロックを取得する
- デッドロック発生
- リソースの持ち合いによるデッドロック
複数のスレッドがお互いに必要なクリティカルセクションのロックを同時期に占有し、他方のクリティカルセクションの解放を待つ状態で、どちらのスレッドもクリティカルセクションを解放しないために前に進めなくなってしまうケース。上記のGifの状態。
基本的にデッドロックはクリティカルセクション数が2以上の場合に発生する。クリティカルセクションが1の場合、セマフォ等はバイナリセマフォとなり、振る舞いはミューテックスと同じになるのでデッドロックは発生しない。ただし、ライブロックと言われる状態になりうる。デッドロックと似ているが、ライブロックはスレッドの実行は行われている。ただし、大きなパフォーマンス低下を引き起こす。ライブロックは、2人の歩行者が狭い道ですれ違おうとしている状況に例えることができる。歩行者は、お互いが相手の動きを予測して動こうとしますが、うまくいかず、何時間もすれ違えないという状況になる。
クリティカルセクション数を1にすることは、デッドロックを回避する根本的な解決方法であるが、その場合プログラムの並列性は著しく損なわれるため、現代のコンピュータープログラミングにおいて現実的な手段とは言えない。ミューテックスは現在使われることはほとんどない様ですが、概念として理解しておく必要がある。
5-4-1. デッドロックを回避する手段
デッドロックを発生させるには4つの条件が同時に満たされている必要があるため、いずれかの条件を1つでも崩すことができればデッドロックの発生を防ぐことができる。
- 相互排除(Mutual Exclusion): リソースが排他的に占有され、他のスレッドやプロセスなどの実行単位が同時にそのリソースを利用できない状態。デッドロックが発生するためには、複数のロックが存在し、それらのロックを獲得することができる状況が必要。
- 保持と待ち(Hold and Wait): 実行単位が少なくとも1つのリソースを占有したまま、他のリソースの解放を待っている状態。スレッドが少なくとも1つのロックを獲得した状態で、他のロックを待っている場合、デッドロックのリスクがある。
- 入れ子になった要求(Nesting of Requests): 実行単位がすでに占有しているリソースの解放を待ちながら、他のリソースを要求している状態。つまり、リソースの要求が入れ子になっている状態。スレッドが既に占有しているロックの解放を待ちながら、他のロックを要求している場合、デッドロックが発生する可能性がある。
- 循環待ち(Circular Wait): リソースの待ち合わせが循環的な関係を持っている状態。複数のスレッドやプロセスがリソースを循環的に要求し合っている状態。複数のスレッドやプロセスが異なる順番でロックを獲得しようとしており、循環的な依存関係が形成され、デッドロックが発生する可能性がある。
循環的な依存関係(Circular Dependency)
循環的な依存関係(Circular Dependency)は、複数の要素やエンティティが循環的に依存し合っている状態を指します。つまり、AがBに依存し、BがCに依存し、CがAに依存するといったように、循環的な結び付きが存在することを意味します。
引用元:Why Circular Dependencies Between Java Packages are Bad?
例えば、AとBが互いに依存し合っている場合、Aの初期化にはBが必要であり、Bの初期化にはAが必要となるような関係です。このような場合、Aの初期化を待ってBを初期化する必要があり、一方でBの初期化も待ってAを初期化する必要があるため、どちらも進まずに相互に待ち合わせることになります。これが循環的な依存関係による問題の一例です。
循環的な依存関係は、ソフトウェア開発やデータベース設計などの領域でよく見られます。この問題を解決するためには、依存関係の再設計や解消が必要になります。例えば、依存関係の切断や抽象化、インターフェースの導入、依存性注入(Dependency Injection)の導入などが考えられます。これにより、循環的な依存関係を回避し、システムの正常な動作と進行を確保することができます。
上記を踏まえ、デッドロックを回避するには、以下の選択肢を状況によって使い分ける・組み合わせる必要がある。
- ロック順序の統一:複数のロックが必要な場合、特定の順序でロックを獲得することで、デッドロックを回避する。
- タイムアウト処理の導入:ロックの獲得が一定時間内に完了しなかった場合、ロックを開放し、処理を中断する。
- ロックの自動開放:あらかじめ決められた時間経過後に、ロックを自動的に開放する。
- ロックの共有:複数のスレッドが同時に読み込み処理を行う場合、排他制御をかけずに複数のスレッドが同時にアクセスできるようにすることで、デッドロックを回避する。
- リソース階層の確立:複数のリソースを使用する場合、リソースの階層を決め、上位リソースを先にロックすることで、デッドロックを回避する。
DBのトランザクションは上記の方法を複合的に使用した排他制御。トランザクションは、複数の操作を一つの論理的な作業単位として扱うための仕組みであり、デッドロックの発生を回避するために排他制御が行われる。トランザクションは、原子性、一貫性、分離性、持続性の特性を備えなければならない。
- 原子性:すべて成功するか、すべて失敗するか。一部が成功し、一部が失敗することはない。
- 一貫性:トランザクションが完了すると、DB整合性が保たれる。
- 分離性:各トランザクションは互いに分離され、トランザクションが完了する前に他のトランザクションの影響を受けない。
- 持続性:トランザクションがコミットされると、変更は永続化される。DBの電源が切れてもデータは揮発しない。
トランザクションは、ロックを使用してクリティカルセクションへのアクセスを保護。ロックの順序、タイムアウト処理、ロックの自動開放、ロックの共有、リソース階層を使用して、デッドロックを回避する。だが、それでも完璧ではなく、デッドロックが発生する可能性はなくせない。デッドロックが発生した場合は、データベース管理者が介入してデッドロックを解除する必要がある。
その他の参考としての引用:
フリー百科事典『ウィキペディア(Wikipedia)』 Lock-freeとWait-freeアルゴリズム
マルチスレッドプログラミングにおいて古典的な手法は、共有リソースにアクセスするときはロックをかけることである。ミューテックスやセマフォといった排他制御は、ソースコードにおいて共有リソースにアクセスする可能性のある領域(クリティカルセクション)を複数同時に実行しないようにすることで、共有メモリの構造を破壊しないようにする。もし、スレッドAが事前に獲得したロックを別のスレッドBが獲得しようとするときは、ロックが解放されるまでスレッドBの動作は停止する。
ロックの解放を待機するスレッドは、スリープやスピンといった手法で待機する。スリープ中はプロセッサを他のスレッドに空け渡すため、システム全体の負荷が下がるが、スリープの時間的な精度や分解能はオペレーティングシステムやプロセッサによって異なることがあり、またスリープから復帰する際に時間的オーバーヘッドが発生する。一方スピンによる待機(スピンロック)中は、スレッドはプロセッサを解放せず、システム全体に負荷をかけたままになる。
スレッドが停止することは多くの理由で望ましくない。まず、スレッドがブロックされている間は、そのスレッドは何もできない。そして、スレッドが優先順位の高い処理やリアルタイム処理を行っているならば、そのスレッドを停止することは望ましくない。また、複数のリソースにロックをかけることは、デッドロック、ライブロック、優先順位の逆転を起こすことがある。さらに、ロックを使うには、並列処理の機会を減らす粒度の粗い(すなわちクリティカルセクションが広い)ロックを選択するか、バグを生みやすく注意して設計しないといけない粒度の細かいロックを選択するかというトレードオフ問題を生む。
5-5. アトミック性(Atomicity)
定義
異なるスレッドが共通のデータにアクセスするような複数の操作を、同時に一つのスレッドだけが処理し途中で割り込まれることがなく、最後まで完了することを保証する性質。アトミックな操作のことをこれ以上分けることができないとして「不可分操作」と呼ぶ。アトミック性を保証できない場合でのマルチスレッド環境では競合が発生しうる。
解説
アトミックな操作とは、クリティカルセクションに対するマルチスレッド操作・複合アクション(インクリメントなど。後述)に対する適切は排他制御がなされている状態を指す。途中で別のプロセスやスレッドが割り込んできても、処理が途中で中断されたり、不正な状態になることがなく、データの整合性が保たれる必要がある。例えば、変数に対するインクリメント操作は、複数のスレッドが同時に実行しても、その結果が正しくなるようにアトミックに実行される必要がある。
アトミック性が保証されていない場合、一方のスレッドが変数を更新中に他方のスレッドが同じ変数にアクセスすると、変数の値が不正確な状態になり、意図しない結果が生じる可能性がある。Java ではsynchronized
キーワードやLock
インタフェースなどの機能が提供されている。複数のスレッドからアクセスされる共有メモリ内の変数に対して、複数の操作がアトミックに実行されることが保証される。synchronized
により排他制御が行われアトミック性が保証されるが、同時にsynchronized
は可視性との両方を確保する。
Java言語仕様は、単一の変数への読み込みと書き込みがアトミックであることを保証しているため、単純な変数の読み書きにsynchronized
を使う必要はない。
// 変数の書きこみはアトミック
int value = 1;
ただし、Javaでは、long型やdouble型の変数は64ビットのサイズ。32bitのアーキテクチャを持つシステムでも64bitの値を扱うことができるように、2つの32ビットの変数に分割する。そのため、原則としてアトミック性が保証されない。これらの型の変数に値を代入する場合、2つの32bit値に分割されてからメモリに格納されるため、複数のスレッドが同時にアクセスすると、意図しない結果が生じる可能性がある。このような場合、volatile修飾子を使用することで、他のスレッドからは64bitの1つの書き込みとして見えるようになりアトミックな動作を保証できる。
参考:JPCERT Coordination Center:「VNA05-J. 64ビット値の読み書きはアトミックに行う」
// 次の命令はアトミックではない。二つのバイト命令に変換される可能性がある。
long value = 1L;
// 次の命令はアトミック
volatile long value = 1L;
※「L」はlong型のリテラルであることを示すために使用される。Javaでは、整数リテラルの末尾にLまたはlを追加することにより、long型を表現する。Lを省略すると、Javaコンパイラは整数リテラルをint型として解釈する。
5-6. 可視性(Visibility)
定義
複数のスレッドが共有する変数が、スレッド間でどのように見えるかを示す。あるスレッドが共有変数に書き込みをした場合、他のスレッドがその変数の値を読み込むときには、その書き込みが見えない事がある。可視性が保証するために、スレッド間での明示的な同期が必要になる。
解説
-
volatile
はsynchronized
の軽量版 -
synchronized
はアトミック性・可視性を保証する -
volatile
は可視性のみを保証する -
volatile
はsynchronized
より実行時のオーバーヘッドが少ない -
volatile
はsynchronized
を使ってできることの一部しかできない
可視性の問題は、一方のスレッドが共有メモリ内の変数を更新しても、他方のスレッドからは更新後の値がすぐに見えない反映されないといった問題が発生すること。つまりそのスレッドは、共有変数の最新ではない値を得る可能性があるということ。最新の更新が反映された値を確実に得るためには、変数をvolatile
宣言するか、変数に対する読み書きを同期(synchronized
など)する必要がある。Javaでは、可視性の問題を解決するために、volatile
もしくはsynchronized
キーワードが提供される。volatile
とは揮発性という意味。
この可視性の問題は Java のメモリモデルの仕組みにより発生する。この辺りはリオーダーやらイントラスレッド・セマンティクスやらめちゃくちゃにややこしくて理解できなかったので、ざっくりと解説。
マルチスレッドの場合、それぞれのスレッドは独自のキャッシュメモリを持っている可能性がある。そのため、あるスレッドが共有変数を上書きしてもその結果はキャッシュだけに反映されてメインメモリには反映されない。他のスレッドからは結果をすぐに見られない状態になりうる。同じ共有変数にも関わらずプロセッサごとに異なる値に見えてしまい、スレッド間でデータの不整合が発生する。
volatile
をつけた変数に対しての読み込みや書き込みは、常にメインメモリから行われるため、キャッシュに値が残っている場合でも最新の値が見えるようになる。
ただし、volatile
だけを付けるケースは少ない上にかなり高度な理解が必要ようなる様です。詳細は以下を参照して下さい。ぶっちゃけよく解りません。
使い所としては、複数スレッド間で可視性を確保する場面。しかし複合操作におけるアトミック性を保証するものではないため 複合操作を排他的に行いたい場合には使うことはできないらしい。
- その変数への書き込みが、その変数の現在の値に依存しない
- その変数が、他の変数との不等式に使われない
- インクリメント演算には使えない
- Compare and Assignment には使えない
上記二点を満たすケースの実装としてvolatile の利用パターンは以下があるらしい。もはや良くわかりません。今はこういうのがあるんだなーって感じです。
- パターン1:ステータスフラグとしての利用
- パターン2:1度だけ安全に公開する
- パターン3:独立した観測結果の公開
- パターン4:volatile bean パターン
- パターン5:安価な読み書きロック(高度な利用例)
参考
『Java の理論と実践: volatile を扱う』を読んで
[Java] volatile 変数
6. これまでの要約
-
プログラム実行には実行内容をメモリ上に展開する必要がある
- ほとんどのパソコンはノイマン型コンピュータ
- HDDなどの補助記憶装置へのアクセスはオーバーヘッドが大きいのでさせない
- そのため、これらのコンピュータはプログラムを実行するために、メモリにプログラムを展開する必要がある。
- メモリは、プログラム実行中に処理されるデータの一時的な記憶領域。
- プロセッサは、プログラムの実行に必要な情報を得るためにメモリにアクセスする。
-
Javaには、プログラムが実行される際に使用される複数のメモリ領域がある。
-
主なメモリ領域は、スタック領域、ヒープ領域、static領域、コンスタントプール。これらのメモリの管理方法やデータの使用目的によって、メモリの空間が定義されている。
-
static領域の特徴と使い分けは以下の通り
- static領域は、クラススコープ内で static キーワード付きで変数宣言を行う。プログラムのどの部分からでも参照することが可能。
- static領域に格納されるデータは、プログラムの開始から終了まで有効。インスタンス化は必要ない。
- static領域には、プログラム全体で共有する変数や定数を格納する。
staticメソッドは、インスタンスを生成しなくても呼び出すことができるため、ユーティリティメソッドやファクトリーメソッドなどに利用される。しかし、static変数やメソッドを多用すると、プログラムの拡張性低下・メモリの無駄遣いになるため、必要最小限に使用することが望ましい。
-
static メソッドの使い所
- ユーティリティメソッド
- インスタンスを必要としない共通の処理を実行する場合。例えば、Math クラスの abs メソッドは、引数に渡された数値の絶対値を返す。このような処理は、インスタンス化する必要がないため、static メソッドとして実装される。
- ファクトリメソッド
- インスタンスを作成するためのメソッドを static メソッドとして定義する。例えば、Java の Collections クラスには、空のリストやイテレーターなどを作成するための static メソッドが用意されている。
- ユーティリティメソッド
-
static フィールドの使い所
- 定数
- static フィールドを定数として定義することができます。例えば、Java の Math クラスには、πの値を表す static フィールドが定義されている。
- 共有データ
- クラスの全てのインスタンスで共有されるデータを static フィールドとして定義することができる。環境変数などが定義されたファイルやインスタンスなどの様に一度決まれば変更されない、もしくは変更される場合にそれを参照する対象全てが同期して欲しいものなど。
- 定数
-
static 領域のフィールド・メソッドはどこからでもアクセスできる
- 他のクラスからアクセスする際には、以下のようにクラス名を指定して参照可能。オブジェクトをインスタンス化する必要はない。
MyClass.increment();
- 他のクラスからアクセスする際には、以下のようにクラス名を指定して参照可能。オブジェクトをインスタンス化する必要はない。
-
static メソッドやフィールドは、オブジェクトの状態に依存しない
- static 領域にあるメソッドやフィールドは、プログラムの開始から終了まで、常にメモリに展開されていてインスタンス化の必要はない。
- インスタンスごとに異なる値を持つインスタンスフィールド。異なる値に依存する、つまり、状態に依存するのがインスタンス。
-
staticメソッドの注意点
- 参照共有・アンスレッドセーフ問題
- static メソッドは、クラスのすべてのインスタンスで共有されるため、意図しない変更が発生する可能性がある。
- static メソッドは、他のスレッドから同時に呼び出される可能性があるため、スレッドセーフではない。
- 参照共有・アンスレッドセーフ問題
-
プロセス(Process)
-
スレッド(Thread)
- プロセスと同じくプログラムの実行の単位で「軽量なプロセス」。プロセス内で複数のスレッドが同時に実行できる
- スレッドはプロセスに含まれる
- 同じプロセスに属するスレッドはメモリ空間を共有するため、スレッド間通信が容易。
- スレッドはプロセス内で通信することができ、並行処理に使用できる。
- 同じデータを複数のスレッドが同時に書き換えることによる不整合に注意する必要がある。
- スレッドセーフかどうかわからない場合は、同期・排他制御、スレッド間データ共有を防ぐか不変オブジェクトの使用などを行う必要がある。
- プロセスと同じくプログラムの実行の単位で「軽量なプロセス」。プロセス内で複数のスレッドが同時に実行できる
-
プロセスとスレッドの違い
- スレッドはプロセスの内部での話であり、プロセスがスレッドを含む。
- プロセスは複数のスレッドを持つことができ、複数のスレッドが1つのプロセス内で実行されることをマルチスレッドと呼ぶ。
- プロセスとスレッドの主な違いは、プロセスは独立した実行単位であり、スレッドはプロセス内の依存関係のある実行単位である。
-
スレッドセーフ(Thread Safe)
- 複数のスレッドからメソッド、フィールド、または共有データにアクセスしても問題がないことを指す。スレッドセーフではない操作とは、非アトミックな操作を含む。
- または複数スレッドからアクセスされることを想定し排他制御が行われているようなクラスやメソッドを指す。
- スレッドセーフでないリソースを複数スレッドからアクセスする場合には、同期・排他制御を行わなくてはならない。
- 一般的には、リソースのクラスをスレッドセーフにする方法が望ましいとされる。
- 複数のスレッドからメソッド、フィールド、または共有データにアクセスしても問題がないことを指す。スレッドセーフではない操作とは、非アトミックな操作を含む。
-
クリティカルセクション・同期制御・排他制御
- クリティカルセクションは、複数の処理が同時に実行されると競合状態を起こす単一の共有データであり、コードセクションを指す。
- 競合状態とは、複数のスレッドが同じデータまたはリソースに同時にアクセスしようとする状況。競合状態は、同じ入力を与えてもプログラム実行のたびに結果が変わってしまう非決定的な動作であり、データの破損またはシステム停止につながる可能性がある。
- 同期制御は複数のプロセスやスレッドが同時に実行される場合に、実行順序やタイミングを制御すること。
- 排他制御を行うためには、ロックを取得し解放する必要がある。ただし、デッドロックに注意しなければならず、Javaでは synchronized や ReentrantLock などの方法がある。また、共有データへのアクセスには、スレッド間の排他制御を行い、アトミック性を確保する必要がある。
- ロックは、クリティカルセクションへのアクセスを制限する排他制御の一種。スレッドがクリティカルセクションにアクセスする前にロックを取得し、アクセスが完了したらロックを解放する。スレッドがクリティカルセクションをロック解除すると、他のスレッドがロックを取得できるようになる。
-
アトミック性(Atomicity)
- アトミックな操作とは、クリティカルセクションに対するマルチスレッド操作・複合アクション(インクリメントなど。後述)に対する適切は排他制御がなされている状態を指す。
- アトミック性とは、、複数のスレッドが共有する変数に、1つのスレッドが操作を開始してから完了するまで他の操作をブロックする性質。これは、複数のスレッドが同じ変数にアクセスするときに重要。不可分操作と呼ぶ。
- アトミック性が保証されていない場合は、複数のスレッドが同時にクリティカルセクションにアクセスした場合に競合しデータ不整合を引き起こす可能性がある。
- Java では、synchronized キーワードや Lock インタフェースなどの機能が提供されている。
-
可視性(Visibility)
- 視性とは、複数のスレッドが共有する変数に、1つのスレッドが値を書き込んだ後、他のスレッドがその値をすぐに読み取ることができることを保証する性質。
- volatile修飾子は、可視性を保証するために使用される。volatile 修飾子が付いた変数は、常にメインメモリから読み書きされるため、キャッシュに値が残っていても最新の値が見えるようになる。これにより、複数のスレッド間での可視性が保証される。
- ただし、volatile修飾子は複合操作のアトミック性を保証するものではないため、複合操作を排他的に行いたい場合には使えない。
7. synchronized
の意味
やっと本題のsynchronized
の話ができます。疲れましたね、私も疲れました。
では問題のコードと解決例のコードを再び見てみましょう。synchronized
が追加されているだけです。
今回の SonarLint 警告文は、端的に言えば
- 「static フィールドへのアクセスは synchronized で static なメソッドにのみ設定されるべきである」
つまり、static フィールド(クリティカルセクション)の操作をインスタンスメソッドでやるのは、クリティカルセクション(private static int count = 0;
の部分)に対するスレッドセーフではない操作(public static void doSomething()
)であり、競合を引き起こすプログラムだ、という訳です。
synchronized
をつけると以下の様な状態になります。
- 排他制御が行われる様になる
- 排他制御として
synchronized
はミューテックスである - ミューテックスの使用箇所によってはパフォーマンスを低下させる可能性が高い
- クリティカルセクションに対するアクセスを単一スレッドに制限する。その間他スレッドは全て停止する
- 排他制御として
- 排他制御により、複数スレッドが同時に
synchronized
ブロックにアクセスしてもスレッドセーフとなる。 - 複数スレッドが同時に
synchronized
ブロックにアクセスしても操作は、一度に単一のスレッドのみとなり、アトミックとなる。- スレッドセーフとアトミックは関連性が深い重要な概念だけど、あくまで別の概念である事に注意
- スレッドセーフはマルチスレッド環境下でも競合せず、期待される処理結果が得られること
- アトミックはあくまで不可分性であり、操作が一連の不可分な単位として実行されること
- スレッドセーフとアトミックは関連性が深い重要な概念だけど、あくまで別の概念である事に注意
-
synchronized
ブロック内でクリティカルセクションの変更結果が他のスレッドに対して可視になる。- 1つのスレッドが
synchronized
ブロックから抜けるときには、それまでの変更がメモリにフラッシュされ、他のスレッドから読み取れるようになる(Java のメモリモデルに依存した現象)
- 1つのスレッドが
それではsynchronized
について説明していきましょう。
7-1. synchronized
で排他制御しアトミックでスレッドセーフに
定義
1つのスレッドだけが、ある時点で1つのメソッドやブロック(クリティカルセクションを扱う)を実行していることを保証、オブジェクトの整合性を保つための排他制御の手段として使われる。また、複数のスレッドが共有する変数が、スレッド間でどのように見えるかといった可視性も保証する。つまり、synchronized
はアトミック性と可視性の両性質とスレッドセーフを保証する。synchronized
におけるロックはミューテックスである。
解説
synchronized
はクリティカルセクションを扱うコードに対して、ミューテックスによる排他制御を付加しそのコードが行う操作がアトミック操作・可視性があることを保証する。
synchronized
で上記を実現する方法は、以下の二通りです。
- synchronizedブロック:クリティカルセクションコードを synchronized で囲んで局所的に排他制御する
- synchronizedメソッド:メソッド全体を排他制御する
7-1-1. synchronizedブロック
public class MyClass {
private static int count = 0;
private static final Object lock = new Object();
public static void doSomething() {
// 排他制御が不要な処理;
....
synchronized (lock) {
// 排他制御が必要な処理;
....
}
// 排他制御が不要な処理;
....
}
}
上記のdoSomething()
はクリティカルセクションを扱うコードで、マルチスレッド環境において競合する可能性があります。その為、各スレッドにこのコードブロックはクリティカルセクションを扱うブロックであるという目印の様なものをつけます。それが以下で説明するロックオブジェクトです。
synchronized ブロックもしくはメソッドは開始時にロックを取得し、ブロック終了時にロックを解放します。ロックのことをロックオブジェクトと呼びます。ロックオブジェクトには、Objectクラスのインスタンスを使用します。Javaにおいて、すべてのクラスは暗黙的にObjectクラスを継承しているので、どのクラスインスタンスでもロックオブジェクトにすることが可能で、無関係なインスタンスでも構いません。目印の様なものですから。
ロックオブジェクトは、スレッドの実行優先権を表し、鍵の様な役割を持ちます。ロックしたい(ミューテックスなので、単一スレッドしかアクセスさせない)コードブロックを複数のスレッドからロックするためのオブジェクトで、ロックを取得したスレッドだけがクリティカルセクションで処理の実行が可能になります。この処理ブロックを実行できるスレッドは、一度にロックを取得した1つのスレッドだけです。synchronized におけるロックは基本的に早い者勝ちです。複数のスレッドがロックを獲得できずに獲得待ちをしている場合、ロックの獲得待ちに並んだ順番にロックを獲得できるわけではなく、次にどのスレッドがロックを獲得できるかは不定になります。キューのような順序管理の機能を期待することはできないため、順序管理を行いたい場合はその様な設定を別途用意する必要があります。
※すべてのロックが、クリティカルセクションへのアクセスを、単一スレッドのみに制限するわけではありません。目的や性質によって変わります。むやみやたらにミューテックスを使用すると、マルチスレッドの利点である効率性を大きく損なう可能性があります。スレッド数が多い処理に単一のスレッドしかアクセスを許さないのであれば、そこがボトルネックになってしまうからです。
synchronized (ロックオブジェクト) {
ロックされるコードブロック(ここの処理が行えるのは、上記ロックオブジェクトを取得できた1スレッドのみ)
}
7-1-2. synchronizedメソッド
public class MyClass {
private static int count = 0;
public static synchronized void doSomething() {
count++;
}
}
この場合、メソッドを呼び出すオブジェクトがロックオブジェクトになります。そのため、MyClass インスタンスの doSomething() メソッドを複数のスレッドから呼び出した場合、後に呼び出した方は、先に呼び出した方の処理終了を待つことになります(後に呼び出した方はブロックされます)。呼び出し側のオブジェクトがロックオブジェクトとなるので、複数のスレッドから同じオブジェクトの doSomething() メソッドを同時に呼び出した場合は排他制御が行われまが、異なるオブジェクトの doSomething() メソッドを呼び出しても当然ですが排他制御は行われません。
7-1-3. Java における synchronized は再入可能なロック
再入可能とは、同じスレッドが同じロックを解放することなく連続で複数回取得できるという意味です。これは、スレッドがロックを取得した後、ロックを解放せずに(デッドロックを発生させずに)、そのロックで保護されている任意の数のメソッドを呼び出すことができることを意味します。デッドロックとは、2 つ以上のスレッドが互いにロックを待っている状態です。その場合、スレッドはロックを取得できず、システム停止などを引き起こします。
public class MyClass {
private static int count = 0;
private static final Object lock = new Object();
public static void doSomething1() {
synchronized (lock) {
// 排他制御が必要な処理;
}
}
public static void doSomething2() {
synchronized (lock) {
// 排他制御が必要な処理;
}
}
}
このコードでは、同じスレッドが doSomething1() と doSomething2() の両方のメソッドを呼び出すことができます。doSomething1() メソッドが lock オブジェクトを保持している場合でも、doSomething2() メソッドは lock オブジェクトを保持できます。
ロックを解放せずに次のロックが持つメソッドを実行できるので、2 つ以上のスレッドが互いにロックを待機する状態を回避できます。スレッドがロックを保持したまま次の処理に移ることができるのは、再入可能という性質のためです。
ただし、再入可能なのは、同じスレッドが同じロックオブジェクトを取得する場合のみです。別々のスレッドから同じロックオブジェクトを取得しようとすれば、当然競合が発生しますし、同じスレッドが複数の異なるロックオブジェクトを取得しようとすると それぞれのロックの状況により獲得できたり待たされたりすることになります。その他、スレッド A がスレッド B が保持しているロックを待っていて、スレッド B がスレッド C が保持しているロックを待っていて、スレッド C がスレッド A が保持しているロックを待っている場合、デッドロックが発生します。この場合、スレッド A、B、C はロックを取得できず、フリーズします。
ちなみに、Go のミューテックスが再入可能ではありません。これもちなみにですが、GOは継承もクラスもありません。面白い設計思想ですね。また、Effective Java では再入可能に関する注意点として以下の様に語られています。
再入可能ロックは、マルチスレッドのオブジェクト指向プログラムの作成を単純化しますが、活性エラーを安全性エラーに変える可能性があります。
活性エラーとは上記で簡単に説明したライブロックのことです。安全性エラーとは、競合状態化における共有データ(クリティカルセクション)のデータ不整合のことで、カウント10000回をマルチスレッドで実行したら6000回くらいの結果になって、期待通り10000回カウントしないとかですね。
活性エラーはデッドロックとして表面化しますが、安全性エラーはコードが動作しているように見えるが正しく動作していないという問題を引き起こします。特に競合状態(race condition)により引き起こされる問題は、現象の再現性も低く、調査が非常に難しいことが多いです。どないせぇっちゅうのや
7−2. synchronized
の注意点
7-2-1. synchronizedメソッド と synchronizedブロック の違い
項目 | synchronized メソッド | synchronized ブロック | 解説 |
---|---|---|---|
ロック対象 | メソッドを実行するスレッド | ブロックを実行するスレッド | 同期領域を適切に定義することで、スレッドの同時アクセスをより柔軟に制御することができる |
ロックの取得・解放タイミング | メソッドが実行されるたび・メソッドの実行が完了するまで | ブロックが実行されるたび・ブロックの実行が完了するまで | 両者は異なる方法でロックを取得することができるが、ロックの解放に関しては同様の振る舞いを示す |
ロックの公開 | メソッドを利用する他のコードにも公開 | ロックオブジェクトを取得したコードのみ | 公開される範囲を制御。特に、信頼できないコードやオブジェクトを外部に公開する場合は、適切なロックオブジェクトの選択とロックの公開範囲の制限が重要。 |
ロックオブジェクトの指定 | 任意のロックオブジェクトを指定することはできない | 任意のロックオブジェクトを指定することができる | synchronizedブロックの方が柔軟性が高く、特定のオブジェクトに対してのみ同期を行うことができる |
パフォーマンス | synchronized ブロックよりもパフォーマンスが低下する可能性がある | synchronized メソッドよりもパフォーマンスが優れている可能性がある | synchronizedメソッドはメソッド全体にロックが適用、メソッド内の全てのコードが同期される。ブロックでは必要な箇所のみロックを取得し、他の箇所では同期されないため、より細かな制御が可能。 |
使い所 | 複数のスレッドから呼び出されるメソッド・インスタンスメンバを保護する場合。メソッド内でアトミックな操作を行う場合。 | 複数のスレッドからアクセスされるオブジェクト(クリティカルセクション)を保護する必要がある場合。同期箇所・ロックオブジェクトの指定による柔軟性とパフォーマンス維持 | 「7-2-4. 異なるインスタンスをロックオブジェクトにする」で紹介しているインクリメントなブロックたちは synchronized メソッドでまとめてしまえば、スレッドセーフかつアトミックになる。ブロックでは場合によってインスタンスメンバに不整合を引き起こす場合がある。 |
7-2-2. synchronized はパフォーマンスを低下させる可能性がある
synchronized を使った排他制御はミューテックスです。ミューテックスはクリティカルセクションへのアクセスを単一の実行単位しか許しません。そのため、ミューテックスは他のスレッドの実行を停止させてしまうので、パフォーマンスを低下させる可能性があります。スレッドの待機時間が長くなると、マルチスレッドにしたことによるメリットが弱まってしまいます。なるべく他のスレッドの処理を停止させないようにするためには、本当に排他制御が必要なところだけを synchronized で括ってあげるのがいいです。
何事においてもそうですが、使用する適切な方法は、具体的な要件やコンテキストに依存します。
7-2-3. スレッドセーフとアトミックの違い
スレッドセーフは、複数のスレッドが同時にクリティカルリソースにアクセスする際に、正しい結果を得ることが保証されることを意味します。スレッドセーフなデータ構造やアルゴリズムは、適切な同期やロック機構を使用して複数のスレッドからのアクセスを制御し、データ整合性を保ち、不整合を防ぎことが目的になります。
一方、アトミック性は、特定の操作が不可分(不分割)であることを意味します。つまり、その操作が一度に完全に実行されるか、または実行されないかのいずれかであることを保証します。アトミックな操作は、スレッドが同時にその操作にアクセスしても競合状態が発生しないことを意味します。
public class MyClass {
private static int count = 0;
public static void doSomething() {
//...
count++; // compliant
}
}
上記コードはスレッドセーフではないといった問題のほかに、実はインクリメントやデクリメントが、アトミックな操作ではないという問題があります。インクリメントの様な操作を複合アクションと呼びます。複合アクションにはインクリメントなどの他、チェック・ゼン・アクト操作があります。
(参考:フリー百科事典『ウィキペディア(Wikipedia)』 競合状態)。
インクリメント操作は以下の三つのアクションが組み合わさった複合アクションで、RMW:Read-Modify-Write操作と呼ばれます。
- 変数の読み込み→取得
- その値に1を加える
- 書き込み
といった3つのアクションが行われるのですが、他のスレッドからはそれらの操作は別々の操作に見えています。操作が複数のステップから構成される為、スレッドでの各アクションの間に、他のスレッドからのアクションが割り込むことができてしまいます。このような場合、競合状態やデータの整合性の問題が発生する可能性があります。
操作が複数のアクションに分割されている様な箇所の操作についてはアトミックな処理が必要です。割り込まれてしまうと、期待する処理結果が出ません。1にインクリメントしたら勿論1になるところが、3になるかもしれないのです。競合状態です。
インクリメント操作における非アトミックなマルチスレッド操作のイメージです。
処理実行順序 |
1 |
2 |
3 |
4 |
5 |
6 |
スレッド1 | 変数の値を読む(0) | 読み取った値に1を足す | 変数に値を設定する(1) | |||
スレッド2 | 変数の値を読む(0) | 読み取った値に1を足す | 変数に値を設定する(1) |
素直に考えれば、スレッド1がインクリメントし変数は1になるので、スレッド2でインクリメントをしたら2が帰ってくると考えるでしょう。マルチスレッドでは、クリティカルセクションである変数を同時に参照し、その値に対して処理を行なってしまうことが発生します。そして、取得したその値に対して1を加え、新しい値を書きこむが可能になります。つまり、二つのスレッドで、同じ値が戻り値として繰り返し返されてしまうわけです。このままスレッドが増えていった場合、どの様な結果になるのか予想がつきません。これが競合です。競合は非アトミックな操作で行われ、それを防ぐ為には排他制御が必要です。
本来は以下の様になってほしい訳です。
処理実行順序 |
1 |
2 |
3 |
4 |
5 |
6 |
スレッド1 | 変数の値を読む(0) | 読み取った値に1を足す | 変数に値を設定する(1) | |||
スレッド2 | 変数の値を読む(1) | 読み取った値に1を足す | 変数に値を設定する(2) |
この様な、アトミックで決定的な操作を実現する為には、java.util.concurrent.atomic
パッケージを使用して、アトミック操作を提供するクラスを用います。
AtomicIntegerにはincrementAndGet()やaddAndGet(int delta)などのメソッドがあり、スレッドセーフにインクリメント・デクリメントが可能です。もし、インクリメント・デクリメントの非アトミックな操作をアトミックにしたい場合、以下の書き方でアトミックかつスレッドセーフになります。
import java.util.concurrent.atomic.AtomicInteger;
public class MyClass {
private static AtomicInteger count = new AtomicInteger(0);
public static void doSomething() {
//...
count.incrementAndGet();
}
}
7-2-4. ロックオブジェクトが異なるインスタンス・スレッドセーフだが非アトミック
ロックを取得中のスレッドがあれば、ロックを取得しようしたスレッドはブロックされます。ロックが開放されたら、後続のスレッドがロックを取得できます。つまり、ロックされる処理ブロックでは、スレッドの実行が同期化(排他制御)することを意味します。
変数 lock をロックオブジェクトにした場合、そのロックオブジェクトのインスタンス単位でロックを行います。これにより、指定した処理ブロックがスレッドセーフであることが保証されます。ただし、複数のインスタンスを生成し、バラバラにロックオブジェクトに指定した場合はスレッドセーフではなくなります。
以下のコードの各 synchronized なメソッドはアトミック、つまり不可分操作(それ以上切り分けられない)であるべきな複合アクションを表現しています。インクリメント(もしくはデクリメント)操作です。上記で説明した様に、ロックオブジェクトを異なるインスタンスで指定してしまうと、スレッドセーフでもなければアトミックでもありません。
public class Increment {
private static int count = 0;
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
private static final Object lock3 = new Object();
private void read () {
synchronized (lock1) {
readObject = count;
add(readObject);
}
}
private void add (int readObject) {
synchronized (lock2) {
addObject = readObject + 1;
set(addObject);
}
}
private void set (int addObject) {
synchronized (lock3) {
count = addObject;
}
}
}
このコードの問題点は2つです。
- 各メソッドで異なるロックオブジェクトが指定されているため、複数のスレッドが同時に同じメソッドを実行している場合、データの競合が発生する可能性がある。
- 2つのスレッドが同時にread()メソッドを実行している場合、スレッドAがcount変数を読み取り、スレッドBがcount変数に書き込む可能性があり、その場合データ競合が発生する可能性がある。
- 各メソッドでロックを解放する前に、次のメソッドを呼び出しているため、他のスレッドがこれらのメソッドを実行できなくなる可能性がある。
- スレッドAが read() メソッドを実行、スレッドBが add() メソッドを実行している状態。スレッドAがlock2オブジェクトを保持しているため、スレッドBはadd()メソッドを実行できない。これにより、スレッドがブロックされ、パフォーマンスの問題が発生する可能性がある。
同じロックオブジェクトをすべてのメソッドで指定すれば、データ競合を防ぎことができます。つまりスレッドセーフですし、本来一気通貫で実行されるべき複合アクション、つまりアトミックであるべきインクリメントが正しくアトミックに行われます。
また、上記で紹介した以下のコードも同じくスレッドセーフでアトミックです。
import java.util.concurrent.atomic.AtomicInteger;
public class MyClass {
private static AtomicInteger count = new AtomicInteger(0);
public static void doSomething() {
//...
count.incrementAndGet();
}
}
7-2-5. synchronized メソッドで this をロックオブジェクトにする場合
this
をロックオブジェクトに使用するのは注意が必要です。this参照とは、オブジェクト自身への参照です。個人的には this は使わない方がいいと思っています。
public class MyClass {
private static int count = 0;
public static void doSomething1() {
synchronized (this) {
// 排他制御が必要な処理;
}
}
public static void doSomething2() {
synchronized (this) {
// 排他制御が必要な処理;
}
}
}
ロックオブジェクトが this のため、new MyClass() が複数回行われるなどして複数のインスタンスが生成された場合、ぞれぞれのインスタンスにて this は別物になります。なので、MyClass インスタンス間での排他制御は行われなくなります。
// 以外のコードはスレッド間で排他制御されない
new MyClass().doSomething1();
new MyClass().doSomething2();
7-2-6. 「LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する
このガイドラインは、信頼できないコードから使用されるクラスを同期する際のセキュリティガイドラインです。
『7-2-1. synchronizedメソッド と synchronizedブロック の違い』で紹介した下記が関係します。
ロックの公開 | メソッドを利用する他のコードにも公開 | ロックオブジェクトを取得したコードのみ | 公開される範囲を制御。特に、信頼できないコードやオブジェクトを外部に公開する場合は、適切なロックオブジェクトの選択とロックの公開範囲の制限が重要。 |
オブジェクトをパブリックに公開すると、不特定多数からアクセスされる可能性があります。このような場合は、synchronized メソッドは脆弱性を伴います。信頼できないコードから使用される可能性のある synchronized メソッドをもつクラスを同期するには注意が必要です。信頼できないコードとは、信頼できないソースコードから生成されたコードや、信頼できないユーザーによって実行されるコードのことです。信頼できないコードから使用されるクラスを同期するためには、以下を実施する必要があります。
- private finalなロックオブジェクトを使用すること
- private static final なロックオブジェクトを使用すること
- クラスフィールドやクラスメソッドを保護する場合
private final なロックオブジェクトを使用すると、信頼できないコードはロックオブジェクトを取得することができず、クラスの同期が破綻することはありません。信頼できないコードにオブジェクトを公開する場合は、オブジェクトのロックを無期限に獲得され競合状態やデッドロック・サービス運用妨害 (DoS) を引き起こすおそれがあるため、private final なロックオブジェクトを使うようにしてロックを露出しないようにするべきということです。
また、クラスフィールドやクラスメソッドを保護する場合には、private static final なロックオブジェクトを使用することが重要です。これにより、同期化の範囲を制限し、複数のインスタンス間での同期化が行われるのを防ぐことができます。
参考:「LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する」(JPCERT)
8. 最後のまとめ 『LCK05-J. 信頼できないコードによって変更されうる static フィールドへのアクセスは同期する』
それでは、最後にこちらのセキュリティガイドラインを紹介して終わりにします。今回、ソナーに警告されたのはこのガイドラインの遵守違反でした。このガイドラインがなんの目的で制定されていて、どの様に順守するのか、そしてそれは何故なのか?
何故なのかというところから、static・マルチスレッド・排他制御・アトミック性と可視性、そして synchronized の話をしてきました。このガイドラインを真に理解するための記事でした。長く、深く、遠い道でした。それでもなお、考慮不足なところや大量に端折ったところがあります。もう無理です(笑)
「LCK05-J. 信頼できないコードによって変更されうる static フィールドへのアクセスは同期する」というルールは、信頼できないコードによって変更されうる static フィールドへのアクセスを同期する必要性を示しています。
このルールの要点は以下の通りです:
- 信頼できないコードによって変更されうる static フィールドへのアクセスは同期する必要がある。
- 信頼できるコード内での static フィールドへのアクセスも同期することが望ましい。
このルールの目的は、複数のスレッドが同時に static フィールドへアクセスする場合に、データ競合や不正確な値の読み書きを防ぐことです。信頼できないコードや複数のスレッドからのアクセスが発生する場合は、適切な同期機構を使用してスレッド間の競合を回避する必要があり、具体的な対策として以下が挙げられます。
- synchronized キーワードを使用してメソッド全体または対象のコードブロックを同期化する。
- Atomic クラスや volatile 修飾子を使用してアトミックな操作を行う。
- ReentrantLock クラスやその他の同期機構を使用して明示的なロックを取得・解放する。
これらの同期手法を使用することで、信頼できないコードによる static フィールドへのアクセスにおいてもデータの整合性を確保し、競合状態や予期しない結果の発生を防ぐことができます。
/* このクラスはスレッドセーフである(アトミックではない) */
public final class CountHits {
private static int counter;
private static final Object lock = new Object();
public void incrementCounter() {
synchronized (lock) {
counter++;
}
}
}
本記事では 1 を中心に 2 も若干紹介しました。3 の様なマルチスレッド用のクラスがたくさんあるのですが、本記事では最も基本的な部分を抑えるにとどめました。いつか応用編を書いてみたいですね。マルチスレッドのデザインパターンもあるのです。使う様なプロダクトやってみたいです。
参考:経営情報システム学特論1 11~12.Javaマルチスレッドパターン
9. 終わりに
以下に過去記事を紹介しています。一つ目の過去記事はマルチスレッドに対する不変オブジェクトの有用性についての記事です。よかったらご覧ください。最後までお読みいただき有難うございました!!