SlideShare a Scribd company logo
Java 並行処理プログラミング
第16章

滝波 景
2

はじめに
• あるスレッドが、変数aVariableに値を代入
aVariable = 3;

• 通常なら他のスレッドは問題なく aVariable=3 として値が見れる。
※ただし、正しく同期化されている場合のみ。

• 逆に同期化されていないと次のような問題が発生
3

メモリやプロセッサの問題
• コンパイラがソースコードに書かれている常識的な順序でない順序で命令を作り
だす。
• 変数をメモリではなくプロセッサのレジスタに保存する。
• プロセッサが複数の命令を平行に実行したり、コンパイラが作ったコードとは違
う順序で命令を実行する。
• キャッシュの介在によって、変数への書き込みが主記憶にコミットされる順序が
変わる。
• プロセッサローカルなキャッシュに保存された値が他のプロセッサから見えない
ことがある。
4

結局なぜJMMが必要?
• JMMはそういった変数へのライトがほかのスレッドから可視になるタイミング
に関する問題を、JVMに対して最低限の保証義務をしてくれている。

• 結果
• あらゆるメモリ操作の結果がやる前から予測可能になる
• プログラムの開発が容易になる
5

メモリモデルの課題
• 共有メモリを使うマルチプロセッサのアーキテクチャでは、各プロセッサが自分
のキャッシュを持つ。
• キャッシュの一貫性もプロセッサのアーキテクチャによってさまざま。
• 各プロセッサは他のプロセッサが何をしているかを知るのは高コストでむしろ、
知らなくていい時の方が多い。なので実際はプロセッサはキャッシュの一貫性を
ゆるめている。
• Javaはこういったアーキテクチャ毎のメモリモデルの違いを開発者に意識させな
くてもいいようにするためにJMMを提供している。
• また、JVMはJMMとプラットフォームのメモリモデルの違いを吸収するために、プ
ログラムがメモリシステムに期待していい保証内容と、データを共有するときに
必要なメモリ保証を取得するための命令(メモリバリヤ)を随所に挿入する。
6

注意点
• プログラマはプログラムが書かれている順序がどんなプロセッサ上でも順番通り
に動くことを想定している。
これを逐次的一貫性という。
• ただ、実際はマルチプロセッサは逐次的一貫性を提供しないし、JMMも提供しな
い。
• なので、プログラマは複数スレッドがデータを共有しているときは、おかしなこ
とが起きることを意識しないとダメ。
• 逆に言えば、共有データがある場所は正しく同期化すればいいだけ。
7

順序変え
• JMMは「一連の活動を構成する複数の操作の順序の見え方がスレッド毎に違っ
てもよい」としています。

• 結果、同期化をしないときの実行順序を判断するのが一層難しくなっている。

• ある操作が不可解に遅れたり、へんな順序で実行されるように見えたりする現
象をひっくるめて、順序変えと呼ぶ。
8

• 正しく同期しないと、結果を推測するのは難しいという簡単な並行プログラム
public class PossibleReordering {
static int x = 0;
static int y = 0;
static int a = 0;
static int b = 0;
public static void main(String rags[]) throws InterruptedException {
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = b;
}
});
one.start();
other.start();
one.join();
other.join();
System.out.println("(" + x + "," + y + ")");
}
9

結果
・結果として(1,0)、(1,1)、(0,1)が出力されるのは簡単に想像できるが
(0,0)も出力することもある。

なんで??
・スレッドが互いにデータフローの依存性がないので、書かれている通りの順序
で実行されたとしても、キャッシュが主記憶にフラッシュされるタイミングによ
ってはBから見てAの2つの代入が逆の順序で実行されることがあるから。
(0,0)といった順序変えになる場合のスレッドの状態
10

JMMのアクション定義
• JMMは以下のアクションの仕様を定義しています。
・変数のread/write
・モニタのロックとアンロック
・スレッドのスタートとジョイン
・またJMMは「事前発生(happens before)」という半順序を定義しています。

• ↑↑なんで?
Ans.
• アクションBを実行しているスレッドから、別のスレッドのアクションAの結果
が見えることを保証するにはAとBの間に事前発生の関係が必要。
じゃないとJVMが勝手に順序を入れ替えたりしてしまうから。
つまりデータ競合がおきる。
11

同期化の相乗り
• 事前発生順序の強さを利用して、既存の同期化の可視性特性を相乗りすること
ができる。
• 事前発生のルールの「プログラム順序」+「モニタロック or 揮発性変数」を
組み合わせて本来ロックでガードされていない変数へのアクセスを順序化しま
す。
• このテクニックを相乗りと呼ぶのは、何か他の理由で作られた既存の事前発生
順を利用してあるオブジェクトXの可視性を確保するため、わざわざXを公開す
るために、わざわざ事前発生順を作ったりする必要がないために、そう呼ばれ
ている。
• 注)ただし、文の順序に依存しているので、かなり脆弱。
12

