Java並行処理インタビューの質問(回答)

Java同時実行性インタビューの質問(回答)

1. 前書き

Javaの同時実行は、技術面接で取り上げられた最も複雑で高度なトピックの1つです。 この記事では、発生する可能性のあるトピックに関するいくつかのインタビューの質問に対する回答を提供します。

Q1. プロセスとスレッドの違いは何ですか?

プロセスとスレッドは同時実行の単位ですが、根本的な違いがあります。プロセスは共通のメモリを共有しませんが、スレッドは共有します。

オペレーティングシステムの観点からは、プロセスは、独自の仮想メモリ空​​間で実行される独立したソフトウェアです。 マルチタスクオペレーティングシステム(つまり、ほとんどすべての最新のオペレーティングシステム)は、メモリ内のプロセスを分離する必要があります。これにより、1つの失敗したプロセスが、共通のメモリをスクランブルして他のすべてのプロセスを引き下げないようにします。

したがって、プロセスは通常分離されており、オペレーティングシステムによって一種の中間APIとして定義されているプロセス間通信によって連携します。

それどころか、スレッドは、同じアプリケーションの他のスレッドと共通のメモリを共有するアプリケーションの一部です。 共通メモリを使用すると、多くのオーバーヘッドを削減し、協調するようにスレッドを設計し、スレッド間でデータをより速く交換できます。

Q2. どうすればスレッドインスタンスを作成して実行できますか?

スレッドのインスタンスを作成するには、2つのオプションがあります。 まず、Runnableインスタンスをコンストラクターに渡し、start()を呼び出します。 Runnableは関数型インターフェースであるため、ラムダ式として渡すことができます。

Thread thread1 = new Thread(() ->
  System.out.println("Hello World from Runnable!"));
thread1.start();

スレッドはRunnableも実装しているため、スレッドを開始する別の方法は、匿名サブクラスを作成し、そのrun()メソッドをオーバーライドしてから、start()を呼び出すことです。

Thread thread2 = new Thread() {
    @Override
    public void run() {
        System.out.println("Hello World from subclass!");
    }
};
thread2.start();

Q3. スレッドのさまざまな状態と、状態遷移がいつ発生するかを説明します。

Threadの状態は、Thread.getState()メソッドを使用して確認できます。 Threadのさまざまな状態は、Thread.State列挙型に記述されています。 彼らです:

  • NEWThread.start()を介してまだ開始されていない新しいThreadインスタンス

  • RUNNABLE —実行中のスレッド。 いつでも実行可能であるか、スレッドスケジューラからの次のクォンタムを待機している可能性があるため、実行可能と呼ばれます。 Thread.start()を呼び出すと、NEWスレッドはRUNNABLE状態になります。

  • BLOCKED —同期されたセクションに入る必要があるが、このセクションのモニターを保持している別のスレッドのためにそれを行うことができない場合、実行中のスレッドはブロックされます

  • WAITING —別のスレッドが特定のアクションを実行するのを待つ場合、スレッドはこの状態になります。 たとえば、スレッドは、保持しているモニターでObject.wait()メソッドを呼び出すか、別のスレッドでThread.join()メソッドを呼び出すと、この状態になります。

  • TIMED_WAITING —上記と同じですが、スレッドはThread.sleep()Object.wait()Thread.join()およびその他のメソッドの時限バージョンを呼び出した後にこの状態になります

  • TERMINATED —スレッドはRunnable.run()メソッドの実行を完了し、終了しました

Q4. RunnableインターフェイスとCallableインターフェイスの違いは何ですか? それらはどのように使用されますか?

Runnableインターフェースには、単一のrunメソッドがあります。 別のスレッドで実行する必要がある計算の単位を表します。 Runnableインターフェイスでは、このメソッドが値を返したり、チェックされていない例外をスローしたりすることはできません。

