ホームページ >Java >&#&チュートリアル >Java での JMM 高同時実行プログラミング例の分析
JMM は Java メモリ モデルです。ハードウェア メーカーやオペレーティング システムが異なるとメモリ アクセスに一定の違いがあるため、同じコードを異なるシステムで実行するとさまざまな問題が発生します。したがって、Java メモリ モデル (JMM) は、さまざまなハードウェアおよびオペレーティング システムのメモリ アクセスの違いを保護し、さまざまなプラットフォーム上の Java プログラムに対して一貫した同時実行効果を実現します。
Java メモリ モデルでは、インスタンス変数や静的変数を含むすべての変数がメイン メモリに格納されることが規定されていますが、ローカル変数やメソッド パラメータは含まれません。各スレッドには独自の作業メモリがあります。スレッドの作業メモリには、スレッドが使用する変数とメイン メモリのコピーが保存されます。変数に対するスレッドの操作はすべて作業メモリ内で実行されます。スレッドはメイン メモリ内の変数を直接読み書きすることはできません。
異なるスレッドは、互いの作業メモリ内の変数にアクセスできません。スレッド間の変数値の転送は、メインメモリを通じて完了する必要があります。
各スレッドのワーキングメモリは独立しており、スレッドの操作データはワーキングメモリ内でのみ実行され、メインメモリにフラッシュバックされます。これは、Java メモリ モデルで定義されているスレッドの基本的な動作方法です。
注意喚起ですが、ここにいる人の中には、Java メモリ モデルを Java メモリ構造と誤解し、ヒープ、スタック、GC ガベージ コレクションに関する質問に答える人もいます。結局のところ、それは質問とは程遠いものです。面接官は聞きたいのです。実際、Java メモリ モデルについて尋ねられると、通常、マルチスレッドと Java 同時実行性に関連した質問をしたがります。
これは簡単で、Java メモリ モデル全体は実際には 3 つの特性を中心に構築されています。それは、原子性、可視性、秩序性です。これら 3 つの特性は、Java 同時実行性全体の基礎であると言えます。
原子性とは、操作が分割不可能かつ中断不可能であり、スレッドが実行中に他のスレッドによって干渉されないことを意味します。
面接官はペンを取り、コードを書きました。次のコード行はアトミック性を保証できますか?
int i = 2; int j = i; i++; i = i + 1;
最初の文は基本的な型の割り当て操作であり、アトミックな操作である必要があります。
2 番目の文は、最初に i の値を読み取り、次にそれを j に割り当てます。この 2 段階の操作ではアトミック性は保証できません。
3 番目と 4 番目の文は実際には同等です。最初に i の値を読み取り、次に 1 を読み取り、最後にそれを i に割り当てます。これは 3 段階の操作であり、アトミック性は保証できません。
JMM は基本的なアトミック性のみを保証できます。コード ブロックのアトミック性を保証したい場合は、synchronized キーワードである 2 つのバイトコード命令、monitorenter と moniterexit が提供されます。したがって、同期されたブロック間の操作はアトミックです。
可視性とは、スレッドが共有変数の値を変更すると、他のスレッドがそれが変更されたことをすぐに知ることができることを意味します。 Java は可視性を提供するために volatile キーワードを使用します。変数が volatile に変更されると、変数は変更直後にメイン メモリにフラッシュされ、他のスレッドが変数を読み取る必要がある場合は、メイン メモリから新しい値を読み取ります。通常の変数ではこれを保証できません。
volatile キーワードに加えて、final および synchronized も可視性を実現できます。
同期の原則は、実行完了後、ロック解除に入る前に、共有変数をメイン メモリに同期する必要があるということです。
最終的に変更されたフィールドは、初期化が完了すると、オブジェクトがエスケープされなければ、他のスレッドから見えるようになります (つまり、初期化の完了後にオブジェクトが他のスレッドで使用できることになります)。
Java では、synchronized または volatile を使用して、複数のスレッド間の操作の順序性を確保できます。実装原則にはいくつかの違いがあります。
volatile キーワードは、メモリ バリアを使用して命令の並べ替えを禁止し、順序性を確保します。
synchronized の原理は、スレッドがロックされた後、他のスレッドが再ロックする前にロックを解除する必要があるため、synchronized でラップされたコード ブロックは複数のスレッド間で連続して実行されます。
メモリ インタラクション操作には 8 種類あります。
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
我再补充一下JMM对8种内存交互操作制定的规则吧:
不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
不允许线程将没有assign的数据从工作内存同步到主内存。
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
很多并发编程都使用了volatile关键字,主要的作用包括两点:
保证线程间变量的可见性。
禁止CPU进行指令重排序。
volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。
volatile保证可见性的流程大概就是这个一个过程:
先说结论吧,volatile不能一定能保证线程安全。
怎么证明呢,我们看下面一段代码的运行结果就知道了:
public class VolatileTest extends Thread { private static volatile int count = 0; public static void main(String[] args) throws Exception { Vector<Thread> threads = new Vector<>(); for (int i = 0; i < 100; i++) { VolatileTest thread = new VolatileTest(); threads.add(thread); thread.start(); } //等待子线程全部完成 for (Thread thread : threads) { thread.join(); } //输出结果,正确结果应该是1000,实际却是984 System.out.println(count);//984 } @Override public void run() { for (int i = 0; i < 10; i++) { try { //休眠500毫秒 Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } count++; } } }
为什么volatile不能保证线程安全?
很简单呀,可见性不能保证操作的原子性,前面说过了count++不是原子性操作,会当做三步,先读取count的值,然后+1,最后赋值回去count变量。需要保证线程安全的话,需要使用synchronized关键字或者lock锁,给count++这段代码上锁:
private static synchronized void add() { count++; }
首先要讲一下as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。
为了使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
重排序的种类分为三种,分别是:编译器重排序,指令级并行的重排序,内存系统重排序。整个过程如下所示:
指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。
所以在多线程环境下,就需要禁止指令重排序。
volatile关键字禁止指令重排序有两层意思:
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
下面举个例子:
private static int a;//非volatile修饰变量 private static int b;//非volatile修饰变量 private static volatile int k;//volatile修饰变量 private void hello() { a = 1; //语句1 b = 2; //语句2 k = 3; //语句3 a = 4; //语句4 b = 5; //语句5 //... }
变量a,b是非volatile修饰的变量,k则使用volatile修饰。所以语句3不能放在语句1、2前,也不能放在语句4、5后。但是语句1、2的顺序是不能保证的,同理,语句4、5也不能保证顺序。
并且,执行到语句3的时候,语句1,2是肯定执行完毕的,而且语句1,2的执行结果对于语句3,4,5是可见的。
首先要讲一下内存屏障,内存屏障可以分为以下几类:
LoadLoad バリア: Load1、LoadLoad、Load2 などのステートメント用。 Load2 で読み取られるデータと後続の読み取り操作がアクセスされる前に、Load1 で読み取られるデータが読み取られていることが保証されます。
Store バリア: Store1、StoreStore、Store2 などのステートメントの場合、Store2 および後続の書き込み操作が実行される前に、Store1 の書き込み操作が他のプロセッサーから認識できることを確認してください。
LoadStore バリア: Load1、LoadStore、Store2 などのステートメントの場合、Store2 と後続の書き込み操作がフラッシュされる前に、Load1 によって読み取られるデータが完全に読み取られることを確認します。
StoreLoad バリア: Store1、StoreLoad、Load2 などのステートメントでは、Load2 と後続のすべての読み取り操作が実行される前に、Store1 の書き込みがすべてのプロセッサーに表示されることが保証されます。
各揮発性読み取り操作の後に LoadLoad バリアを挿入し、読み取り操作の後に LoadStore バリアを挿入します。
各揮発性書き込み操作の前に StoreStore バリアを挿入し、後ろに SotreLoad バリアを挿入します。
以上がJava での JMM 高同時実行プログラミング例の分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。