同期使用の原則とロック最適化のアップグレードプロセス(インタビュー)

目次

 

前書き

同期使用レベル

同期されたJVMレベル

同期された最適化レベル

総括する


前書き


マルチスレッドは常に面接の焦点であり困難でした。あなたがどのレベルにいても、同期されたキーワードを学ぶことは避けられません。これは私の経験です。面接の考え方と同期について体系的に説明しましょう。面接官から質問があったら、同期についての理解を教えてください。同期された使用レベル同期されたJVMレベル、および同期された最適化レベルの3つの側面から体系的に答えることができます。おそらく、インタビュアーはあなたを賞賛して見ます!記事には理解しやすいコードがたくさん含まれています。時間があれば、自分でコードを作成して理解と記憶を深める必要があります。この記事があなたに役立つことができれば、私は創造の道に最も満足しています。

image.png

同期使用レベル


同期がロックであることは誰もが知っていますが、ロックとは正確には何ですか?たとえば、鍵はトイレのドアの鍵の唯一の鍵であると理解できます。誰もがこの鍵を使用してトイレの扉を開けることができます。この鍵は一度に1人しか所有できません。キーはトイレに繰り返し出入りすることができます。プログラムでは、この繰り返しのトイレの出入り動作をリエントリーロックと呼びます。静的メソッド、インスタンスメソッド、コードブロックを変更できるので、コードロックの同期に使用される同期の意味を見てみましょう。

  • 通常の同期メソッドの場合、オブジェクトインスタンスはロックされます。
  • 静的同期メソッドの場合、ロックされるのはクラスのClassオブジェクトです。
  • 同期されたコードブロックの場合、括弧内のオブジェクトはロックされます。

同期と非同期の概念について話しましょう。

  • 同期:代替実行。
  • 非同期:同時実行。

たとえば、テレビを食べることと見ることは2つあり、食べた後にテレビを見ることができます。この2つは、同期と呼ばれる時間次元で連続しています。食事とテレビ番組を同時に見ることができます。時間の次元では、順不同で同時に進むことができます。食事が終わった後、テレビを見て学ぶことができます。これは非同期です。利点非同期性の利点は、効率を向上させることができるということです。学習する時間を節約できます。

コードを見てみましょう。コードには詳細なコメントがあり、テストのためにローカルにコピーできます。シンクロナイズドベースの子供靴をお持ちの場合は、ロックの使用に関する説明をスキップできます。

/**
 * @author :jiaolian
 * @date :Created in 2020-12-17 14:48
 * @description:测试静态方法同步和普通方法同步是不同的锁,包括synchronized修饰的静态代码块用法;
 * @modified By:
 * 公众号:叫练
 */
public class SyncTest {

    public static void main(String[] args) {
        Service service = new Service();
        /**
         * 启动下面4个线程,分别测试m1-m4方法。
         */
        Thread threadA = new Thread(() -> Service.m1());
        Thread threadB = new Thread(() -> Service.m2());
        Thread threadC = new Thread(() -> service.m3());
        Thread threadD = new Thread(() -> service.m4());
        threadA.start();
        threadB.start();
        threadC.start();
        threadD.start();

    }

    /**
     * 此案例说明了synchronized修饰的静态方法和普通方法获取的不是同一把锁,因为他们是异步的,相当于是同步执行;
     */
    private static class Service {
        /**
         * m1方法synchronized修饰静态方法,锁表示锁定的是Service.class
         */
        public synchronized static void m1() {
            System.out.println("m1 getlock");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m1 releaselock");
        }

        /**
         * m2方法synchronized修饰静态方法,锁表示锁定的是Service.class
         * 当线程AB同时启动,m1和m2方法是同步的。可以证明m1和m2是同一把锁。
         */
        public synchronized static void m2() {
            System.out.println("m2 getlock");
            System.out.println("m2 releaselock");
        }

        /**
         * m3方法synchronized修饰的普通方法,锁表示锁定的是Service service = new Service();中的service对象;
         */
        public synchronized void m3() {
            System.out.println("m3 getlock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m3 releaselock");
        }

        /**
         * 1.m4方法synchronized修饰的同步代码块,锁表示锁定的是当前对象实例,也就是Service service = new Service();中的service对象;和m3一样,是同一把锁;
         * 2.当线程CD同时启动,m3和m4方法是同步的。可以证明m3和m4是同一把锁。
         * 3.synchronized也可以修饰其他对象,比如synchronized (Service.class),此时m4,m1,m2方法是同步的,启动线程ABD可以证明。
         */
        public void m4() {
            synchronized (this) {
                System.out.println("m4 getlock");
                System.out.println("m4 releaselock");
            }
        }

    }
}

上記のテストの後、質問があるかもしれませんが、ロックは存在するので、どこに保存されますか?回答:オブジェクトの内部。それを証明するためにコードを使用しましょう。