Callableインターフェイスには単一のcallメソッドがあり、値を持つタスクを表します。 そのため、callメソッドは値を返します。 例外をスローすることもできます。 Callableは通常、ExecutorServiceインスタンスで非同期タスクを開始し、返されたFutureインスタンスを呼び出してその値を取得するために使用されます。

Q5. デーモンスレッドとは何ですか、その使用例は何ですか? デーモンスレッドを作成するにはどうすればよいですか?

デーモンスレッドは、JVMの終了を妨げないスレッドです。 デーモン以外のスレッドがすべて終了すると、JVMは残りのすべてのデーモンスレッドを単に破棄します。 通常、デーモンスレッドは、他のスレッドのサポートタスクまたはサービスタスクを実行するために使用されますが、いつでも破棄される可能性があることを考慮する必要があります。

スレッドをデーモンとして開始するには、start()を呼び出す前にsetDaemon()メソッドを使用する必要があります。

Thread daemon = new Thread(()
  -> System.out.println("Hello from daemon!"));
daemon.setDaemon(true);
daemon.start();

不思議なことに、これをmain()メソッドの一部として実行すると、メッセージが出力されない場合があります。 これは、デーモンがメッセージを出力するポイントに到達する前にmain()スレッドが終了する場合に発生する可能性があります。 デーモンスレッドは、放棄された場合にfinallyブロックを実行してリソースを閉じることさえできないため、通常、デーモンスレッドでI / Oを実行しないでください。

Q6. スレッドの割り込みフラグとは何ですか? どのように設定して確認できますか? 中断された例外とどのように関連していますか?

割り込みフラグ、または割り込みステータスは、スレッドが中断されたときに設定される内部Threadフラグです。 設定するには、スレッドオブジェクト.thread.interrupt()を呼び出すだけです。

スレッドが現在InterruptedExceptionwaitjoinsleepなど)をスローするメソッドの1つにある場合、このメソッドはすぐにInterruptedExceptionをスローします。 スレッドは、独自のロジックに従ってこの例外を自由に処理できます。

スレッドがそのようなメソッド内になく、thread.interrupt()が呼び出された場合、特別なことは何も起こりません。 static Thread.interrupted()またはインスタンスisInterrupted()メソッドを使用して、割り込みステータスを定期的にチェックするのはスレッドの責任です。 これらの方法の違いは、static Thread.interrupt()は割り込みフラグをクリアしますが、isInterrupted()はクリアしないことです。

Q7. ExecutorおよびExecutorserviceとは何ですか? これらのインターフェースの違いは何ですか?

ExecutorExecutorServiceは、java.util.concurrentフレームワークの2つの関連するインターフェースです。 Executorは、実行用にRunnableインスタンスを受け入れる単一のexecuteメソッドを備えた非常に単純なインターフェースです。 ほとんどの場合、これはタスク実行コードが依存すべきインターフェイスです。

ExecutorServiceは、並行タスク実行サービス(シャットダウンの場合のタスクの終了)のライフサイクルを処理およびチェックするための複数のメソッドと、Futuresを含むより複雑な非同期タスク処理のためのメソッドでExecutorインターフェースを拡張します。 s。

ExecutorおよびExecutorServiceの使用の詳細については、記事A Guide to Java ExecutorServiceを参照してください。

Q8. 標準ライブラリで利用可能なExecutorserviceの実装は何ですか?

ExecutorServiceインターフェースには、次の3つの標準実装があります。

  • ThreadPoolExecutor —スレッドのプールを使用してタスクを実行するため。 スレッドがタスクの実行を終了すると、プールに戻ります。 プール内のすべてのスレッドがビジーの場合、タスクは順番を待つ必要があります。

  • ScheduledThreadPoolExecutorを使用すると、スレッドが使用可能になったときにすぐに実行するのではなく、タスクの実行をスケジュールできます。 また、固定レートまたは固定遅延でタスクをスケジュールできます。

  • ForkJoinPoolは、再帰的アルゴリズムタスクを処理するための特別なExecutorServiceです。 再帰的アルゴリズムに通常のThreadPoolExecutorを使用すると、すべてのスレッドが低レベルの再帰が終了するのを待ってビジーであることがすぐにわかります。 ForkJoinPoolは、使用可能なスレッドをより効率的に使用できるようにする、いわゆるワークスティーリングアルゴリズムを実装します。

