Javaメモリモデルは独自のものになります

一緒に書く習慣を身につけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して17日目です。クリックしてイベント詳細をご覧ください。

この一連の列 Java並行プログラミング列-Yuanhao875の列-ナゲッツ(juejin.cn)

序文

Javaメモリモデルについて説明する前に、混乱しやすいものについて説明しましょう。ここでのJavaメモリモデルは、並行プログラミングのいくつかの問題を解決するために使用される複雑な仕様のセットであるJavaメモリモデルです。

また、多くの人がJavaメモリモデルとも呼ばれるJVMランタイムメモリ構造と呼んでいますが、これは非常に間違っています。つまり、次の図です。

image.png

これは、Javaプログラムの実行時にJVMによって開かれるさまざまなスペースです。これには、スレッドによって共有されるヒープおよびメソッド領域(異なるバージョンによって異なる方法で処理されるメタデータ領域または永続生成)、およびメソッドスタックとプログラムカウンターが含まれます。スレッドによって共有されません。これはJVMのメモリ構造であり、Javaメモリモデルとは呼ばないでくださいこの章では、Javaメモリモデルが何であるかを実際に理解するようになります。

1650097935(1).jpg

文章

前回の記事では、並行プログラミングのバグを引き起こす3つの主要な問題、つまり可視性、原子性、秩序性について説明しました。これら3つの問題は、Javaが高度なプログラミング言語であるのに対し、数十年にわたるコンピューター開発で進化した問題です。言語は並行性をサポートしているため、可視性と順序によって引き起こされる問題を解決するために、Java言語は有名なJavaメモリモデルを導入しています

Javaメモリモデルとは

前回の記事で、可視性の問題の原因はCPUキャッシュであり、秩序の原因はコンパイラの最適化であると述べたので、CPUキャッシュとコンパイラの最適化を無効にするだけで済みますが、問題は解決しましたが、また、それがないと、プログラムのパフォーマンスが心配になります。

合理的な解決策は、オンデマンドでキャッシュとコンパイルの最適化を無効にすることです。必要なのはいつですか?これはコードを作成するプログラマーなので、必要に応じてキャッシュとコンパイルの最適化を無効にする方法をプログラマーに提供すると便利です。 Javaメモリモデルはまさにそれを行います。

Java内存模型定义了一套规范,能使JVM按需禁用CPU缓存和编译优化;而对于程序员来说,就是提供了一些方法可以让JVM按需禁用缓存和编译优化,这些方法包括了volatile、synchronized和final三个关键字,以及六项Happen-Before规则。

使用volatile的困惑

volatile在古老的C语言中就有,最原始的意义就是禁用CPU缓存,例如我们声明一个volatile变量volatile int x = 0,这句话的意思对这个变量的读写,不能使用CPU缓存,必须从内存中读取或者写入。

那如果只有这个功能的话也只能说明这个变量是线程间可见的,但是还不够完全解决问题,我们来看下面代码:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
    }
  }
}
复制代码

假如线程A执行writer()方法,会把变量v的值为true写入内存,线程B执行reader()方法,按照volatile语义,线程B会从内存中获取变量v,如果线程B看到的是"v == true",这里x值可能是42也可能是0,这里要分版本:低于1.5版本上,x值可能是42,也可能是0;如果1.5以上的版本,x只能是42。

产生这个原因也非常简单,变量x可能被CPU缓存导致可见性问题,也有可能是指令重排导致的,但是在1.5以上对volatile语义进行了增强,如何增强呢 就是Happens-Before规则。

Happens-Befroe规则

Happen-Before规则是Java内存模型制定的规则,用来处理线程间可见性问题,至于如何去处理,我们先不做讨论细节。

这个Happen-Before可以说是Java内存模型中最难懂的地方,理解起来非常绕;首先这个词的翻译就比较难,Happen-Before并不是说前面一个操作发生在后续操作之前,它要真正表达的意思是:前面一个操作的结果对于后续操作是可见的

就像有心灵感应的2个人,虽然远隔千里,一个人所想,另一个人能看得见,而Happens-Before规则就是要线程之间保持这种"心灵感应",所以比较正式的说法是:Happens-Berfore约束了编译器的优化行为,允许编译器优化,但是优化后必须遵守Happens-Before规则

