[Collector's Edition]80Javaマルチスレッド並行性クラシックインタビューの質問

序文

スペースが長すぎるため、80個のJavaマルチスレッド/並行性の古典的なインタビューの質問の個人的なコレクションです。ここで、1〜10の回答を提供して分析し、後で一緒に改善して、githubにアップロードします〜

github.com/whx123/Java…❞

1.同期およびロック最適化の実装原則?

同期の実現原理

  • Synchronizedは「メソッド」または「コードのブロック」に作用し、変更されたコードに一度に1つのスレッドのみがアクセスできるようにします。
  • 同期によってコードブロックが変更されると、JVMは「monitorenter、monitorexit」という2つの命令を使用して同期を実現します。
  • 同期が同期方法を変更する場合、JVMは「ACC_SYNCHRONIZED」タグを使用して同期を実現します
  • monitorenter、monitorexit、またはACC_SYNCHRONIZEDは、すべて「モニターベース」の実装です。
  • インスタンスオブジェクトにはオブジェクトヘッダーがあり、オブジェクトヘッダーにはMark Wordがあり、MarkWordポインターは「モニター」を指しています。
  • モニターは実際には「同期ツール」であり、 「同期メカニズム」とも言えます
  • Java仮想マシン(HotSpot)では、Monitorは「ObjectMonitor」によって実装されます。ObjectMonitorは、Monitor〜の動作原理を反映しています。
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0;  //锁的重入次数
    _object       = NULL;
    _owner        = NULL;  // 指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
复制代码

ObjectMonitor _count、_recursions、_owner、_WaitSet、_EntryListのいくつかの主要な属性は、モニターの動作原理を反映しています。

ロックの最適化

ロックの最適化について説明する前に、JAVAオブジェクトヘッダー(32ビットJVM)のMarkWordの構造図を見てみましょう。

Mark Wordは、 「ハッシュコード、GC生成年齢、ロックステータスフラグ、バイアスタイムスタンプ(Epoch)」などのオブジェクト自体の動作データを保存します。なぜ「バイアスロック、軽量ロック、重量ロック」などを区別するのですか。ロック状態の種類について?

JDK1.6より前は、同期の実装はObjectMonitorの開始と終了を直接呼び出し、この種のロックは「ヘビーウェイトロック」と呼ばれていました。JDK6以降、HotSpot仮想マシン開発チームは、アダプティブスピン、ロック除去、ロック粗大化、軽量ロック、バイアスロックなどの最適化戦略を追加するなど、Javaでロックを最適化しました。

  • バイアスロック:競合がない場合、同期全体が排除され、CAS操作は実行されません。
  • 軽量ロック:マルチスレッドの競合がない場合、比較的重いロックであり、オペレーティングシステムのミューテックスによって引き起こされるパフォーマンスの消費を削減します。ただし、ロックの競合がある場合は、ミューテックス自体のオーバーヘッドに加えて、CAS操作のオーバーヘッドもあります。
  • スピンロック:不要なCPUコンテキストスイッチを減らします。軽量ロックを重量ロックにアップグレードする場合は、スピンロック方式が使用されます
  • ロックの粗大化:複数の連続するロック操作とロック解除操作を接続して、より大きなロックに拡張します。

たとえば、動物園に入るためのチケットを購入します。先生は子供たちのグループを訪問します。チケット検査官は、子供たちがグループであることを知っている場合、チケットを1つずつ確認するのではなく、子供たち全体(ロックされた家賃)と見なして一度にチケットを確認できます。 。

  • ロックの除去:仮想マシンのジャストインタイムコンパイラが実行されている場合、一部のコードを同期する必要がありますが、共有データの競合の可能性がないことが検出されます。

興味のある友人は私の記事を読むことができます:同期分析-あなたが私の心を層ごとに剥がしてくれるなら[1]

2. ThreadLocalの原則、使用上の注意、およびアプリケーションのシナリオは何ですか?

4つの主要なポイントに答えてください:

  • ThreadLocalとは何ですか?
  • ThreadLocalの原則
  • ThreadLocalの使用に関する注意
  • ThreadLocalアプリケーションのシナリオ

ThreadLocalとは何ですか?

ThreadLocal、スレッドローカル変数。ThreadLocal変数を作成すると、この変数にアクセスする各スレッドは変数のローカルコピーを持ちます。複数のスレッドがこの変数を操作する場合、実際には独自のローカルメモリで変数を操作するため、スレッドの分離が実現します。スレッドの安全性を回避する機能問題。

//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();
复制代码

ThreadLocalの原則

ThreadLocalメモリ構造図:

構造図からわかります。

  • Threadオブジェクトは、ThreadLocal.ThreadLocalMapのメンバー変数を保持します。
  • ThreadLocalMapは、エントリの配列を内部的に維持します。各エントリは完全なオブジェクトを表し、キーはThreadLocal自体であり、値はThreadLocalの総称値です。

これらの主要なソースコードを比較することで理解しやすくなります〜

public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;
}
复制代码