Q9. Javaメモリモデル(Jmm)とは その目的と基本的な考え方を説明してください。

Javaメモリモデルは、Chapter 17.4で説明されているJava言語仕様の一部です。 複数のスレッドが並行Javaアプリケーションの共通メモリにアクセスする方法、および1つのスレッドによるデータの変更が他のスレッドから見えるようにする方法を指定します。 JMMは非常に短く簡潔ですが、数学的背景がなければ理解が難しい場合があります。

メモリモデルの必要性は、Javaコードがデータにアクセスする方法が、下位レベルで実際に発生する方法ではないという事実から生じます。 メモリの書き込みと読み取りは、これらの読み取りと書き込みの観察可能な結果が同じである限り、Javaコンパイラ、JITコンパイラ、さらにはCPUによって再配列または最適化される場合があります。

これらの最適化のほとんどは実行の単一スレッドを考慮に入れるため、アプリケーションが複数のスレッドにスケーリングされる場合、これは直感に反する結果につながる可能性があります(クロススレッドオプティマイザーの実装は依然として非常に困難です)。 別の大きな問題は、現代のシステムのメモリが多層化されていることです:プロセッサの複数のコアは、キャッシュまたは読み取り/書き込みバッファにフラッシュされていないデータを保持する可能性があり、他のコアから観察されるメモリの状態にも影響します。

さらに悪いことに、さまざまなメモリアクセスアーキテクチャが存在すると、Javaの「一度書けばどこでも実行できる」という約束が破られます。 プログラマにとって幸いなことに、JMMは、マルチスレッドアプリケーションを設計する際に信頼できるいくつかの保証を指定しています。 これらの保証に固執することで、プログラマーはさまざまなアーキテクチャ間で安定して移植可能なマルチスレッドコードを書くことができます。

JMMの主な概念は次のとおりです。

  • Actions、これらは、変数の読み取りまたは書き込み、モニターのロック/ロック解除など、1つのスレッドで実行され、別のスレッドで検出できるスレッド間アクションです。

  • Synchronization actionsvolatile変数の読み取り/書き込み、モニターのロック/ロック解除などのアクションの特定のサブセット

  • Program Order(PO)、単一スレッド内の観察可能なアクションの全順序

  • Synchronization Order(SO)、すべての同期アクション間の全順序—プログラムの順序と一致している必要があります。つまり、POで2つの同期アクションが次々に発生する場合、SOでは同じ順序で発生します。

  • モニターのロック解除や同じモニターのロック(別のスレッドまたは同じスレッド内)など、特定の同期アクション間のsynchronizes-with(SW)関係

  • Happens-before Order — POとSW(集合論ではtransitive closureと呼ばれます)を組み合わせて、スレッド間のすべてのアクションの半順序を作成します。 あるアクションが別のアクションhappens-beforeである場合、最初のアクションの結果は2番目のアクションで観察できます(たとえば、あるスレッドで変数を書き込み、別のスレッドで読み取る)。

  • Happens-before consistency —すべての読み取りで、発生前の順序でその場所への最後の書き込み、またはデータ競合を介したその他の書き込みが観察された場合、一連のアクションはHB整合性があります

  • Execution —順序付けられたアクションの特定のセットとそれらの間の整合性ルール

特定のプログラムについて、さまざまな結果を持つ複数の異なる実行を観察できます。 ただし、プログラムがcorrectly synchronizedの場合、その実行はすべてsequentially consistentであるように見えます。つまり、マルチスレッドプログラムを、ある順序で発生する一連のアクションとして推論できます。 これにより、内部の並べ替え、最適化、データキャッシュについて考える手間が省けます。

