揮発性の「Javaの並行プログラミングの芸術」

この章の学習後のために、メモリ・モデルである揮発性のキーワードがあまりにも多くの包括的な理解を持って、知識の点を分析し、まとめました。

揮発性の特性

揮発性のような単一の操作で同期

  • 可視性:すべてのスレッドに対して可視保証の揮発性のフィールドを書きます
  • アトミック:単一揮発性読み出し及び書き込み動作(32ビットマシン上で、例えば64ビット長、double型を読み取る)アトミックフィールドであるが、原子揮発性有しない++

揮発的には、事前発生します

このセクションでは、アクションが起こる前に、揮発性の関係定める使用します

int a;
volatile boolean flag;
public void init(){
    a = 1; //①
    flag = true; // ②
}
public void doTask(){
    if(flag){ // ③
        result = a; // ④
    }
    ......
}

)実行INITスレッドと仮定()、スレッドBは(doTaskを実行し、このプロセスにおいて、事前発生関係は、3つのカテゴリに分類されます。

  1. ①事前発生②
  2. ③事前発生④
  3. ②事前発生③

それが起こる前に、次のようにこのプロセスの図です。

コンプライアンスの手順は、ルール、volatile変数のルールと渡すルールの順序のとおりです。

  • プログラムシーケンスルール:スレッド、操作の前にコードブックエディトリアルの順序に応じて、後で書き込み動作で発生します
  • volatile変数のルール:変数事前に操作を書く読み出し動作の後に、この変数の顔に発生しました
  • ルールを渡す:操作が先手順Bを発生し、そしてBが最初の操作は、Cを発生して動作する場合、動作は前述の操作で得られるCを発生

揮発性メモリのセマンティクス

JMMコンパイラは、のために開発された揮発性のルールの並べ替え

  • 第2の操作は、並べ替えることができない最初の操作が何であるかに関係なく、揮発性書き込み、です。この規則は、揮発書き込み動作が以前にされないことを保証落胆の並べ替えをコンパイル揮発した後に書くこと。
  • 最初の操作揮発性の読み取りは、関係なく、第二が何であるか、ときに並べ替えすることはできません。この規則は、操作が読んだ後で揮発性でないことを保証ソート落胆コンパイルに揮発性の読み取りの前に。
  • 最初の書き込み動作がある場合に揮発性、揮発性の第2の動作は、読み出し、無並べ替えです。

不揮発性メモリ書き込みセマンティクス

揮発性の書き込みが発生すると、ローカルメモリはメインメモリをリフレッシュ。事前発生上記の例をとると、init()は揮発性変数に書き込まれたときに実行されるスレッドAは、実行doTaskのBスレッド()は、揮発性変数を読み取り。図に示すように、メモリ状態の変化。

スレッド後に書き込みフラグ変数、ローカルメモリ更新共有変数(更新されたいくつかのいくつかのリフレッシュ)、メインメモリ、同じメインメモリとローカルメモリ共有変数スレッドを更新します。

揮発性読み出しメモリのセマンティクス

スレッドは、フラグ変数Bを読み取ることができる場合、共有変数Bは、ローカルメモリが無効化されていると、スレッドBは、メインメモリは、共有変数にあっ読み出します。スレッドBは、ローカルメモリBは、共有変数の値とメインメモリは一貫なる原因となります読み込みます。

二つのマップは、まとめると揮発性可変読み取りスレッドBを読んだ後、全ての可視の共有変数が直ちにリーダスレッドBに見えるようになる、この揮発性変数を書き込む前に書き込まれたスレッド

セマンティック概要

  • 本質的には、書き込みスレッドがvolatile変数を書いています(その共有変数への変更)は、それが次のスレッドに書き込まれたメッセージを送信した場合、この変数スレッドを読むには
  • 揮発性の変数を対応するスレッドの読み取りを読んで、本質的には、(揮発性書き込み前に、共有変数を変更)スレッドによって送信され書かれたメッセージを受信しました
  • その後、揮発性変数スレッドは、プロセス、スレッドがスレッドBを介してメインメモリにメッセージを送る本質である、読み、書きvolatile変数にスレッドを読んで書き込み、

セマンティック揮発性メモリの実装

volatileキーワードの実装原理は、主にメモリバリアによって制御されます。コンパイラが生成する場合のシーケンスにおけるバイトコード命令は、並べ替えを阻害する特定のメモリバリアに挿入されます。コンパイラの場合、挿入された裁量がそう障壁の合計数を最小限に抑えます。このため、JMMは、保守的な戦略を取ります:

  • 在每个volatile写操作的前面加入StoreStore
  • 在每个volatile写操作的后面加入StoreLoad
  • 在每个volatile读操作的后面加入LoadLoad
  • 在每个volatile读操作的后面加入LoadStore

上面的插入策略十分保守,但它可以保证在任意处理器平台上(在X86里,写/写,读/读,读/写 是不会发生重排序的,而且只有StoreLoad一个内存屏障),任意的程序中都能实现正确的语义。

volatile写的内存语义实现

下面是保守策略下,volatile写插入内存屏障的指令序列示意图。

StoreStore保证在执行volatile写前,所有写操作的处理已经刷新至内存,保证对其他线程可见了。而StoreLoad的作用是避免后面还有其他的volatile读/写操作发生重排序。由于JMM无法准确判断StoreLoad所处的环境(比如结尾是return),所以有两种选择:

  1. 在volatile读前加上StoreLoad
  2. 在volatile写后加上StoreLoad

但是因为StoreLoad相比其他内存屏障更加消耗性能,考虑更多场景下是少写多读,所以将StoreLoad加在volatile写后。

讲到StoreLoad的性能问题,不得不提一下Unsafe里面的putOrderedObject()。 这个方法很有意思,乍一看命名是放一个有序的对象,但它是通过避免加上StoreLoad内存屏障来弥补volatile写的性能问题。这时可能会有朋友问,不加上volatile不会影响可见性吗?会影响可见性,但不会永远影响下去,最多就两三秒的延迟,就会将共享变量刷新至主内存。所以当延迟要求不高,性能要求高时,就可以采用这个方法(Unsafe不安全类,这个方法的实现在Atmoic***类里面)。

volatile读的内存语义实现

下面是保守策略下,volatile读插入内存屏障的指令序列示意图。

LoadLoad保证先执行volatile读再执行后续的读操作(禁止volatile读和后续的读发生重排序),而后的LoadStore保证先执行volatile读再执行写操作(禁止volatile读和后续的写发生重排序)。两者联合起来就是无论如何volatile读必须和程序顺序保持一致。

volatile执行时的优化

上面的volatile读/写的内存屏障插入策略都十分保守,但是在实际过程中,只要不改变volatile写/读的内存语义,编译器可以根据实际情况省略不必要的屏障。

int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite(){
    int i = v1;
    int j = v2;
    a = i + j;
    v1 = i + 1;
    v2 = j + 2;
}

针对readAndWrite()方法,编译器在生成字节码时会做如下优化。

按顺序下来,第一个volatile读先于第二个volatile,第二个volatile先于所有后续的写,故第一个volatile读一定不会被重排序;StoreStore保证普通写先于第一个volatile写,StoreStore又保证第一个volatile写先于第二个volatile写,最后安全起见插入StoreLoad。

上面的优化针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。比如X86处理器,由于X86不会对读/读,读/写,写/写做重排序,所以面对X86处理器时,JMM会省略掉三种类型对应的内存屏障,保留StoreLoad内存屏障。

JSR-133为什么增强volatile的内存语义

在之前的版本,虽然不允许volatile变量间 的重排序,但是允许volatile和普通变量间的重排序。为了提供一种比锁更轻量级的线程间通信机制,专家组决定增强volatile的内存语义,严格限制volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。

由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和性能上,volatile更有优势。具体看《Java理论与实践:正确使用volatile变量》

おすすめ

転載: www.cnblogs.com/codeleven/p/10963117.html