ホームページ  >  記事  >  Java  >  [Java 同時実行性との戦い]-----同期化の実装原理の詳細な分析

[Java 同時実行性との戦い]-----同期化の実装原理の詳細な分析

黄舟
黄舟オリジナル
2017-02-24 09:58:501120ブラウズ

私が初めて Java を学び始めたとき、マルチスレッドに遭遇するたびに、同期が使用されていたのを覚えています。当時の私たちと比較すると、同期は非常に魅力的で強力でした。私たちのソリューションは、マルチスレッド状況に対する実証済みの解決策です。しかし、研究が進むにつれて、同期はロックに比べて非常に面倒なロックであることがわかり、あまり効率的ではないと考え、徐々に放棄していきます。

確かに、Javs SE 1.6 では synchronized のさまざまな最適化が行われており、synchronized がそれほど重く感じることはありません。 LZ に従って、同期実装メカニズム、Java がそれを最適化する方法、ロック最適化メカニズム、ロック ストレージ構造、アップグレード プロセスについて調べてみましょう。同時にクリティカル セクションに入ることができ、共有変数のメモリの可視性も確保できます

Java のすべてのオブジェクトはロックとして使用でき、これが同期の同期実装の基礎です: 通常の同期:
1.メソッド、ロックは現在のインスタンス オブジェクトです

2. 静的同期メソッド、ロックは現在のクラスのクラス オブジェクトです

3. 同期メソッド ブロック、ロックは括弧内のオブジェクトです

スレッドが同期されたコードにアクセスするとき同期コードを実行するには、まずロックを取得する必要があります。では、このメカニズムはどのように実装されているのでしょうか。まずは簡単なコードを見てみましょう:

public class SynchronizedTest {
    public synchronized void test1(){

    }    public void test2(){        synchronized (this){

        }
    }
}

javap ツールを使用して、生成されたクラス ファイル情報を表示し、Synchronize の実装を分析します

上記からわかるように、同期コード ブロックは、monitorenter を使用して実装されています。同期メソッド (JVM の基礎となる実装を確認する必要があることは明らかではありません。) は、メソッド修飾子の ACC_SYNCHRONIZED 実装に依存します。