ThreadLocalの主要なメソッドset()とget()

    public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程t
        ThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMap
        if (map != null)
            map.set(this, value); //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //创建一个新的ThreadLocalMap
    }

    public T get() {
        Thread t = Thread.currentThread();//获取当前线程t
        ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
        if (map != null) {
            //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value; 
                return result;
            }
        }
        return setInitialValue();
    }
复制代码

ThreadLocalMapのエントリ配列

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}
复制代码

では、「ThreadLocalの実装原理」にどのように答えればよいのでしょうか。以下のように、上記の構造図を組み合わせて説明するのが最善です〜

❝Threadクラスには、ThreadLocal.ThreadLocalMap型のインスタンス変数threadLocalsがあります。つまり、各スレッドには独自のThreadLocalMapがあります。ThreadLocalMapはEntryの配列を維持し、各Entryは完全なオブジェクトを表し、キーはThreadLocal自体であり、値はThe ThreadLocalの総称値。各スレッドがThreadLocalに値を設定すると、それを独自のThreadLocalMapに格納し、読み取り時にThreadLocalを参照として使用し、独自のマップで対応するキーを見つけて、スレッド検疫を実現します。 ❞

ThreadLocalメモリリークの問題

TreadLocalの参照図を見てみましょう。

ThreadLocalMapで使用されるキーは、次のようにThreadLocalの弱参照です。

弱参照:ガベージコレクションメカニズムが実行されている限り、JVMのメモリスペースが十分であるかどうかに関係なく、オブジェクトが占有していたメモリが再利用されます。

弱参照はリサイクルが容易です。したがって、ThreadLocal(ThreadLocalMapのキー)がガベージコレクターによってリサイクルされるが、ThreadLocalMapのライフサイクルはThreadのライフサイクルと同じであるため、この時点でリサイクルされない場合、これが発生します。ThreadLocalMapのキーはなくなります。 、そして値はまだそこにあります。これで、これは「メモリリークの問題を引き起こします」

「メモリリークの問題を解決する」方法はThreadLocalを使用した後、適切なタイミングでremove()メソッドを呼び出して、メモリスペースを解放します。

ThreadLocalアプリケーションのシナリオ

  • データベース接続プール
  • セッション管理で使用

3.同期とReentrantLockの違いは何ですか?

学校が募集していたとき、この面接の質問の頻度がかなり高かったのを覚えています〜この質問には、ロックの実装、機能特性、パフォーマンスなどのいくつかの側面から答えることができます。

  • 「ロックの実装:」 SynchronizedはJava言語のキーワードであり、JVMに基づいて実装されます。ReentrantLockは、JDKのAPIレベルに基づいて実装されます(通常、lock()メソッドとunlock()メソッドはtry / finalステートメントブロックで完了します)。
  • 「パフォーマンス:」 JDK1.6ロックの最適化以前は、同期のパフォーマンスはReenTrantLockのパフォーマンスよりもはるかに劣っていました。ただし、JDK6以降、アダプティブスピン、ロック除去などが追加されており、2つのパフォーマンスは類似しています。
  • 「機能:」 ReentrantLockは、割り込み可能、​​フェアロック、選択的通知の待機など、同期よりも高度な機能を追加します。