オブジェクトヘッダーにロックされているオブジェクトには、オブジェクトヘッダー、インスタンスデータ、および配置パディングが含まれます。オブジェクトヘッダーには、MarkWordとオブジェクトポインタが含まれます。オブジェクトポインタは、メソッド領域のオブジェクトタイプを指します。インスタンスオブジェクトは属性データです。オブジェクトには多くの属性があり、属性は動的です。アラインメントパディングはバイト数を構成することです。オブジェクトのサイズが8バイトの整数倍でない場合は、残りのバイト数を構成する必要があります。これは、コンピューターが計算するのに便利です。64ビットマシンでは、オブジェクトのオブジェクトヘッダーは通常それ自体のサイズの12を占め、64ビットオペレーティングシステムでは通常4バイトを占めるため、MarkWordは8バイトです。

MarkWordには、オブジェクトハッシュコード、バイアスロックフラグ、スレッドID、およびロックIDが含まれています。オブジェクトヘッダーのコンテンツのテストを容易にするために、mavenopenjdkの依存関係パッケージを導入する必要があります。

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.10</version>
</dependency>
/**
 * @author :duyang
 * @date :Created in 2020-05-14 20:21
 * @description:对象占用内存
 * @modified By:
 *
 *  Fruit对象头是12字节(markword+class)
 *  int 占4个字节
 *
 *  32位机器可能占8个字节;
 *
 *  Object对象头12 对齐填充4 一共是16
 */
public class ObjectMemory {
    public static void main(String[] args) {
        //System.out.print(ClassLayout.parseClass(Fruit.class).toPrintable());
        System.out.print(ClassLayout.parseInstance(Fruit.class).toPrintable());
    }
}

/**
 *Fruit 测试类
 */
public class Fruit {

    //占一个字节大小
    private boolean flag;

}

 

テスト結果:下の赤い線のある3行は、それぞれオブジェクトヘッダー、インスタンスデータ、および配置のパディングを表しています。オブジェクトヘッダーは12バイトで、インスタンスデータのブールフィールドフラグFruitオブジェクトは1バイトのサイズを占め、残りの3バイトは、合計16バイトの配置パディング部分です。

image.png

 

え?あなたが言った錠はどうですか、なぜあなたはそれを見なかったのですか?心配しないでください。後で同期アップグレードと最適化のレベルについて説明するときに、詳細に分析しましょう。まず、JVMレベルで同期の意味を分析しましょう。

image.png

 

最後に、上記の図の要約:

image.png

 

同期されたJVMレベル


/**
 * @author :jiaolian
 * @date :Created in 2020-12-20 13:43
 * @description:锁的jvm层面使用
 * @modified By:
 * 公众号:叫练
 */
public class SyncJvmTest {
    public static void main(String[] args) {
        synchronized (SyncJvmTest.class) {
            System.out.println("jvm同步测试");
        }
    }
}

上記の場合、同期コードブロックに文を出力するだけで、主にjvmでの実装方法を確認します。次の図に示すように、Javap -vSyncJvmTest.classを使用して上記のコードを逆コンパイルします。

image.png

上の図の1行目にはmonitorenterがあり、6行目にはmonitorexitがあります。中央のjvm命令(2〜5行目)は、Javaコードのmainメソッドのコードに対応しています。同期はこれら2つの実装によって異なります。指示。JVM仕様のmonitorenterセマンティクスを見てみましょう

  1. 各オブジェクトにはロックがあります。スレッドが同期コードブロックに入ると、このオブジェクトが保持するモニターオブジェクトのロックを取得します(C ++で実装)。現在のスレッドがロックを取得すると、モニターオブジェクトのエントリ数は次のように増加します。 1。
  2. スレッドが繰り返し入ると、モニターオブジェクトのエントリー番号がもう一度インクリメントされます。
  3. 他のスレッドが入ると、他のスレッドは、ロックを取得するスレッドがモニターオブジェクトのエントリ番号を0に設定してロックを解放するまで待機キューに入れられ、その後、他のスレッドがロックを取得する機会があります。

 

同期された最適化レベル


Synchronizedは、主にスレッド競合ロックによってオペレーティングシステムがユーザーモードとカーネルモードに切り替わり、リソースが浪費されるため、重いロックです。効率は高くありません。jdk1.5より前は、同期は最適化を行いませんでしたが、 jdk1.6でのパフォーマンス。最適化では、バイアスロック、軽量ロック、最後にヘビーウェイトロックのプロセスが実行されます。パフォーマンスが大幅に向上します。jdk1.7のConcurrentHashMapは、ロックを実現するためにReentrantLockに基づいていますが、jdk1では。8以降、同期に置き換えられました。これから、JVMチームは同期のパフォーマンスに非常に自信を持っていることがわかります。以下では、それぞれロックフリー、バイアスロック、軽量ロック、および重量ロックを紹介します。以下に、オブジェクトヘッダー内のこれらのレベルのロックのストレージステータスを説明する図を示します。写真が示すように。