Q10. 揮発性フィールドとは何ですか?また、Jmmはそのようなフィールドに対してどのような保証をしますか?

volatileフィールドには、Javaメモリモデルに応じた特別なプロパティがあります(Q9を参照)。 volatile変数の読み取りと書き込みは同期アクションです。つまり、全体的な順序があります(すべてのスレッドがこれらのアクションの一貫した順序を監視します)。 揮発性変数の読み取りは、この順序に従って、この変数への最後の書き込みを監視することが保証されています。

複数のスレッドからアクセスされ、少なくとも1つのスレッドが書き込まれているフィールドがある場合は、それをvolatileにすることを検討する必要があります。そうしないと、特定のスレッドがこのフィールドから読み取る内容が少し保証されます。

volatileのもう1つの保証は、64ビット値(longおよびdouble)の書き込みと読み取りのアトミック性です。 volatile修飾子がなければ、そのようなフィールドの読み取りは、別のスレッドによって部分的に書き込まれた値を監視できます。

Q11. 次の操作のどれがアトミックですか?

  • volatile以外のintへの書き込み;

  • volatile intへの書き込み;

  • volatile long以外への書き込み;

  • volatile longへの書き込み;

  • volatile longをインクリメントしますか?

int(32ビット)変数への書き込みは、volatileであるかどうかに関係なく、アトミックであることが保証されています。 long(64ビット)変数は、たとえば32ビットアーキテクチャでは、2つの別々のステップで書き込むことができるため、デフォルトでは、原子性の保証はありません。 ただし、volatile修飾子を指定すると、long変数はアトミックにアクセスされることが保証されます。

インクリメント操作は通常、複数のステップ(値の取得、変更、書き戻し)で実行されるため、変数がvolatileであるかどうかに関係なく、アトミックであることが保証されることはありません。 値のアトミックインクリメントを実装する必要がある場合は、クラスAtomicIntegerAtomicLongなどを使用する必要があります。

Q12. Jmmはクラスの最終フィールドに対してどのような特別な保証を保持しますか?

JVMは基本的に、スレッドがオブジェクトを保持する前に、クラスのfinalフィールドが初期化されることを保証します。 この保証がなければ、オブジェクトへの参照が公開される可能性があります。 並べ替えやその他の最適化により、このオブジェクトのすべてのフィールドが初期化される前に、別のスレッドから見えるようになります。 これにより、これらのフィールドに迅速にアクセスできます。

これが、不変オブジェクトを作成するときに、getterメソッドを介してアクセスできない場合でも、常にすべてのフィールドをfinalにする必要がある理由です。

Q13. メソッドの定義における同期キーワードの意味は何ですか? 静的メソッドの ブロックの前に?

ブロックの前のsynchronizedキーワードは、このブロックに入るすべてのスレッドがモニター(括弧内のオブジェクト)を取得する必要があることを意味します。 モニターがすでに別のスレッドによって取得されている場合、前のスレッドはBLOCKED状態に入り、モニターが解放されるまで待機します。

synchronized(object) {
    // ...
}

synchronizedインスタンスメソッドのセマンティクスは同じですが、インスタンス自体がモニターとして機能します。

synchronized void instanceMethod() {
    // ...
}

static synchronizedメソッドの場合、モニターは宣言クラスを表すClassオブジェクトです。

static synchronized void staticMethod() {
    // ...
}

Q14. 2つのスレッドが異なるオブジェクトインスタンスの同期メソッドを同時に呼び出す場合、これらのスレッドの1つがブロックされる可能性はありますか? メソッドが静的な場合はどうなりますか?

メソッドがインスタンスメソッドの場合、インスタンスはメソッドのモニターとして機能します。 異なるインスタンスでメソッドを呼び出す2つのスレッドは異なるモニターを取得するため、ブロックされることはありません。