❝ReentrantLockは、ロックを待機しているスレッドを中断できるメカニズムを提供します。このメカニズムは、lock.lockInterruptibly()を介して実装されます。ReentrantLockは、それがフェアロックかアンフェアロックかを指定できます。フェアロックと呼ばれるのは、最初に待機するスレッドが最初にロックを取得することです。同期は、wait()およびnotify()/ notifyAll()メソッドと組み合わされて、待機/通知メカニズムを実装します。ReentrantLockクラスは、ConditionインターフェイスとnewCondition()メソッド。ReentrantLockを手動で宣言してロックおよび解放する必要があります。ロックは通常、finallyと組み合わせて解放されます。ただし、同期では手動でロックを解放する必要はありません。❞

4.CountDownLatchとCyclicBarrierの違いについて話します

  • CountDownLatch:実行する前に他のスレッドが何かを完了するのを待っている1つ以上のスレッド。
  • CyclicBarrier:複数のスレッドは、同じ同期ポイントに到達するまで互いに待機し、その後、一緒に実行を続けます。

例えば:

❝CountDownLatch:教師とクラスメートが週末に公園のゲートで会い、全員の準備ができたらチケットを発行することに同意したとします。次に、チケット(このメインスレッド)を発行するには、すべてのクラスメートは実行前に到着します(他のすべてのスレッドは完了します)。この時点で、アスリートはギャロッピングします。❞

5.フォーク/結合フレームワークの理解

Fork / Joinフレームワークは、Java7が提供するタスクを並行して実行するためのフレームワークであり、大きなタスクをいくつかの小さなタスクに分割し、最後に各小さなタスクの結果を要約して大きなタスクの結果を取得するフレームワークです。

Fork / Joinフレームワークは、「分割統治」「作業盗用アルゴリズム」の2つのポイントを理解する必要があります。

"分割統治"

上記のフォーク/結合フレームワークの定義は、分割統治のアイデアの具体化です。

「ワークスティーリングアルゴリズム」

大きなタスクを小さなタスクに分割し、実行のためにそれらを異なるキューに入れ、実行のために異なるスレッドに渡します。一部のスレッドは最初に担当するタスクを完了し、他のスレッドはまだゆっくりと独自のタスクを処理しています。現時点では、効率を完全に向上させるために、作業盗難アルゴリズムが必要です〜

ワークスティーリングアルゴリズムは、「スレッドが実行のために他のキューからタスクを盗むプロセス」です。一般的には、高速スレッド(盗難スレッド)が低速スレッドのタスクを取得することを意味します。同時に、ロックの競合を減らすために、通常、高速スレッドと低速スレッドの両端キューが使用されます。スレッドは一方の端にあります。

6. start()メソッドを呼び出すとrun()メソッドが実行されるのはなぜですか。また、run()メソッドを直接呼び出せないのはなぜですか。

Thread〜のstartメソッドの説明を見てください。

    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {
     ......
    }
复制代码

JVMがstartメソッドを実行すると、新しいスレッドを開始してスレッドのrunメソッドを実行します。これには、マルチスレッドの効果があります〜 「run()メソッドを直接呼び出せないのはなぜですか?」スレッドのrun()メソッド、メソッドはまだメインスレッドで実行されており、マルチスレッド効果はありません。

7. CAS?CASの何が問題になっていて、どのように修正するのですか?

CAS、コンペアアンドスワップ、コンペアアンドエクスチェンジ;

CASには、メモリアドレス値V、期待される元の値A、および新しい値Bの3つのオペランドが含まれます。メモリ位置の値Vが期待される元のA値と一致する場合は、新しい値Bに更新されます。それ以外の場合は、次のようになります。更新されていない

CASの何が問題になっていますか?

「ABA問題」

並行環境では、初期条件がAであると仮定して、データを変更するときに、Aが変更を実行することがわかります。ただし、Aですが、AがBに変わり、BがAに戻る場合があります。このとき、AはAではなくなり、データが正常に変更されたとしても、問題が発生する可能性があります。