同期化の相乗りの例
private final class Sync extends AbstractQueuedSynchronizer {
private static final int RUNNING = 1;
private static final int RAN = 2;
private static final int CANCELLED = 4;
private V result;
private Exception exception;
void innerSet(V v) {
while (true) {
int s = getState();
if (ranOrCancelled(s)) {
return;
}
if (compareAndSetState(s, RAN)) {
break;
}
result = v;
releaseShared(0);
done();
}
V innerGet() throws InterruptedException, ExecutionException {
acquireSharedInterruptibly(0);
if (getState() == CANCELLED) {
throw new CancellationException();
}
if (exception != null) {
throw new ExecutionException(exception);
}
return result;
}
}
13

同期化の相乗り
• 復習
FutureTaskは、tryReleaseSharedの成功呼び出しがつねに、その後の
tryAcquireSharedの呼び出しよりも事前発生するように実装されている。
• 結果
innerSetはreleaseShared(これがtryReleasedSharedを呼び出す)
前にライト、innerGetはacquiredShared(これがtryAcquireSharedを呼び
だす)を呼び出した後にリードするので「プログラム順序」と「揮発性変数」の
組み合わせにより、innerSetによるresultのライトがinnerGetによるresultの
リードよりも確実に発生。
14

公開
• 事前発生の関係がないときに順序変えが起きると、他のスレッドが一部だけ構
築されたオブジェクトを見てしまう。
• すると、そのスレッドはオブジェクトの参照の最新値を見るが、オブジェクト
のステートはその全部または一部が古い値のまま。
他のスレッドが一部だけ構築されたResourceの参照をみてしまう可能性の例

public class UnsafeLazyInitialization {
private static Resource resource;
public static Resource getInstance() {
if (resource == null) {
resource = new Resource();
}
return resource;
①A
}
②B

①B
②A
}

AはResourceを初期化してからresourceが
それを参照するようにset。
しかし、Bから見ると、resourceへのライ
トがResourceのフィールドへのライトの目
に行われている。
15

安全な初期化の慣用句
• 高価なオブジェクトの初期化を、オブジェクトが実際に必要になるまで遅らせ
ると便利な場合がある。
• 多くのスレッドから頻繁に呼び出さなければ、ロックの争奪がほとんどなくて
、実行性能もまぁまぁとのこと。
スレッドスレーフな遅延初期化

public class SafeLazyInitialization {
private static Resource resource;

public synchronized static Resource getInstance() {
if (resource == null) {
resource = new Resource();
}
return resource;
}
}
16

その他の初期化
• 以下のコードはgetInstanceを呼ぶときの、毎回の同期化にかかる費用がなく
なります。
念入りな初期化

public class ResourceFactory {
public static Resource resource = new Resource();
private static Resource getResource() {
return resource;
}

• 以下のコードはResourceがstaticイニシャライザで初期化されるので、明示的
な同期化がいりません。
遅延初期化ホルダークラス

public class ResourceFactory {
private static ResourceHolder {
public static Resource resource = new

Resource();
}
public static Resource getResource() {
return ResouceHolder.resource;
}
17

ダブルチェックロッキング
• 初期のJVMは同期化の実効性能費用が、無争奪の同期ですらかなり高価。
そのため、下記のようなダブルロッキングといったアンチパターンが発生。
ダブルロッキングアンチパターン

public class DoubleCheckedLocking {
private static Resource resource;
public static Resource getInstance() {
if (resource == null) {
synchronized (DoubleCheckedLocking.class) {
if (resource == null) {

resource = new Resource();
}
}
return resource;
}
}
18

ダブルロッキングの問題点
・処理フロー
1.初期化が必要かどうかを同期化なしでチェックする
2.resourceがnullでなければ、それを使う
3.nullなら同じチェックを今度は同期化して行い、再びnullなら共有Resource
を初期化
・結果
確実に唯一のスレッドがResourceオブジェクトを初期化する。
・問題点
すでにコンストラクトされているResourceの参照を取り出す操作は同期化
されません。
つまりスレッドが一部だけ構築されたResourceを見ることになる。
19

初期化の安全性
• 初期化の安全性とは、正しくコンストラクトされた不可変オブジェクトがデ
ータ競合を使った公開でも、同期化なしで複数スレッドから安全に共有され
ること。
また、コンストラクタにセットしたfinalフィールドの正しい値を全てのスレ
ッドが見ることを保証します。
不可変オブジェクトの初期化安全性

public class SafeStates {
private final Map<String, String> states;
public SafeStates() {
states = new HashMap<String, String()>;
states.put("alaska", "AK");
states.put("alabama", "AL");
states.put("wyoming", "WY");
}
public String getAbbreviation(String s) {
return states.get(s);
}
}
20

• また、正しく構築されたオブジェクトのfinalフィールドから到達できる変数(
例えばfinalな配列の成分や、finalフィールドが参照するHashMapの内容)はど
れでも、他のスレッドにとって可視であることが保証されます。
• 注)statesがfinalフィールドでなかったり、コンストラクタ以外のメソッドが
その内容を変えていたら、他のスレッドからそれらのフィールドの不正な値を
見る可能性がでてくる。
つまり、ノンfinalなフィールドから到達できる値や、コンストラクタの後で
変
わることもある値に関しては、同期化を使って可視性を確保する必要がある
。
21

まとめ
• Javaのメモリモデルとは
スレッドが行うメモリ上のアクションの、他のスレッドからの可視性を保証
できる状況を指定する。
• 可視性を指定するには
複数の操作が事前発生と呼ばれる半順序によって確実に順序されること。
また操作単位は個々のメモリ操作や同期操作なので、十分に同期がないと
スレッドが共有データにアクセスしたときに奇妙なことが起きる。
• 対応
2章、3章で説明された高レベルなルール、例えば@GuardByや安全な公開
を使うと、事前発生の低レベルな細部に頼らずにスレッドセーフが確保可能。

More Related Content

Java並行処理プログラミング 第16章ver2