Javaの並行処理 - 揮発性の話

Javaの並行処理 - 揮発性の話

入門

volatileキーワードを言えば、ほとんどの開発者は一定の理解を持って、徹底した非常に奇妙なキーワードの後に​​、開発者は非常に精通していると言うことができます。同等の軽量でも同期に比べて、少ないオーバーヘッドパフォーマンスに軽量ロックとして知られている、同期するだけでなく、可視性、秩序と不可分の一部で持って、Javaの並行処理が鍵となることが非常に重要です。この記事では、我々は彼が可視性、秩序といくつかの原子であることを確認するだけでなく、volatileキーワードの典型的なアプリケーションシナリオの一部を要約する方法のアップの詳細な分析の基本原理揮発ます。

揮発性「部分」アトミック

いわゆるアトミックは、アクションが完了し、全体であること、他のスレッドでは、この操作の開始または、操作が取引にやや似中央を、表示されません完了しているではないことが表示されます。

基本的に揮発性がアトミック使用できないため、なぜだけ揮発性アトミック「部」は、彼が割り当てを読んで、ただ一つの変数を変更し、ほとんどの場合、単一の変数自体はアトミックですが、言っています、32ビットのJava仮想マシンの操作に長い/二重可変である一つの例外があります。

変数がない場合、Java仮想マシン32において、ロング/二重変数は、読み取りおよび書き込み操作を二つの部分に分割され、低い、または副で第一の高書込み32ビット、32ビットの書き込みはその逆なので、変数は、揮発性として宣言されていることつまり、ない単一全体的な読書ので、ロング/ダブル変数を書いて読み書きするために複数のスレッドが、おそらく予期しない結果を引き起こすことがあるときに、唯一の単一の長い/ double型の変数の揮発性の修正を使用した後、原子性を持っていません。読者は、原子の特性を有しています。だけの簡単な修正を読み取るためには、揮発性使用する必要がない場合は、Java仮想マシン64では、ロング/ダブル変数自体は、アトミック書き込みがあります。

理解する必要が唯一の変数が唯一の保証アトミック操作ですが、また、保証アトミック変数複合動作のためにしません、これはノートの場所である揮発性の読み取りおよび書き込みで、個々の変数と増分の最も古典的なシーンですデクリメント。

private volatile static int increaseI = 0;

public static void main(String[] args) {
	for (int i = 0; i < 100000; i++) {
		Thread thread = new Thread(new Runnable() {
			
			@Override
			public void run() {
				
				increaseI++;
			}
		}, String.valueOf(i));
		thread.start();
	}
	
	while(Thread.activeCount()>1)  
		Thread.yield();
	System.out.println(increaseI);
}
复制代码

あなたがテストに合格した場合、あなたは、多くの時間を見つけるの結果が100,000でない出力します。volatile変数のみ読み書き変数はアトミックで保証することができ、かつincreaseI ++は複雑な操作であるため、これは、彼は単にに分けることができますされています。

var = increaseI; //步骤1:将increaseI的值加载到寄存器var

var = var + 1;//步骤2:将寄存器var的值增加1

increaseI = var;//步骤3:将寄存器var的值写入increaseI
复制代码

揮発性の缶は、第一工程及び第3の単原子の動作を保証し、increaseI ++アトミック操作が変更揮発性ではない原子の全体インクリメントとデクリメント処理を、保証することはできません。次の図は、この問題を説明することができます。

 

 

 

揮発性の可視性

可視性について、目の前での「Java並行処理(2) - チャットが起こる-前に」の記事は、運用効率を向上させるためには、読み書き共有変数が変数である場合には、ローカルメモリのスレッドで行われ、言いました更新、およびマルチスレッド環境でメインメモリへのタイムリーな結果変数のリフレッシュ戻った後、他のスレッドは変数の最新の値にタイムリーに読むことはありません。私たちは、それを分析するために、次のコードから見ることができます。

private static boolean flag = false;
	
private static void refershFlag() throws InterruptedException {
	
	Thread threadA = new Thread(new Runnable() {
		
		@Override
		public void run() {
			while (!flag) {
				//do something
			}
		}
	});
	
	Thread threadB = new Thread(new Runnable() {
		
		@Override
		public void run() {
			
			flag = true;
		}
	});
	
	DateFormat dateFormat  = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
	
	System.out.println("threadA start" + dateFormat.format(new java.util.Date()));
	threadA.start();
	
	Thread.sleep(100);
	
	threadB.start();
	
	threadA.join();
	System.out.println("threadA end" + dateFormat.format(new java.util.Date()));
}

//threadA start2018/07/25 16:48:41
复制代码

通常のBスレッドが変数のフラグを更新した後に、論理的に、スレッドはすぐに撤退する必要がありますが、仮想マシンは、アカウントにvolatile修飾子を取って共有変数を使用していないため、実際には、時間とスレッドBの多くは、すぐに終了しないだろう、デフォルトの変数は必要ありませんその最適化されたマルチスレッドアクセス、他のスレッドがメインメモリの結果を読む時間がないながら、共有変数につながることは、メインメモリにリフレッシュフラグバックをしませんでした。その後、我々は何が起こるかをマーク揮発性フラグに変数を追加しますか?

private volatile static boolean flag = false;

//threadA start2018/07/25 16:48:59
//threadA end2018/07/25 16:48:59
复制代码

スレッドは、すぐに見られる揮発性、視認性を見ることができ、この時点から撤退することができます。

揮発性秩序

JMMが起こる-前にシングルスレッド複数のスレッドの秩序と適切な同期を確保するために、それらの間の揮発性変数ルール上のルールに基づいて:書き込みをvolatile変数に起こる-前に、変数の読みの背後に動作しています。

