blog

Java仮想マシンを理解する - Javaメモリ・モデル (1)

1つ目は、私の理解力が足りないということです。 第二に、それはビジネスステージの準備に長い時間である可能性があり、知識のポイントに接続することはできません。 第三は、誰もが思考の惰性のようなものが表示...

Feb 4, 2020 · 11 min. read
シェア

序文

最近、JVMの本を読んでいる、私は別の感じを見て毎回、確かに、Androididerとして、Java言語の使用が、Java言語の魅力に深く掘り下げなかったことを感じています。

  • ひとつには、それを理解できるほど私は上手くないということ。
  • 第二に、私が長い間書く仕事をしてきて、知識の点と点をつなげることができなかったということかもしれません。
  • 3つ目は、私たち誰もが苦しんでいる心の惰性のようなものです。

まあ、真剣に本を読むために、レベルはもちろん十分ではありません、または参照してください、繰り返し参照してくださいし続けるために、著者のレベルは、この本の抜粋だけでなく、本の個人的な理解の重要な概念に限定され、記載されていない可能性があり、私はあなたが私を許してほしい。

Javaメモリ・モデルの起源

Java仮想マシン仕様では、さまざまなハードウェアやオペレーティング・システムによるメモリ・アクセスの違いを遮蔽し、さまざまなプラットフォーム上のJavaプログラムで一貫したメモリ・アクセスを実現するために、「Javaメモリ・モデル」を定義しています。

Java仮想マシンは、Java言語の使用では、少なくともエラーによって引き起こされる様々なプラットフォームのデータの不整合を心配する必要はありません、規範的な措置のシリーズを開発しているため、この文から、あなたはなぜJava言語がクロスプラットフォームすることができるように簡単にすることができます知っています。

その後、モデルは当然のことながら、条件を定義しながら、Javaの同時アクセス操作が曖昧さを生成しませんできるようにするために、十分に厳格に定義する必要があります十分に緩いですが、仮想マシンを実装するために、より良い、より高速な実行速度を得るためにハードウェアのさまざまな特性を活用するのに十分な空き領域を持つことができます。JDK5のリリース後まで、Javaのメモリモデルは、最終的に成熟した、完璧です。

当然、記憶についての議論は、次のセクション、主記憶と作業記憶の概念につながります。

主記憶と作業記憶

1)主記憶と作業記憶の関係

Javaメモリ・モデルの主な目的は、プログラム内のさまざまな変数にアクセスするためのルールを定義すること、つまり、変数の値をメモリに格納し、仮想マシン内のメモリから取り出すという基本的な詳細に焦点を当てることです

ここでは変数について話しています。

  • インスタンスフィールド
  • 静的フィールド
  • 配列オブジェクトを構成する要素

ローカル変数とメソッド・パラメータはスレッドプライベートで共有されないため、ここには記載されていません。

Javaのメモリ・モデルでは、すべての変数はメイン・メモリに格納され、各スレッドは独自の作業メモリを持ち、その作業メモリはスレッドが使用する変数のメイン・メモリのコピーを保持します。各スレッドは独自の作業メモリを持ち、その作業メモリはそのスレッドが使用する変数のコピーを保持します。 変数 ** に対する操作はすべて作業メモリで実行する必要があり、メインメモリから直接読み書きすることはできません。スレッドは作業メモリ内の互いの変数に直接アクセスすることはできず、スレッド間で値を受け渡す唯一の方法はメインメモリ**を介することです。

注意

ここで、Javaのヒープ、スタック、メソッド領域などのメインメモリ、ワーキングメモリとJavaのメモリ領域は、メモリ分割のレベルではありません、あるいは、2つは基本的に何の関係もないということです。

メイン・メモリは物理的なハードウェア・メモリを重視し、ワーキング・メモリは主にアクセスされる領域であるため、仮想マシンは高速化のためにワーキング・メモリをレジスタやキャッシュに格納することを優先するかもしれません。

概要

メイン・メモリとワーキング・メモリの関係についての話は、すべて変数を中心に展開されるわけです。変数はすべてメインメモリに格納され、ワーキングメモリがそれを使うためには、メインメモリにある変数のコピーを次の操作のためのコピーとして作る必要があります。

2)主記憶と作業記憶の相互作用