说到这里或许就明白了,虽然volatile变量禁用了CPU缓存,但是没有禁止编译器优化啊,编译器依旧可以优化,但是像前面说的把"x = 42和v = true"给调换位置的优化就不会,而且x可见性也能得到保证,那这个强大的Happen-Before规则是什么样的呢。

和程序员相关的规则有6个,且都关于可见性的。

(1) 程序的顺序性规则

指的是在一个线程中按照程序顺序,前面的操作Happens-Berfore于后续的任意操作。比如前面的代码中,第6行代码"x = 42" Happens-Before于第7行代码"v = true",这比较符合单线程的思维:程序前面对某个变量的修改,一定是对后续操作可见的。

注意哦,这个规则是单线程下的规则,比如上面代码如果没有volatile修饰的话,x和v的赋值之间是没有依赖的,所以这2个赋值操作可以重排,但是x的赋值结果却对v的赋值这条语句来说是可见的,虽然这个可见性没啥用(因为v的赋值不依赖x的值)。

(2) volatile变量规则

这条规则是指一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作,单独看这一条规则,这不就是禁用缓存的意思吗,别急,我们看第三条。

(3) 传递性规则

这条规则是指如果A Happens-Berfore B,且B Happens-Before C,那么A Happens-Before C,那我们将规则3的传递性应用到我们的例子中是:

image.png

可以看到:

  • "x = 42" Happens-Before 写变量"v = true",这是规则1;

  • 写变量"v = true" Happens-Before 读变量"v = true",这是规则2;

再根据传递性规则,我们得到结果"x = 42" Happens-Before读变量"v = true",这意味什么呢 那就是线程A设置的"x = 42"是对线程B可见的,也就是线程B能看到"x == 42",这就是1.5版本对volatile语义的增强,这个意义重大,Java并发工具包就是靠volatile语义来搞定可见性的。

而这里对这个可见性的实现是禁止这2段语句的重排,这个也是volatile的通俗功能说法会禁止指令重排序。

(4) 管程中锁的规则

这条规则是指一个锁的解锁Happens-Before于后续对这个锁的加锁

管程是一种通用的同步原语,在Java中是指synchronized,synchronized是Java里对管程的实现,管程中的锁在Java里隐式实现的,比如下面代码在进入同步代码块之前,会自动加锁,而在代码块执行完后会自动释放锁,加锁以及释放锁都是编译器帮我实现的:

synchronized (this) { //此处自动加锁
  // x是共享变量,初始值=10
  if (this.x < 12) {
    this.x = 12; 
  }  
} //此处自动解锁
复制代码

可以这样理解:假设x初始值为10,线程A执行完代码块后,x值为12,自动释放锁,线程B进入代码块时,能够看见线程A对x的写操作,即线程B能够看到x==12,这也是符合我们对synchronized的用法。

(5) 线程start()规则

このルールはスレッドの起動に関するものです。つまり、メインスレッドAが子スレッドBを開始した後、子スレッドBは、次のコードのように、子スレッドBを開始する前にメインスレッドの動作を確認できます。

Thread B = new Thread(()->{
  // 主线程调用B.start()之前
  // 所有对共享变量的修改,此处皆可见
  // 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();
复制代码

これは、私たちの常識、つまり、スレッドBの操作の前に発生するHappens-Beforeのstart()操作とも一致しています。

(6)スレッドjoin()ルール

このルールはスレッド待機に関するものです。つまり、メインスレッドAはサブスレッドBが完了するのを待機します。つまり、メインスレッドAはサブスレッドBのjoin()メソッドを呼び出します。サブスレッドBの場合が完了すると、メインスレッドはここで呼び出されるサブスレッドの操作を確認できます。確認は共有変数の操作も参照します

たとえば、次のコード:


Thread B = new Thread(()->{
  // 此处对共享变量var修改
  var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66
复制代码

これは、スレッドBで発生する前の操作がjoin()操作に戻ることです。

要約する

この章の内容は比較的複雑で、主に前述の可視性と順序の問題を解決するためのJavaメモリモデルを紹介します。その中で、Happens-Beforeルールは比較的明確であり、本質的には可視性ルールであり、メモリモデル。Happens-Beforeルールとvolatileおよびsynchronizedキーワードは、可視性と順序付けの問題を解決できます。次の記事では、アトミック問題を解決する方法を見ていきます。

おすすめ

転載: juejin.im/post/7087488770420768782