メソッドがstaticの場合、モニターはClassオブジェクトです。 両方のスレッドで、モニターは同じであるため、一方がブロックして、もう一方がsynchronizedメソッドを終了するのを待つ可能性があります。

Q15. オブジェクトクラスのWait、Notify、Notifyallメソッドの目的は何ですか?

オブジェクトのモニターを所有するスレッド(たとえば、オブジェクトによって保護されているsynchronizedセクションに入ったスレッド)は、object.wait()を呼び出してモニターを一時的に解放し、他のスレッドにモニターを取得する機会を与えることができます。 。 これは、たとえば、特定の条件を待つために行われます。

モニターを取得した別のスレッドが条件を満たすと、object.notify()またはobject.notifyAll()を呼び出して、モニターを解放する場合があります。 notifyメソッドは待機状態で単一のスレッドを起動し、notifyAllメソッドはこのモニターを待機するすべてのスレッドを起動し、それらはすべてロックの再取得を競います。

次のBlockingQueue実装は、複数のスレッドがwait-notifyパターンを介してどのように連携するかを示しています。 要素を空のキューにputすると、takeメソッドで待機していたすべてのスレッドがウェイクアップし、値を受け取ろうとします。 要素を完全なキューにputすると、getメソッドの呼び出しに対してputメソッドwaitsが実行されます。 getメソッドは要素を削除し、putメソッドで待機しているスレッドに、キューに新しいアイテム用の空の場所があることを通知します。

public class BlockingQueue {

    private List queue = new LinkedList();

    private int limit = 10;

    public synchronized void put(T item) {
        while (queue.size() == limit) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        if (queue.isEmpty()) {
            notifyAll();
        }
        queue.add(item);
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        if (queue.size() == limit) {
            notifyAll();
        }
        return queue.remove(0);
    }

}

Q16. デッドロック、ライブロック、および飢vの状態を説明してください。 これらの状態の考えられる原因を説明してください。

Deadlockは、グループ内のすべてのスレッドが、グループ内の別のスレッドによって既に取得されているリソースを取得する必要があるため、進行できないスレッドのグループ内の条件です。 最も単純なケースは、2つのスレッドが2つのリソースの両方をロックして進行する必要がある場合で、最初のリソースはすでに1つのスレッドによってロックされ、2番目のリソースは別のスレッドによってロックされています。 これらのスレッドは、両方のリソースへのロックを取得しないため、進行しません。

Livelockは、複数のスレッドがそれ自体によって生成された条件またはイベントに反応する場合です。 1つのスレッドでイベントが発生し、別のスレッドで処理する必要があります。 この処理中に、最初のスレッドで処理する必要のある新しいイベントが発生します。 そのようなスレッドは生きており、ブロックされていませんが、それでも役に立たない作業でお互いを圧倒しているため、進行しません。

Starvationは、他のスレッド(または複数のスレッド)がリソースを占有する時間が長すぎるか、優先度が高いために、リソースを取得できないスレッドの場合です。 スレッドは進行できないため、有用な作業を実行できません。

Q17. Fork / JoinFrameworkの目的と使用例を説明します。

fork / joinフレームワークにより、再帰アルゴリズムを並列化できます。 ThreadPoolExecutorのようなものを使用して再帰を並列化する際の主な問題は、各再帰ステップが独自のスレッドを必要とし、スタックの上のスレッドがアイドル状態で待機しているため、スレッドがすぐに不足する可能性があることです。

フォーク/結合フレームワークのエントリポイントは、ExecutorServiceの実装であるForkJoinPoolクラスです。 アイドル状態のスレッドがビジーなスレッドから「スチール」しようとするワークスティールアルゴリズムを実装しています。 これにより、異なるスレッド間で計算を分散し、通常のスレッドプールで必要とするよりも少ないスレッドを使用しながら進行することができます。

フォーク/結合フレームワークの詳細とコードサンプルは、記事“Guide to the Fork/Join Framework in Java”にあります。