同期コード ブロック
:monitorenter 命令は同期コード ブロックの先頭に挿入され、monitorexit 命令は同期コード ブロックの最後に挿入されます。JVM は、各 Monitorenter に対応する Monitorexit があることを確認する必要があります。 。どのオブジェクトにもモニターが関連付けられています。モニターを保持すると、そのオブジェクトはロック状態になります。スレッドは、monitorenter 命令を実行すると、オブジェクトに対応するモニターの所有権を取得しようとします。つまり、オブジェクトのロックを取得しようとします。 [Java 同時実行性との戦い]-----同期化の実装原理の詳細な分析
同期メソッド
: 同期メソッドは、通常のメソッド呼び出しと戻り命令に変換されます。 invokevirtual、areturn 命令など、synchronized によって変更されたメソッドを実装するための VM バイトコード レベルの特別な命令はありません。代わりに、クラスのメソッド テーブル内のメソッドの access_flags フィールドの同期フラグの位置が 1 に設定されます。ファイルを作成し、メソッドが同期メソッドであることを示します。また、メソッドを呼び出すオブジェクト、またはメソッドが属するクラスを使用して、JVM の内部オブジェクトのロック オブジェクトとして Klass を表します。 (抜粋: http://www.php.cn/)分析を続けましょう。ただし、さらに深く進む前に、Java オブジェクト ヘッダーとモニターという 2 つの重要な概念を理解する必要があります。
Java オブジェクト ヘッダーとモニター

Java オブジェクト ヘッダーとモニターは同期を実現するための基礎です。これら 2 つの概念については、以下で詳しく紹介します。

Java オブジェクト ヘッダー

synchronized で使用されるロックは Java オブジェクト ヘッダーに保存されます。 Hotspot 仮想マシンのオブジェクト ヘッダーには、主に Mark Word (マーク フィールド) と Klass Pointer (タイプ ポインター) の 2 つの部分のデータが含まれています。このうち、Klass Point は、オブジェクトのクラス メタデータへのポインタであり、仮想マシンはこのポインタを使用して、オブジェクト自体の実行時データを保存するために使用されるオブジェクトを決定します。バイアスロックの鍵となるため、以下でそれに焦点を当てます

単語をマークします。
Mark Word は、ハッシュ コード (HashCode)、GC 生成経過時間、ロック ステータス フラグ、スレッドが保持するロック、偏ったスレッド ID、偏ったタイムスタンプなど、オブジェクト自体の実行時データを保存するために使用されます。 Java オブジェクト ヘッダーは通常 2 つのマシン コードを占有します (32 ビット仮想マシンでは、1 つのマシン コードは 4 バイト、つまり 32 ビットに相当します)。ただし、オブジェクトが配列型の場合は、JVM 仮想マシン コードが存在するため、3 つのマシン コードが必要になります。 Java オブジェクトのサイズは Java オブジェクトのメタデータ情報によって決まりますが、配列のサイズは配列のメタデータから確認できないため、配列の長さを記録するためにブロックが使用されます。次の図は、Java オブジェクト ヘッダー (32 ビット仮想マシン) のストレージ構造です。
[Java 同時実行性との戦い]-----同期化の実装原理の詳細な分析
オブジェクト ヘッダー情報は、オブジェクト自体によって定義されるデータとは無関係の追加のストレージ コストですが、仮想マシンのスペース効率を考慮しています。 Mark Word は、非常に小さなメモリ領域にできるだけ多くのデータを格納するために、オブジェクトの状態に応じて独自の記憶領域を再利用するように設計されています。 Word はプログラムの実行に応じて変化し、変化ステータスは次のとおりです (32 ビット仮想マシン):
[Java 同時実行性との戦い]-----同期化の実装原理の詳細な分析

Java オブジェクト ヘッダーについて簡単に説明します。次に、モニターを見てみましょう。

モニター

モニターとは何ですか?これは同期ツールとして理解することも、同期メカニズムとして説明することもできます。通常はオブジェクトとして説明されます。
すべてがオブジェクトであるのと同じように、すべての Java オブジェクトはモニターになる可能性を持っています。Java の設計では、すべての Java オブジェクトは目に見えない力で子宮から出てくるためです。内部ロックまたはモニターロックと呼ばれます。
モニターはスレッドプライベートのデータ構造であり、各スレッドには使用可能なモニターレコードのリストがあり、グローバルな使用可能なリストもあります。ロックされた各オブジェクトはモニターに関連付けられます (オブジェクト ヘッダーの MarkWord 内の LockWord は、モニターの開始アドレスを指します)。同時に、モニターには、スレッドの一意の識別子を格納する Owner フィールドがあります。ロックを所有しています。これは、ロックが占有されているこのスレッドによって所有されていることを示します。その構造は次のとおりです:
[Java 同時実行性との戦い]-----同期化の実装原理の詳細な分析
Owner: 最初は NULL は、現在モニター レコードを所有しているスレッドがないことを意味します。スレッドがロックを正常に所有すると、スレッドの一意の識別子が保存され、ロックが解放されると、その識別子が設定されます。 NULL に;
EntryQ : システムのミューテックス ロック (セマフォ) を関連付けて、モニター レコードのロックに失敗したすべてのスレッドをブロックします。
RcThis: モニター レコードでブロックされている、または待機しているすべてのスレッドの数を示します。
Nest: 再入ロックのカウントを実装するために使用されます。
HashCode: オブジェクトヘッダーからコピーされた HashCode 値を保存します (GC 経過時間も含まれる場合があります)。
候補: ロックを解放する前のスレッドがブロックまたは待機しているすべてのスレッドを起動すると、一度に 1 つのスレッドだけが正常にロックを所有できるため、不要なブロックやスレッドの起動の待機を回避するために使用されます。不必要なコンテキストの切り替え (ブロックから準備完了、そして競合するロックの失敗により再びブロック) が発生し、パフォーマンスが大幅に低下します。 Candidate には 2 つの値しかありません。0 は起動する必要のあるスレッドがないことを意味し、1 はロックを競合するために後続スレッドを起動する必要があることを意味します。
Java での synchronized の実装原則とアプリケーションから抜粋)
synchronized は重量級のロックであり、あまり効率的ではないことはわかっていますが、同時に、この概念は常に念頭にありましたが、さまざまな最適化が行われてきました。 jdk 1.6 での synchronize の実装により、重さが軽くなったように見えます。では、JVM ではどのような最適化方法が採用されているのでしょうか。

ロックの最適化

jdk1.6 では、スピン ロック、適応スピン ロック、ロックの削除、ロック粗化、バイアス ロック、軽量ロック、およびロック操作を減らすその他のテクノロジなど、ロックの実装に多数の最適化が導入されています。経費。
ロックには主に、ロックなし状態、偏ったロック状態、軽量ロック状態、重量ロック状態の 4 つの状態が存在し、激しい競争によって徐々にアップグレードされます。ロックはアップグレードできますが、ダウングレードはできないことに注意してください。この戦略は、ロックの取得と解放の効率を向上させることを目的としています。

自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:

    public void vectorTest(){
        Vector<String> vector = new Vector<String>();        for(int i = 0 ; i < 10 ; i++){
            vector.add(i + "");
        }

        System.out.println(vector);
    }

在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的,LZ也一直坚持着这个观点。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

軽量ロック