image.png

  • ロックなし。同期キーワードを追加しないと、ロックがないことを意味し、理解しやすいです。
  • バイアスロック。
    • アップグレードプロセス:スレッドが同期ブロックに入ると、MarkwordはバイアスされたスレッドのIDを格納し、casはMarkwordロック状態を01としてマークします。バイアスを使用して電流がバイアスされたロックにあることを示すかどうか(上記を参照)写真)、スレッドの下でバイアスがかかっている場合同期コードを入力すると、マークワードのスレッドIDが現在のスレッドIDと等しいかどうかを比較するだけで済みます。等しい場合は、同期コードの実行を入力せずに入力できます。比較後にそれらが等しくない場合は、ロックを競合する他のスレッドがあり、同期がlightweight.lockにアップグレードされることを意味します。このプロセスでは、オペレーティングシステムレベルでカーネルモードとユーザーモードを切り替える必要がないため、スレッドの切り替えによるリソース消費が削減されます。
    • 拡張プロセス:別のスレッドが入ると、バイアスロックが軽量ロックにアップグレードされます。たとえば、スレッドAはバイアスロックです。スレッドBが入ると、軽量ロックになります。スレッドが2つある限り、軽量ロックにアップグレードされます

以下のコードでバイアスロックのロック状態を見てみましょう。

package com.duyang.base.basic.markword;

import lombok.SneakyThrows;
import org.openjdk.jol.info.ClassLayout;

/**
 * @author :jiaolian
 * @date :Created in 2020-12-19 11:25
 * @description:markword测试
 * @modified By:
 * 公众号:叫练
 */
public class MarkWordTest {

    private static Fruit fruit = new Fruit();

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread threadA = new Thread(task);
        Thread threadB = new Thread(task);
        Thread threadC = new Thread(task);
        threadA.start();
        //threadA.join();
        //threadB.start();
        //threadC.start();
    }

    private static class Task extends Thread {

        @SneakyThrows
        @Override
        public void run() {
            synchronized (fruit) {
                System.out.println("==================="+Thread.currentThread().getId()+" ");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(ClassLayout.parseInstance(fruit).toPrintable());
            }
        }
    }
}

上記のコードはスレッドAを開始し、コンソール出力は次の図のようになります。赤でマークされた3ビットはそれぞれ101で、上位1はバイアスロックを意味し、01はバイアスロック識別ビットです。バイアスされたロック識別の状況に準拠します。

image.png

  • 軽量ロック。
    • アップグレードプロセス:スレッドが実行されてロックを取得した後、スレッドはスタックフレームにロックレコードを作成し、MarkWordをロックレコードにコピーしてから、MarkWordをロックレコードにポイントします。現在のスレッドがロックを保持している場合、他のスレッドが再び入り、ロックが取得されるまで他のスレッドがcasスピンします。軽量ロックはマルチスレッドの代替実行に適しており、非常に効率的です(casはcpuのみを消費します。これについては、casの原則に関する記事で詳しく説明しました)。 。
    • 拡張プロセス:重量級のロックに拡張する2つの状況があります。1つのケースは、casが10回回転し、ロックを取得していない場合です。2番目のケースでは、他のスレッドがcasでロックを取得しており、3番目のスレッドがロックを取得するために競合し、ロックが拡張されてヘビーウェイトロックになります。

以下に、軽量ロックのロック状態をテストするためのコードを示します。

23行から24行のコードを開き、スレッドA、Bを実行します。私の目的は、スレッドABを順番に実行することなので、最初にコードでthreadA.join()を実行し、スレッドAに実行を終了させて​​から、スレッドBを実行します。以下に示すようにMarkWordのロック状態の変化に示すように、スレッドAは最初に101で示されるロックによって逸脱し、実行スレッドBは軽量ロックになり、ロック状態は00になり、軽量ロック状態に準拠します。証明が完了しました。

image.png

  • ヘビーウェイトロックアップグレードされたヘビーウェイトロックは元に戻せません。つまり、ヘビーウェイトロックは軽量ロックにはなりません。

25行のコードを開き、スレッドA、B、Cを実行します。私の目的は、最初にスレッドAを実行し、コードでthreadA.join()を実行し、スレッドAに最初に実行を終了させて​​から、同時にスレッドBCを実行することです。下図のようにMarkWordのロック状態の変化を示します。スレッドAは最初にロックするようにバイアスされ、同時にスレッドBCを実行します。激しい競争のため、軽量ロック拡張の2番目のケースに属します。他のスレッドがcasでロックを取得している場合、ロックを競合する3番目のスレッドが拡張されてヘビーウェイトロックになります。このとき、BCネジロック状態は10になり、ヘビーウェイトロック状態と一致しています。重量級ロックの拡張が証明されています。

image.png

 

 

これまでのところ、同期ロックアップグレードプロセス中のロックステータスをコードの形式で証明しており、それがお役に立てば幸いです。下の図は私自身の要約です。

image.png

 

総括する


マルチスレッド同期は常に非常に重要なトピックであり、インタビューの一般的なテストポイントでもあります。皆さんができるだけ早くそれを理解して習得し、あなたと共有し、そしてあなたがそれを好きになることを願っています!

私はリアンと呼ばれています。もっとリアンに電話してください。みんなが私と話し合ったり交換したりすることを歓迎します。好きでフォローしたいので、できるだけ早く返信します。パブリックアカウント[CallingLian]。

 

Awesome.gif

おすすめ

転載: blog.csdn.net/duyabc/article/details/111468664