主な議論は、メインメモリーからワーキングメモリーへの変数のコピーの詳細と、ワーキングメモリーからメインメモリーへの同期の方法についてです。

Javaのメモリモデルでは、上記の操作を実行するための8つの操作が定義されており、これらの8つの操作はアトミックでなければならないことが保証されています。

  • lock(ロック):メイン・メモリ上で動作する変数で、ある変数をスレッド専用としてマークします。

  • unlock:メイン・メモリ上の変数に作用し、ロックされた変数を解放して、他のスレッドがその変数を操作できるようにします。

  • read:メインメモリ上の変数に作用し、ロード操作のためにメインメモリから取得した変数の値を作業メモリ上の変数のコピーに置きます。

  • load:割り当てメモリー上の変数に作用し、読み出し操作によってメインメモリから得た変数の値を作業メモリ上の作業コピーに置きます。

  • use:作業メモリの変数に作用し、作業メモリの変数の値を実行エンジンに渡します。

  • assign:作業メモリの変数に作用し、実行エンジンから値を受け取り、作業メモリの変数に代入します。

  • store:割り当てメモリー上の変数に作用し、作業メモリ上の変数の値をメインメモリに渡し、メインメモリで書き込み操作を行います。

  • write:メインメモリ上の変数に作用し、ストア操作によってワーキングメモリから取得した変数の値をメインメモリ変数に格納します。

上の図から、メインメモリからワーキングメモリへの変数のコピーには、リードとロードの操作が必要であることがわかります。

変数を作業メモリからメイン・メモリに同期させるには、記憶操作と書き込み操作が必要です。これらの操作は連続して行われる必要はありません。

並行処理セキュリティ入門

操作がコンカレンシーセーフかどうか、スレッドセーフをどのように確保するか、など

Javaにはこの問題を解決する方法があるのかと思っていたら、ありました。 Javaには同期メカニズムがあります。それについて説明します。

1) 揮発性変数の紹介

volatileというキーワードは、Java仮想マシンが提供する軽量の同期メカニズムだと言えますが、マルチスレッドのデータ競合を扱う場合、ほとんどの場合、常にsynchronizedを使って同期しています。

Javaのメモリ・モデルでは、volatileに対するいくつかの特別なアクセス・ルールが定義されています。

変数がvolatileとして定義されている場合、一般的に2つの性質を持ちます。

  • この変数のすべての変換は、他のスレッドから見えます。
  • コマンドの並べ替え最適化の無効化

以下は、これら2つの機能のそれぞれの概要です。

1、上記の特徴の1つ目:「可視性」とは、スレッドが***揮発性修飾子***変数の値を変更したとき、変更後の新しい値が他のスレッドから直ちに利用可能であることを意味します。

しかし、***general***変数の値は、それが変更された場合、他のスレッドが知ることができない、情報はスレッド間で分離されているため、他のスレッドに通知する必要がある場合は、作業スレッドAの作業メモリを介してメインメモリに値を同期させ、その後、メインメモリを介して作業スレッドBの作業メモリに更新する必要があります。

volatileによって変更された変数が他のスレッドから見えるからといって、volatile変数に基づく***操作が同時実行下でスレッドセーフであるとは限りません。***

理由は、他のスレッドで変数の揮発性変更が矛盾することができますが、変数の使用の各スレッドは、最初にその値をリフレッシュされ、その後、変数の値が同期され、各スレッドのスレッドは、値が同じであるということです。しかし、多くの場合、問題を無視:Javaの算術演算子は、必然的に操作の同時実行で揮発性変数につながるアトミック操作ではありませんも安全ではありません。


public class VolatileTest{
 
 public static volatile int race =0;
 public static void increase(){
 race++;
 }
 public static final int thread_count =20;
 public static void main(String[] args){
 Thread[] threads = new Thread[thread_count];
 for(int i=0; i< thread_count;i++){
 threads[i] = new Thread(new Runnable(){
 @Override
 public void run(){
 for(int i=0;i<10000;i++){
 increase();
 }
 }
 });
 threads[i].start();
 }
 }
 // すべての累積スレッドの終了を待つ
 while(Thread.activeCount()>1){
 Thread.yield();
 }
 System.out.print(race);
}

このコードは20スレッドを起動し、各スレッドがレース変数に対して10,000回のインクリメンタル演算を実行します。

残念ながら、出力はすべて異なり、すべて20*10000未満です。