そのノートには、2つのポイントがあります。まず、同じvolatile変数への書き込みは、唯一の事前発生関係読み込みの間、第二の点を、時間順に、起こる-前にする読書書かなければなりません操作。で - 例の「Java並行処理(2)話が起こる-前に」揮発性の優れた記述に再ソート機能の並べ替え禁止しています。

public class AAndB {

	int x = 0;
	int y = 0;
	int a = 0;
	int b = 0;
	
	public void awrite() {

		a = 1;
		x = b;
	}
	
	public void bwrite() {

		b = 1;
		y = a;
	}
}

public class AThread extends Thread{

	private AAndB aAndB;
	
	public AThread(AAndB aAndB) {
		
		this.aAndB = aAndB;
	}
	
	@Override
	public void run() {
		super.run();
		
		this.aAndB.awrite();
	}
}

public class BThread extends Thread{

	private AAndB aAndB;
	
	public BThread(AAndB aAndB) {
		
		this.aAndB = aAndB;
	}
	
	@Override
	public void run() {
		super.run();
		
		this.aAndB.bwrite();
	}
}

private static void testReSort() throws InterruptedException {

	AAndB aAndB = new AAndB();

	for (int i = 0; i < 10000; i++) {
		AThread aThread = new AThread(aAndB);
		BThread bThread = new BThread(aAndB);

		aThread.start();
		bThread.start();

		aThread.join();
		bThread.join();

		if (aAndB.x == 0 && aAndB.y == 0) {
			System.out.println("resort");
		}

		aAndB.x = aAndB.y = aAndB.a = aAndB.b = 0;

	}

	System.out.println("end");
}
复制代码

AとBのスレッドスレッドが浮上しているときの並べ替えは、リゾートをプリントアウトするかもしれないが、この状況が再び発生しません後の変数は、変数が揮発性となっています。

揮発性の二つの典型的な使用シナリオ

図1は、状態量を示すために使用されます。状態量は、ロジックを実行する必要があるかどうかを決定するブール変数で示されています。上記は、揮発性、視認性のコードです:

Thread threadA = new Thread(new Runnable() {
	
	@Override
	public void run() {
		while (!flag) {
			//do something
		}
	}
});

Thread threadB = new Thread(new Runnable() {
	
	@Override
	public void run() {
		
		flag = true;
	}
});
复制代码

あなたは、同期や言葉遣いをロックを使用する場合、揮発性の変数は、この問題に良い解決策を変更する場合は、他のスレッドが更新を強制する一方、メインメモリへのタイムリーなリフレッシュバックの状態量を確保するために、より複雑になるだろうが。

2ダブルチェックの質問は、ダブルチェックすべき問題は、シーンの最も揮発性の使用です。次のコードに示すように:

public class DoubleCheck {

	private volatile static DoubleCheck instance = null;
	
	private DoubleCheck() {
		
	}
	
	public static DoubleCheck getInstance() {
		
		if (null == instance) {   //步骤一
			synchronized (DoubleCheck.class) {
				if (null == instance) {   //步骤二
					instance = new DoubleCheck();   //步骤三
				}
			}
		}
		return instance;
	}
	
	public static void main(String[] args) throws InterruptedException {

		DoubleCheck doubleCheck = DoubleCheck.getInstance();
	}
}
复制代码

コード内の三つはない原子ステップであり、3つのステップに分けることができ、以前の増加に幾分類似しています。

3.1は、メモリアロケーションメモリアドレスがDoubleCheckで割り当てます

3.2 DoubleCheckはDoubleCheckを初期化オブジェクトを初期化

3.3インスタンス・インスタンス>メモリアドレスを指し示すアドレス参照

CPUで3.2と3.3 3.2と3.3を並べ替えた場合、再注文である可能性が高い何の依存性がないようです。

 

 

ステップ瞬間に決定され、スレッド2は、オブジェクトの実際に空のインスタンスが初期化されていない場合ではなく、3.2は行われません。次は、エラーが発生するオブジェクトのリードを使用しています。揮発性インスタンス変数を使用して、この時間は、複数のスレッドがコードにアクセスするときにこのようにして正確さを保証する、3.2および3.3を並べ替え防止するために修飾することができます。

私たちは、現在の規則的な実行を保証するために、より多くのステップ3でロック命令よりもvolatileキーワードの使用でコンパイルされたコードを表示することができます:揮発使用しないでください。

 

 

揮発性の使用

 

 

揮発性の原則の背後にあります

DoubleCheckアセンブリコードでは、我々はラインロック命令を加え、コンパイル後の揮発性のキーワードよりも多くのコードを参照してください、そして、この命令は、それがどういう意味を表しますか?

ロック命令は、2つの機能があります。

  1. CPUバスとキャッシュのロックのうち、命令は、メインメモリへのデータのキャッシュバックをリフレッシュすると、ロックを解除し、ロック後に実行されています。
  2. lock会让其他CPU高速缓存中的缓存行失效,其他CPU读取时必须要从主内存加载最新数据。

简单来说就是lock指令可以实现缓存一致性。通过lock指令的这两个功能,我们就可以很简单的理解当共享变量flag用volatile修饰后,每次更新flag的值都会导致缓存行的数据强制刷新最新值到主内存,volatile变量之前的数据也会被刷新回主内存。同时其他线程必须到主内存读取最新flag的值。这样就实现了共享变量的可见性以及有序性。


参考资料:

  1. 《深入理解Java虚拟机》
  2. 《Java并发编程的艺术》
发布了900 篇原创文章 · 获赞 387 · 访问量 279万+

おすすめ

転載: blog.csdn.net/kingmax54212008/article/details/104067531