オリジナル:カバラツリーhttps://juejin.im/post/5a2b53b7f265da432a7b821c
Java関連の就職の面接では、多くの雇用のマネージャーは、Java並行処理の理解のインタビュアーレベルを調査するために、と小さなエントリポイントなどの揮発性キーワードに、多くの場合、最後に質問することができたいと、Javaのメモリモデル(JMM)、ジャワいくつかの特徴が、あなたは基礎となるJVMおよびオペレーティングシステムの知識を得ることができ、並行プログラミング、綿密な調査を行っ関与しています。
ここでは、それvolitileキーワードの下への洞察を得るために、仮想的な面接のプロセスを持っています!
インタビュアー:同時のJavaのこの種を理解する方法?volatileキーワードのあなたの理解についての話
私の知る限り理解するように、修飾された揮発性の共有変数が、それは、以下の2つの特徴を有する:1メモリ可変動作の異なるスレッドの視認性を確保するために、2命令の並べ替えを禁止します。
インタビュアー:あなたは、メモリの可視性が何であるかを詳しく、それをどのように並べ替えていることはできますか?
このチャットまでずっと、私はまだJavaのメモリモデルから、それについて話しています。
さまざまなプラットフォーム上でJavaプログラムが同じ効果メモリアクセスを実現するためにそうすることを、ハードウェアとオペレーティングシステムの違いの様々なメモリアクセスを遮断するために、Javaのメモリモデル(JMM)を定義しようとしているJava仮想マシン仕様。CPUの命令実行速度が非常に高速ですが、多くの低速の上のメモリアクセスの速度は、大きさの差がないため、そのキャッシュのリガいくつかの層に大物プロセッサとCPUのグループを行うため、単純に、置きます。
Javaメモリー・モデルでは、上記の最適化は、抽象化の波を行いました。JMMは、すべての変数は、上記の共通メモリに似たメインメモリの存在、ある必要があり、各スレッドは、レジスタやCPUのキャッシュとして見ることができる理解しやすい、独自のワーキングメモリが含まれています。操作スレッドが仕事にメインメモリであるので、彼らは独自のメモリ作品にアクセスすることができ、作業の前と後に、我々は同期してメインメモリに値背中を置く必要があります。
私はいくつかの明確なを入れて持っているので、絵に関する枚の紙を取ります:
場合は、スレッドの実行、その後、その後、ワーキングメモリにコピーをロードし、次に実行するプロセッサに渡され、その後、完成した作品の割り当てメモリのコピーを渡し、作業記憶し、メインメモリ変数から値を読み、そしてれる最初のメインメモリへの値のバックは、メインメモリの値が更新されます。
ワーキングメモリとメインメモリ、ものの加速率を使用するが、それはまた、いくつかの問題をもたらします。たとえば、次の例を参照してください:
私は、1 + =。
私は0の初期値、一つのスレッドだけがそれを実行しているときに、2つのスレッドが実行すると、結果は、1を得るには、あなたが結果2を取得することを想定しますか?必ずしもそうではありません。そのような場合があるかもしれません。
1スレッド:負荷は、メインメモリからI // I 0 = I + 1 // I = 1 スレッド2:負荷私のメインメモリから // ので、私は、メインメモリの値を書き戻していなかったスレッド1、私はまだ0ので 、私。+ 1 // I. 1 = スレッド1:私セーブでメインメモリへ 2スレッド:メインメモリに私を保存
上記の方法に従って実行の二つのスレッド場合、私の最終的な値は、実際には1 Aです。最後のライトバックを有効にするに遅い場合は、その後、iの値をリードキャッシュの矛盾である、可能性が0になることです。
ここでは、あなただけのJMMは、主にどのように同時プロセスの処理方法をを中心に、質問をしていることを言及すべきである原子性を、可視性との順序付けすることができます、これらの3つの課題に取り組むことで、ビルドにこれらの3つの特徴キャッシュ矛盾を解放します。視認性と秩序を持つ揮発性が関係しています。
インタビュアー:あなたは、特にこれらの三つの特徴が何について何を語るのか?
1.アトミック(不可分)
Javaの、基本的な読書のデータ型と代入演算子はアトミック操作である、いわゆる原子操作は、これらの操作を意味し、中断されない、いくつかは行われない、またはまったく実行はありません。例えば:
I = 2 ; J = I; I ++ ; 私は私は1 + =。
4つの業務上の、私は2は、読み出し動作である=、操作がアトミックである必要があり、J =はアトミック操作であるあなたは、実際に、最初に、iの値を読み込み、2つのステップに分割され、その後に割り当てられた、と思いますか3ステップの操作であるメインメモリへの書き戻し、増分、2段階の操作でjは、++ I、アトミック操作を呼び出すことができず、私の値が読み出され、I + 1が実際に等価です= したがって、上記の例では、理由の原子の最後の多くの例の可能な値は、満たすことができません。
だからと言って、単純な読書の割り当てがアトミックである、それだけでより多くの可変ステップ読み出し動作の値よりも変数の言葉に、数学的な評価を使用することができます。例外は、仮想マシンの仕様は、処理動作の32〜2回に分けて64ビットのデータタイプ(ロングとダブル)を可能にするが、最新のJDK実装がアトミック操作を達成したということです。
JMMのI上記のように動作として、基本的な原子道具++、およびアトミック・ブロック・コードを保証するために、ロックによって同期されなければなりません。スレッドがロックを解除する前に、iの値は、メインメモリにブラシバックにバインドされています。
2.可視性(視認性)
可視性といえば、Javaは可視性を提供するために、揮発性の使用です。変数がvolatile宣言されると、それはすぐにメインメモリ、他のスレッドが変数を読むために必要がある場合、メモリに新しい値を読み込むにフラッシュ変わります。一般的な変数は、これを保証することはできません。
実際には、同期とロックがロックを解放する前に、スレッドの可視性を保証できることで、メインメモリに変数ブラシ背面の値を共有するが、同期とロックの費用は大きいだろう。
3.発注(発注)
JMMは、コンパイラやプロセッサの命令が並べ替えできますが、つまりは関係なく並べ替えの実行結果は、プログラムを変更することはできませんどのよう-IF-シリアルセマンティクス、用意されています。例えば、次のプログラム・セグメント:
ダブル PI = 3.14; // ダブル R = 1。 // B ダブル S = PI * R *、R。// C
上記のステートメントは、結果は3.14であるが、B-の実行順序に従って> A-> Cであってもよく、A、Bは、2つの別々の文であるため、およびCに依存して、A-> B-> Cの実行に従うことができA、B、その結果、Bを並べ替えることができ、それはC、前述のBに排出することができません。JMMは、並べ替えは、シングルスレッドの実装には影響しません保証するが、複数のスレッドで問題。
たとえば、このコード:
int型、A = 0 ; ブールフラグ = 偽。 公共 のボイド書き込み(){= 2; // 1 フラグ= 真。 // 2 } 公共 ボイド乗算(){ 場合(フラグ){ // 3 INT RET = *。// 4 } }
上記のコードを実行する2つのスレッドがある場合、スレッド1は、最初の書き込みを実行する多重実行して2スレッド、その後、RET最終的な値は4右でなければなりませんか?結果は必ずしもありません。
図に示すように、方法1及び2における書き込み並べ替えない、スレッド割り当てフラグの最初の対が真で、次いでスレッド2の実装、RET直接計算結果は、時間に割り当てられた1、スレッド2 、それはステップ遅れることは明らかです。
この時間は、秩序を保証するためにも同期ヘビー級とロックに、プログラム「秩序」という並べ替え性を保証を禁止し、フラグvolatileキーワードとして追加することができ、彼らはそのエリアどこコード確保することができます完成したら、それは実行されます。
さらに、JMMはすなわち、任意の手段を経由せずに一般に呼ばれる、秩序を保証することができ、いくつかの固有の順序を持って起こる-前に原則。<< JSR-133:Javaのメモリモデルと仕様スレッドは>> 次のことが起こり、前のルール定義されています。
(1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
(2)监视器锁规则:对一个线程的解锁,happens-before于随后对这个线程的加锁
(3)volatile变量规则:对一个volatile域的写,happens-before于后续对这个volatile域的读
(4)传递性:如果A happens-before B ,且 B happens-before C, 那么 A happens-before C
(5)start()规则:如果线程A执行操作ThreadB_start()(启动线程B), 那么A线程的ThreadB_start() happens-before 于B中的任意操作
(6)join()原则:如果A执行ThreadB.join()并且成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
(7)interrupt()原则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生
(8)finalize()原则:一个对象的初始化完成先行发生于它的finalize()方法的开始
第1条规则程序顺序规则是说在一个线程里,所有的操作都是按顺序的,但是在JMM里其实只要执行结果一样,是允许重排序的,这边的happens-before强调的重点也是单线程执行结果的正确性,但是无法保证多线程也是如此。
第2条规则监视器规则其实也好理解,就是在加锁之前,确定这个锁之前已经被释放了,才能继续加锁。
第3条规则,就适用到所讨论的volatile,如果一个线程先去写一个变量,另外一个线程再去读,那么写入操作一定在读操作之前。
第4条规则,就是happens-before的传递性。
后面几条就不再一一赘述了。
面试官:volatile关键字如何满足并发编程的三大特性的?
那就要重提volatile变量规则:对一个volatile域的写,happens-before于后续对这个volatile域的读。
这条再拎出来说,其实就是如果一个变量声明成是volatile的,那么当我读变量时,总是能读到它的最新值,这里最新值是指不管其它哪个线程对该变量做了写操作,都会立刻被更新到主存里,我也能从主存里读到这个刚写入的值。也就是说volatile关键字可以保证可见性以及有序性。
继续拿上面的一段代码举例:
int a = 0; bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a;//4 } }
这段代码不仅仅受到重排序的困扰,即使1、2没有重排序。3也不会那么顺利的执行的。假设还是线程1先执行write操作,线程2再执行multiply操作,由于线程1是在工作内存里把flag赋值为1,不一定立刻写回主存,所以线程2执行时,multiply再从主存读flag值,仍然可能为false,那么括号里的语句将不会执行。
如果改成下面这样:
int a = 0; volatile bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a;//4 } }
那么线程1先执行write,线程2再执行multiply。根据happens-before原则,这个过程会满足以下3类规则:
(1)程序顺序规则:1 happens-before 2; 3 happens-before 4; (volatile限制了指令重排序,所以1 在2 之前执行)
(2)volatile规则:2 happens-before 3
(3)传递性规则:1 happens-before 4
从内存语义上来看:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
面试官:volatile的两点内存语义能保证可见性和有序性,但是能保证原子性吗?
首先我回答是不能保证原子性,要是说能保证,也只是对单个volatile变量的读/写具有原子性,但是对于类似volatile++这样的复合操作就无能为力了,比如下面的例子:
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
按道理来说结果是10000,但是运行下很可能是个小于10000的值。有人可能会说volatile不是保证了可见性啊,一个线程对inc的修改,另外一个线程应该立刻看到啊!可是这里的操作inc++是个复合操作啊,包括读取inc的值,对其自增,然后再写回主存。
假设线程A,读取了inc的值为10,这时候被阻塞了,因为没有对变量进行修改,触发不了volatile规则。
线程B此时也读读inc的值,主存里inc的值依旧为10,做自增,然后立刻就被写回主存了,为11。
此时又轮到线程A执行,由于工作内存里保存的是10,所以继续做自增,再写回主存,11又被写了一遍。所以虽然两个线程执行了两次increase(),结果却只加了一次。
有人说,volatile不是会使缓存行无效的吗?但是这里线程A读取到线程B也进行操作之前,并没有修改inc值,所以线程B读取的时候,还是读的10。
又有人说,线程B将11写回主存,不会把线程A的缓存行设为无效吗?但是线程A的读取操作已经做过了啊,只有在做读取操作时,发现自己缓存行无效,才会去读主存的值,所以这里线程A只能继续做自增了。
综上所述,在这种复合操作的情景下,原子性的功能是维持不了了。但是volatile在上面那种设置flag值的例子里,由于对flag的读/写操作都是单步的,所以还是能保证原子性的。
要想保证原子性,只能借助于synchronized,Lock以及并发包下的atomic的原子操作类了,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。
面试官:说的还可以,那你知道volatile底层的实现机制?
如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令。
lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:
(1)重排序时不能把后面的指令重排序到内存屏障之前的位置
(2)使得本CPU的Cache写入内存
(3)写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。
面试官:你在哪里会使用到volatile,举两个例子呢?
状态量标记,就如上面对flag的标记
int a = 0; volatile bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a;//4 } }
这种对变量的读写操作,标记为volatile可以保证修改对线程立刻可见。比synchronized,Lock有一定的效率提升。
单例模式的实现,典型的双重检查锁定(DCL)
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给instance加上了volatile。