ABA問題は、AtomicStampedReferenceによって「解決」できます。これは、タグ付きのアトミック参照クラスであり、変数値のバージョン管理を制御することにより、CASの正確性を保証します。

「長いサイクル時間のオーバーヘッド」

スピンCASがループで実行された場合、失敗すると、CPUに非常に大きな実行オーバーヘッドが発生します。

多くの場合、CASのアイデアは、この時間のかかる問題を回避するために、多数のスピンがあることを反映しています〜

「1つの変数に対する不可分操作のみが保証されています。」

CASは、1つの変数で実行される操作の原子性を保証します。現在、CASは、複数の変数で操作する場合の操作の原子性を直接保証することはできません。

この問題は、次の2つの方法で解決できます。

❝mutexを使用して原子性を確保し、複数の変数をオブジェクトにカプセル化し、AtomicReferenceを使用して原子性を確保します。❞

興味のある友人は私の以前の実用的な記事を見ることができますha〜並行性の問題を解決するためのCAS楽観的ロックの実践[2]

9.マルチスレッドでi++の正しい結果を保証するにはどうすればよいですか?

  • サイクリックCASを使用してi++アトミック操作を実現
  • ロックメカニズムを使用してi++アトミック操作を実装する
  • 同期を使用してi++アトミック操作を実装する

コードデモがないと、次のように魂がないように感じます。

/**
 *  @
 */
public class AtomicIntegerTest {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        testIAdd();
    }

    private static void testIAdd() throws InterruptedException {
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(() -> {
                for (int j = 0; j < 2; j++) {
                    //自增并返回当前值
                    int andIncrement = atomicInteger.incrementAndGet();
                    System.out.println("线程:" + Thread.currentThread().getName() + " count=" + andIncrement);
                }
            });
        }
        executorService.shutdown();
        Thread.sleep(100);
        System.out.println("最终结果是 :" + atomicInteger.get());
    }
    
}
复制代码

演算結果:

...
线程:pool-1-thread-1 count=1997
线程:pool-1-thread-1 count=1998
线程:pool-1-thread-1 count=1999
线程:pool-1-thread-2 count=315
线程:pool-1-thread-2 count=2000
最终结果是 :2000
复制代码

10.デッドロックを検出する方法は?デッドロックを防ぐ方法は?デッドロックに必要な4つの条件

デッドロックとは、競合するリソースが原因で複数のスレッドが互いに待機するデッドロックを指します。このように感じてください:

「デッドロックに必要な4つの条件:」

  • 相互排除:一度に1つのプロセスのみがリソースを使用できます。他のプロセスは、他のプロセスに割り当てられているリソースにアクセスできません。
  • 占有して待機:プロセスが他のリソースが割り当てられるのを待機しているとき、プロセスは割り当てられたリソースを占有し続けます。
  • 非プリエンプティブ:プロセスによってすでに占有されているリソースを強制的にプリエンプションすることはできません。
  • 循環待機:各リソースがチェーン内の次のプロセスに必要な少なくとも1つのリソースを占有するように、閉じたプロセスチェーンがあります。

「デッドロックを防ぐ方法は?」

  • 順序のロック(スレッドは順番に機能します)
  • ロック時間制限(スレッド要求によって追加されたアクセス許可、タイムアウトは破棄され、それ自体が占有していたロックは同時に解放されます)
  • デッドロックの検出

参考と感謝

ニュートンは、私が遠くを見る理由は、私が巨人の肩の上に立っているからだと言いました〜ありがとう、次の先輩〜

  • 面接で聞かなければならないCASを理解していますか?[3]
  • Javaマルチスレッド:デッドロック[4]
  • ReenTrantLockリエントラントロックの概要(同期との違い)[5]
  • 並行性について話す(8)-Fork/Joinフレームワークの概要[6]


著者:カタツムリを拾う小さな男の子
リンク:https://juejin.cn/post/6854573221258199048
出典:希土類ナゲット
著作権は著者に帰属します。商用の再版については、著者に連絡して許可を求め、非商用の再版については、出典を示してください。

おすすめ

転載: blog.csdn.net/wdjnb/article/details/124323297