何が問題なの?問題は、volatileは可視性を保証しますが、実行の原子性は保証しないということです。

問題コード:race++(レースプラスプラス

問題分析。

public static void increase(){
	Code:
		Stack=2,Locals=0,Args_size=0
		0: getstatic 		#13; //Field race:I
		3: iconst_1
		4: iadd
		5: putstatic		#13; //Field race:I
		8: return
		LineNumberTable:
		line 14 : 0
		line 15 : 8
}

}

getstatic命令は、演算スタックの先頭にレースの値を取るために、volatileキーワードは、この時点でのレースの値が正しいことを保証しますが、icont_1、iaddこれらの命令の実行では、他のスレッドがレースの値を変更することがあり、演算スタックの先頭の値が期限切れのデータになるので、他のスレッドが累積操作の小さい値を取得するように、メインメモリに同期バックレースの小さい値の後にputstatic命令を実行することができます。そのため、putstatic命令が実行された後、小さい方のレース値をメインメモリに同期して戻すことができ、他のスレッドが累積演算の小さい方の値を取得できるようになります。

以下の2つのルールのどちらかを満たさない算術演算シナリオでも、アトミック性を確保するためにロックが必要です。

  • この結果は、現在の値に依存しないか、または単一のスレッドだけが変数の値を変更することを保証します。
  • 変数は、他の状態変数との不変制約に参加する必要はありません。
volatile boolean shutdownFlag;
public void shutdown(){
 shutdownFlag = true;
}
public void doWork(){
 while(!shutdowFlag){
 //..
 }
}

shutdown()メソッドが呼び出されると、doWork()メソッドを実行しているすべてのスレッドを確実に停止させることができます。

2、揮発性変数の使用については、2番目のセマンティクスは、命令の並べ替えを禁止することです、通常の変数は、メソッドの実行プロセスは、すべての場所の結果の割り当てに依存して正しい結果を得ることができることを確認するだけでなく、変数の割り当て操作の順序とプログラムの実行の順序を確保するため。

命令の並べ替えがプログラムの並行実行をどのように妨害するかを見るためのケーススタディ。

Map configOptions;
char[] configText;
// init volatileによって変更されるので、後で解析するためにメモしておくこと。
volatile boolean init = false;
// スレッドAは以下を実行する
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
init =true;
// スレッドBは以下のコードを実行する。
while(!init){
 sleep();
}
// スレッドAで初期化された構成情報を使う
doSomethingWithConfig();

init変数の上記のコードスニペットは、volatileキーワードで変更されていない場合は、コードセグメントのスレッドAの処理init = trueの最後の行が事前に実行される可能性がありますので、コードセグメントのスレッドBの実行は、それが可能なスレッドAの場合、情報の構成を構成するために構成されていません急いで使用される情報のスレッドAの構成は、プログラムのエラーになります。

もう1つのケース:DCL(シングルトンパターン)

public class Singleton{
 private volatile static Singleton instance;
 
 public static Singleton getInstance(){
 if(instance == null){
 synchronized(Singleton.class){
 if(instance == null){
 instance = new Singleton();
 }
 }
 }
 return instance;
 }
 
 public static void main(String[] args){
 Singleton.getInstance();
 }
}

コンパイル後のバイトコード


0x01a3de0f : mov $0x3375cdb0,%esi ; ...beb0cd75 33
 ....
0x01a3de1f : lock addl $0x0,(%esp); ...f
 ; *putstatic instance
 ; - Singleton::getInstance@24

ロック・アド...命令を実行すると、メモリバリアが設定され、後続の命令がシーケンスできないようになります。これにより、命令の並べ替えができなくなります。

2) 並行性セキュリティの3つの主な特徴の概要

Javaのメモリ・モデルは、同時実行時の原子性、可視性、順序性を保証することを中心に構築されています。この3つの機能について説明しましょう。

1.原子性

Javaメモリ・モデルによって直接保証されるアトミック変数操作には、読み取り、ロード、割り当て、使用、保存、書き込みが含まれます。つまり、基本的なデータ・アクセス、読み取り、書き込みはアトミックとみなすことができます。

より広い範囲の原子性保証の必要性がある場合、Javaメモリ・モデルは、この必要性を満たすロック操作とアンロック操作を提供します。これらの操作は、ユーザーが直接利用できるわけではありませんが、Java仮想マシンは、暗黙的にこれらを使用するために、より高レベルのバイトコード 同等物であるmonitorenterとmonitorexitを 提供します。