軽量ロックを導入する主な目的は、マルチスレッドの競合を行わずにオペレーティング システムのミューテックスを使用する従来の重量ロックによって引き起こされるパフォーマンスの消費を削減することです。バイアス ロック機能がオフになっている場合、または複数のスレッドがバイアス ロックを競合してバイアス ロックが軽量ロックにアップグレードされる場合、軽量ロックの取得が試行されます。手順は次のとおりです。 lock
1. 現在のオブジェクトがロック フリー ステータス (ハッシュコード、0、01) であるかどうかを確認します。そうである場合、JVM はまず現在のスレッドのスタック フレームにロック レコードと呼ばれるスペースを作成し、現在のコピーを保存します。ロック オブジェクトの Mark Word (正式には、この A Displaced プレフィックスがコピーに追加されます。つまり、Displaced Mark Word)。それ以外の場合、JVM は CAS 操作を使用して次のことを試みます。ロック レコードを指すようにオブジェクトのマーク ワードを更新します。成功した場合は、ロックが完了したことを意味し、ロック フラグを 00 (このオブジェクトが軽量ロック状態にあることを示します) に変更し、同期操作を実行します。失敗した場合は、ステップ (3) を実行します。 3. 現在のオブジェクトのマーク ワードが現在のスレッドのスタック フレームを指しているかどうかを確認します。そうであれば、現在のスレッドがすでに現在のスレッドのロックを保持していることを意味します。それ以外の場合は、ロック オブジェクトが他のスレッドによってプリエンプトされたことを意味するだけであり、軽量ロックは重量ロックに拡張される必要があり、ロック フラグは になります。 10 と、後で待機しているスレッドがブロック状態になります。

ロックを解放します。
軽量ロックの解放も CAS 操作によって実行されます。

1. 軽量ロックを取り出して保存します。置き換えられたマーク ワード内のデータをロックします。

2. CAS 操作を使用して、現在のオブジェクトのマーク ワード内のフェッチされたデータを置き換えます。そうでない場合は、(3) を実行します。 CAS 操作が置き換えに失敗した場合、他のスレッドがロックを取得しようとした場合、ロックを解放するときに中断されたスレッドをウェイクアップする必要があることを意味します。 軽量ロックの場合、パフォーマンス向上の基礎は「ほとんどのロックでは、ライフサイクル全体を通じて競合が存在しない」ということです。この基礎が崩れると、相互排他によるオーバーヘッドに加えて、追加の CAS も発生します。そのため、マルチスレッド競合の場合、軽量ロックは重量ロックよりも遅くなります


次の図は、軽量ロックの取得と解放のプロセスです

バイアスされたロックの主な目的ロックは、マルチスレッドの競合を発生させずに、不要な軽量ロックの実行パスを最小限に抑えることです。前述したように、軽量ロックのロックおよびロック解除操作には、複数の CAS アトミック命令が必要です。では、バイアスされたロックはどのようにして不必要な CAS 操作を減らすのでしょうか?それはマークの作品の構造を見れば分かります。バイアスされたロックであるかどうかを確認する必要があるだけです。ロック識別子は、ThreadID です。 処理フローは次のとおりです。

ロックを取得する
[Java 同時実行性との戦い]-----同期化の実装原理の詳細な分析1. マーク ワードがバイアス可能な状態にあるかどうかを確認します。バイアスされたロック 1 であり、ロック識別ビットは

1 です。バイアス可能な状態にある場合は、スレッド ID が現在のスレッド ID であるかどうかをテストします。そうである場合は、ステップ (3) を実行します。 );

1. スレッド ID が現在のスレッド ID でない場合は、パスします。 CAS 操作はロックを競合します。競合が成功した場合は、Mark Word のスレッド ID が現在のスレッド ID に置き換えられます。そうでない場合は、スレッド (4) ) が実行されます。

4. CAS がロックを競合できないということは、現在マルチスレッドの競合状況が存在することを示しており、バイアスされたロックを取得したスレッドは一時停止されます。ロックは軽量ロックにアップグレードされ、安全なポイントでブロックされたスレッドは同期コード ブロックを実行し続けます


ロックを解放します
ロックの解放には、次のようなメカニズムが採用されています。競合によってロックが解放されると、スレッドは率先してバイアス ロックを解放せず、他のスレッドが競合するまで待つ必要があります。バイアスされたロックの取り消しは、グローバル安全ポイントを待つ必要があります (この時点は、実行中のコードが存在しないときです)。手順は次のとおりです。
1. バイアスされたロックを保持しているスレッドを一時停止し、ロック オブジェクトがまだロックされているかどうかを確認します。
2. バイアスされたロックを解除し、ロックなしの状態 (01) または軽量ロック状態に戻ります。

下の図は、バイアスされたロックの取得と解放のプロセスを示しています


ヘビーウェイトロック

ヘビーウェイトロックは、オブジェクト内のモニター(モニター)を通じて実装されます。モニターの本質は、ミューテックスロックに依存することです。スレッド間のオペレーション システムの切り替えにはユーザー モードからカーネル モードへの切り替えが必要であり、切り替えコストが非常に高くなります。


上記は [Deadly Java Concurrency]-----synchronized の実装原理の詳細な分析です。さらに関連する内容については、PHP 中国語 Web サイト (www.php.cn) に注目してください。


[Java 同時実行性との戦い]-----同期化の実装原理の詳細な分析

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。