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列挙型で説明されています。 彼らです:

  • NEW Thread.start()を介してまだ開始されていない新しいThreadインスタンス
  • RUNNABLE —実行中のスレッド。 これは、実行可能と呼ばれます。これは、いつでも実行中であるか、スレッドスケジューラからの次の時間量を待機している可能性があるためです。 NEW スレッドは、 Thread.start()を呼び出すと、RUNNABLE状態になります。
  • BLOCKED —同期されたセクションに入る必要があるが、このセクションのモニターを保持している別のスレッドのためにそれを行うことができない場合、実行中のスレッドはブロックされます
  • WAITING —別のスレッドが特定のアクションを実行するのを待機している場合、スレッドはこの状態になります。 たとえば、スレッドは、保持しているモニターで Object.wait()メソッドを呼び出すか、別のスレッドで Thread.join()メソッドを呼び出すと、この状態になります。
  • TIMED_WAITING —上記と同じですが、スレッドは Thread.sleep() Object.wait()、[X156X ] Thread.join()およびその他のメソッド
  • TERMINATED —スレッドは Runnable.run()メソッドの実行を完了し、終了しました

Q4。 実行可能インターフェースと呼び出し可能インターフェースの違いは何ですか? それらはどのように使用されますか?

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.interrupt()を呼び出すだけです。

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

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

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

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

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

ExecutorおよびExecutorServiceの使用の詳細については、記事 JavaExecutorServiceのガイドを参照してください。

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

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

  • ThreadPoolExecutor —スレッドのプールを使用してタスクを実行するため。 スレッドがタスクの実行を終了すると、プールに戻ります。 プール内のすべてのスレッドがビジーの場合、タスクは順番を待つ必要があります。
  • ScheduledThreadPoolExecutor を使用すると、スレッドが使用可能になったときにすぐに実行するのではなく、タスクの実行をスケジュールできます。 また、固定レートまたは固定遅延でタスクをスケジュールすることもできます。
  • ForkJoinPool は、再帰的アルゴリズムタスクを処理するための特別なExecutorServiceです。 再帰的アルゴリズムに通常のThreadPoolExecutorを使用すると、すべてのスレッドが低レベルの再帰が終了するのを待ってビジー状態になっていることがすぐにわかります。 ForkJoinPool は、利用可能なスレッドをより効率的に使用できるようにする、いわゆるワークスティーリングアルゴリズムを実装しています。

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

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

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

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

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

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

  • アクション、これらは、変数の読み取りまたは書き込み、モニターのロック/ロック解除など、あるスレッドで実行され、別のスレッドで検出できるスレッド間アクションです。
  • 同期アクション volatile 変数の読み取り/書き込み、モニターのロック/ロック解除などのアクションの特定のサブセット
  • プログラム順序(PO)、単一スレッド内のアクションの観察可能な全順序
  • 同期順序(SO)、すべての同期アクション間の全順序—プログラム順序と一致している必要があります。つまり、POで2つの同期アクションが次々に発生する場合、それらはで同じ順序で発生します。それで
  • synchronizes-with (SW)モニターのロック解除や同じモニターのロック(別のスレッドまたは同じスレッド内)などの特定の同期アクション間の関係
  • 発生-順序の前— POとSW(集合論では推移閉包と呼ばれます)を組み合わせて、スレッド間のすべてのアクションの半順序を作成します。 あるアクションが別のアクションの前に発生した場合、最初のアクションの結果は2番目のアクションで観察できます(たとえば、あるスレッドで変数を書き込み、別のスレッドで読み取る)。
  • 発生-一貫性の前-すべての読み取りが、発生前の順序でその場所への最後の書き込み、またはデータ競合を介した他の書き込みのいずれかを監視する場合、一連のアクションはHB-一貫性があります
  • 実行—順序付けられたアクションの特定のセットとそれらの間の一貫性ルール

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

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

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

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

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

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

  • 揮発性intへの書き込み;
  • volatileintへの書き込み;
  • 揮発性longへの書き込み;
  • volatilelongへの書き込み;
  • volatile long をインクリメントしますか?

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

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

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

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

そのため、不変オブジェクトを作成するときは、getterメソッドでアクセスできない場合でも、常にすべてのフィールドをfinalにする必要があります。

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

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

synchronized(object) {
    // ...
}

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

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

静的同期メソッドの場合、モニターは宣言クラスを表す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フルキューに入れると、putメソッドwaitgetメソッドの呼び出しを待ちます。 get メソッドは要素を削除し、 put メソッドで待機しているスレッドに、キューに新しいアイテム用の空の場所があることを通知します。

public class BlockingQueue<T> {

    private List<T> queue = new LinkedList<T>();

    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。 デッドロック、ライブロック、および飢餓の状態を説明してください。 これらの状態の考えられる原因を説明してください。

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

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

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

Q17。 Fork /JoinFrameworkの目的とユースケースを説明します。

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

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

フォーク/結合フレームワークの詳細とコードサンプルは、記事「Javaでのフォーク/結合フレームワークのガイド」にあります。