これら2つのバイトコード命令は、同期ブロック ------ synchronized キーワードによってJavaコードに反映されます。同様に、同期化されたブロック間の操作もアトミックです。

2.視認性

可視性とは、スレッドが共有変数の値を変更したときに、他のスレッドがその変更にすぐに気づくことを意味します。

Javaメモリ・モデルは、変数が変更された後に新しい値をメイン・メモリに同期し、変数が読み込まれる前にメイン・メモリから新しい値をリフレッシュするために、配信媒体としてメイン・メモリに依存することによって可視性を達成します。

volatileの特別な規則により、新しい値は即座にメイン・メモリに同期され、通常の変数がこれを保証しないのに対し、使用するたびに即座にメイン・メモリからフラッシュされます。

volatile の他に、Java には実装可能なキーワードが 2 つあります。

同期ブロックの可視性は、最初にメイン・メモリに同期して戻らなければならない変数に対してアンロック操作を実行することで達成されます。

finalキーワードの可視性とは、finalによって変更されたフィールドがコンストラクタで初期化され、コンストラクタがthis参照を渡さなかった場合、finalフィールドの値が他のスレッドから見えるようになることを意味します。他のスレッドで

public static final int i;
public final int j;
static {
 i =0;
}
{
 j=1;
}
//変数iとjは可視であるため、他のスレッドから同期なしで正しくアクセスできる。

thisという参照逃げの定義について

/**
 * このエスケープをシミュレートする
 * 
 *
 */
public class ThisEscape {
 //final定数はコンストラクタで初期化されることが保証されている。
 final int i;
 //インスタンス変数に初期値があっても、それはインスタンス化される。
 int j = 0;
 static ThisEscape obj;
 public ThisEscape() {
 i=1;
 j=1;
 //このエスケープをスレッドに投げるB
 obj = new ThisEscape();
 }
 public static void main(String[] args) {
 //スレッドA:コンストラクタからのエスケープをシミュレートし、不完全なオブジェクトへの未構築の参照を投げ出す。
 /*Thread threadA = new Thread(new Runnable() {
 @Override
 public void run() {
 //obj = new ThisEscape();
 }
 });*/
 //スレッドB: オブジェクト参照を読み、変数にアクセスする。i/j 
 Thread threadB = new Thread(new Runnable() {
 @Override
 public void run() {
 //初期化に失敗する可能性のある状況の説明:インスタンス変数iの初期化がコンストラクタの外に並び替えられ、1がまだ初期化されていないとき。
 ThisEscape objB = obj;
 try {
 System.out.println(objB.j);
 } catch (NullPointerException e) {
 System.out.println("ヌル・ポインタ・エラーが発生した:共通変数jが初期化されていない");
 }
 try {
 System.out.println(objB.i);
 } catch (NullPointerException e) {
 System.out.println("ヌル・ポインタ・エラーが発生した:最終変数iが初期化されていない");
 }
 }
 });
 //threadA.start();
 threadB.start();
 }
}

3.秩序

Javaプログラムの順序性は、次のように要約できます:このスレッドで観察すれば、すべての操作は順序付けられ、あるスレッドで別のスレッドを観察すれば、すべての操作は無秩序です。

Java言語には、スレッド間の操作の順序を保証するためのvolatileキーワードとsynchronizedキーワードが用意されています。

volatileキーワード自体には、命令の並べ替えを禁止するセマンティクスが含まれており、synchronizedは、"一度に1つのスレッドだけが変数をロックできる "というルールから派生したものです。これは、"一度に1つのスレッドだけが変数をロックできる "というルールから得られます。

このルールでは、同じロックを保持する2つの同期コード・ブロックは、連続してしか入力できないことになっています。

Read next

デザインパターン(11) ヘドニック・パターン

オブジェクト指向のアイデアに基づいて設計されたアプリケーションは、同じオブジェクトや表示オブジェクトのインスタンスを大量に必要とするシナリオに遭遇することがあります。例えば、囲碁のゲームで、手を打つたびに新しいオブジェクトを作成すると、大量のメモリを消費することになります。さらに、多数のアクティブなオブジェクト...

Feb 3, 2020 · 4 min read