Java マルチスレッドに関する面接の質問
プロセスは自己完結型の実行環境であり、プログラムまたはアプリケーションとみなすことができます。スレッドはプロセス内で実行されるタスクです。 Java ランタイム環境は、さまざまなクラスとプログラムを含む単一のプロセスです。スレッドは軽量プロセスと呼ぶことができます。スレッドは、プロセス内での作成と常駐に必要なリソースが少なくて済み、プロセス内でリソースを共有できます。
マルチスレッド プログラムでは、プログラムの効率を向上させるために複数のスレッドが同時に実行されます。スレッドはリソースを待機する必要があるため、CPU はアイドル状態になりません。複数のスレッドはヒープ メモリを共有するため、複数のプロセスを作成するよりも、いくつかのタスクを実行するために複数のスレッドを作成する方が適切です。たとえば、サーブレットはマルチスレッドをサポートしますが、CGI はサポートしないため、サーブレットは CGI よりも優れています。
Java プログラムでスレッドを作成すると、それはユーザー スレッドと呼ばれます。デーモン スレッドはバックグラウンドで実行され、JVM の終了を妨げないスレッドです。ユーザー スレッドが実行されていない場合、JVM はプログラムを閉じて終了します。デーモン スレッドによって作成された子スレッドは、依然としてデーモン スレッドです。
スレッドを作成するには 2 つの方法があります。1 つは Runnable インターフェイスを実装し、それを Thread コンストラクターに渡して Thread オブジェクトを作成する方法で、もう 1 つは Thread クラスを直接継承する方法です。さらに詳しく知りたい場合は、Java でスレッドを作成する方法に関するこの記事を読んでください。
Java プログラムで新しいスレッドを作成すると、そのステータスは New になります。スレッドの start() メソッドを呼び出すと、ステータスが Runnable に変更されます。スレッド スケジューラは、実行可能スレッド プール内のスレッドに CPU 時間を割り当て、ステータスを実行中に変更します。他のスレッド状態には、待機中、ブロック済み、デッドなどがあります。スレッドのライフサイクルについて詳しくは、この記事をお読みください。
もちろん、Thread の run() メソッドを呼び出すと、新しいスレッドでコードを実行するには、Thread.start() メソッドを使用する必要があります。
Thread クラスの Sleep() メソッドを使用して、スレッドを一定期間一時停止できます。これによってスレッドが終了するわけではないことに注意してください。スレッドがスリープから復帰すると、スレッドのステータスは実行可能に変更され、スレッドのスケジュールに従って実行されます。
各スレッドには優先順位があります。一般に、実行時には優先順位の高いスレッドが優先されますが、これは OS に依存するスレッド スケジューリングの実装によって異なります。スレッドの優先順位を定義できますが、これは、優先順位の高いスレッドが優先順位の低いスレッドよりも前に実行されることを保証するものではありません。スレッドの優先順位は int 変数 (1 ~ 10) で、1 は最低の優先順位を表し、10 は最高の優先順位を表します。
スレッド スケジューラは、実行可能状態のスレッドに CPU 時間を割り当てる役割を担うオペレーティング システム サービスです。スレッドを作成して開始すると、その実行はスレッド スケジューラの実装に依存します。タイム スライシングとは、利用可能な CPU 時間を利用可能な実行可能スレッドに割り当てるプロセスを指します。 CPU 時間の割り当ては、スレッドの優先順位またはスレッドの待機時間に基づいて行うことができます。スレッドのスケジューリングは Java 仮想マシンによって制御されないため、アプリケーションが制御することをお勧めします (つまり、プログラムをスレッドの優先順位に依存させないでください)。
コンテキストの切り替えは、CPU の状態を保存および復元するプロセスであり、これにより、スレッドの実行が中断された時点から実行を再開できるようになります。コンテキストの切り替えは、マルチタスク オペレーティング システムおよびマルチスレッド環境に不可欠な機能です。
Thread クラスの Joint() メソッドを使用すると、プログラムによって作成されたすべてのスレッドが main() メソッドが終了する前に終了するようにできます。 Threadクラスのjoint()メソッドについての記事です。
スレッド間でリソースを共有できる場合、スレッド間通信はそれらを調整する重要な手段です。 Object クラスの wait()/notify()/notifyAll() メソッドを使用して、リソース ロックのステータスについてスレッド間で通信できます。スレッド待機、notify、notifyAll の詳細については、ここをクリックしてください。
Java の各オブジェクトにはロック (モニター、モニターになることもできます) があり、wait() や Notify() などのメソッドは、オブジェクトのロックを待機したり、オブジェクトのモニターが使用可能であることを他のスレッドに通知したりするために使用されます。 Java スレッドのオブジェクトに対して使用できるロックやシンクロナイザーはありません。そのため、これらのメソッドは Object クラスの一部であり、Java のすべてのクラスにはスレッド間通信のための基本メソッドが含まれています。
スレッドがオブジェクトの wait() メソッドを呼び出す必要がある場合、スレッドはオブジェクトのロックを所有する必要があり、その後、他のスレッドがオブジェクトの notify() メソッドを呼び出すまで、オブジェクトのロックを解放し、待機状態に入ります。同様に、スレッドがオブジェクトのnotify()メソッドを呼び出す必要がある場合、スレッドはオブジェクトのロックを解放し、待機中の他のスレッドがオブジェクトのロックを取得できるようにします。これらのメソッドはすべて、スレッドがオブジェクトのロックを保持する必要があり、これは同期によってのみ実現できるため、同期メソッドまたは同期ブロック内でのみ呼び出すことができます。
Thread クラスの sleep() メソッドと yield() メソッドは、現在実行中のスレッド上で実行されます。したがって、待機している他のスレッドでこれらのメソッドを呼び出すことは意味がありません。このため、これらのメソッドは静的です。これらは現在実行中のスレッドで動作し、プログラマがこれらのメソッドを他の非実行スレッドで呼び出すことができると誤って考えるのを防ぐことができます。
Java でスレッドの安全性を確保するには、同期、アトミック同時クラスの使用、同時ロックの実装、volatile キーワードの使用、不変クラスとスレッドセーフ クラスの使用など、さまざまな方法があります。詳細については、スレッド セーフティのチュートリアルをご覧ください。
volatile キーワードを使用して変数を変更すると、スレッドは変数をキャッシュせずに直接読み取ります。これにより、スレッドによって読み取られる変数がメモリ内の変数と一致することが保証されます。
同期ブロックはオブジェクト全体をロックしないため、より良い選択です (もちろん、オブジェクト全体をロックすることもできます)。同期メソッドは、クラス内に無関係な同期ブロックが複数ある場合でもオブジェクト全体をロックします。そのため、通常、メソッドの実行が停止し、オブジェクトのロックを取得するまで待機する必要があります。
Thread クラスの setDaemon(true) メソッドを使用して、スレッドをデーモン スレッドとして設定できます。このメソッドは、start() メソッドを呼び出す前に呼び出す必要があることに注意してください。そうしないと、IllegalThreadStateException がスローされます。
ThreadLocal は、スレッド ローカル変数を作成するために使用されます。オブジェクトのすべてのスレッドがそのグローバル変数を共有するため、これらの変数はスレッド セーフではありません。ただし、同期を使用したくない場合は、ThreadLocal 変数を選択できます。
各スレッドには独自の Thread 変数があり、get()/set() メソッドを使用してデフォルト値を取得したり、スレッド内で値を変更したりできます。 ThreadLocal インスタンスは通常、関連するスレッド状態をプライベート静的プロパティにする必要があります。 ThreadLocal のサンプル記事では、ThreadLocal に関する小さなプログラムを見ることができます。
ThreadGroup は、スレッド グループに関する情報を提供することを目的としたクラスです。
ThreadGroup API は比較的弱く、Thread より多くの機能は提供しません。これには 2 つの主な機能があります。1 つはスレッド グループ内のアクティブなスレッドのリストを取得すること、もう 1 つはスレッドにキャッチされなかった例外ハンドラー (ncaught 例外ハンドラー) を設定することです。ただし、Java 1.5 では、Thread クラスに setUncaughtExceptionHandler(UncaughtExceptionHandler eh) メソッドも追加されたため、ThreadGroup は廃止されたため、引き続き使用することはお勧めできません。
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){ @Overridepublic void uncaughtException(Thread t, Throwable e) {System.out.println("例外が発生しました:"+e.getMessage());} });
スレッド ダンプは JVM アクティブ スレッドのリストであり、システムのボトルネックやデッドロックを分析するのに非常に役立ちます。スレッド ダンプを取得するには、プロファイラー、Kill -3 コマンド、jstack ツールなどを使用するさまざまな方法があります。 jstack ツールは使いやすく、JDK が付属しているため、私はこれを好みます。これはターミナルベースのツールであるため、分析用のスレッド ダンプを定期的に生成するスクリプトを作成できます。スレッド ダンプの生成について詳しくは、このドキュメントをお読みください。
デッドロックとは、3 つ以上のスレッドが永久にブロックされる状況を指します。この状況では、少なくとも 2 つのスレッドと 3 つ以上のリソースが必要です。
デッドロックを分析するには、Java アプリケーションのスレッド ダンプを確認する必要があります。どのスレッドが BLOCKED ステータスにあり、どのスレッドが待機しているリソースを確認する必要があります。各リソースには一意の ID があり、この ID を使用して、どのスレッドがそのオブジェクト ロックをすでに所有しているかを調べることができます。
デッドロックを回避する一般的な方法は、ネストされたロックを回避し、必要な場合にのみロックを使用し、無期限の待機を回避することです。デッドロックを分析する方法については、この記事をお読みください。
java.util.Timer は、将来の特定の時間にスレッドを実行するようにスケジュールするために使用できるツール クラスです。 Timer クラスを使用して、1 回限りのタスクまたは定期的なタスクをスケジュールできます。
java.util.TimerTask は、Runnable インターフェースを実装する抽象クラスです。独自のスケジュールされたタスクを作成し、Timer を使用してその実行をスケジュールするには、このクラスを継承する必要があります。
Java タイマーに関する例を次に示します。
スレッド プールはワーカー スレッドのグループを管理し、実行を待機しているタスクを配置するためのキューも含みます。
java.util.concurrent.Executors は、スレッド プールを作成するための java.util.concurrent.Executor インターフェイスの実装を提供します。スレッド プールの例では、スレッド プールを作成して使用する方法を示します。または、ScheduledThreadPoolExecutor の例を読んで、定期タスクの作成方法を学習します。
Java 同時実行に関する面接の質問
アトミック操作とは、他の操作の影響を受けない操作タスク単位を指します。アトミック操作は、マルチスレッド環境でのデータの不整合を回避するために必要な手段です。
int++ はアトミック操作ではないため、あるスレッドがその値を読み取って 1 を追加すると、別のスレッドが前の値を読み取ってエラーが発生する可能性があります。
この問題を解決するには、増加操作がアトミックであることを確認する必要があります。JDK1.5 より前では、同期テクノロジを使用してこれを行うことができました。 JDK 1.5 の時点では、java.util.concurrent.atomic パッケージは、操作がアトミックであることを自動的に保証し、同期の使用を必要としない int 型および long 型のロード クラスを提供します。 Java のアトミック クラスについて学ぶには、この記事を読むことができます。
Lock インターフェイスは、同期メソッドや同期ブロックよりもスケーラブルなロック操作を提供します。これらにより、完全に異なるプロパティを持つことができるより柔軟な構造が可能になり、条件付きオブジェクトの複数の関連クラスをサポートできます。
その利点は次のとおりです。
ロックの例について詳しく読む
Executor フレームワークは、Java 5 で java.util.concurrent.Executor インターフェイスとともに導入されました。 Executor フレームワークは、一連の実行戦略に従って呼び出され、スケジュールされ、実行され、制御される非同期タスクのフレームワークです。
無制限にスレッドを作成すると、アプリケーション メモリがオーバーフローする可能性があります。したがって、スレッド プールを作成する方が、スレッドの数を制限でき、これらのスレッドをリサイクルして再利用できるため、より良い解決策となります。 Executor フレームワークを使用してスレッド プールを作成すると、非常に便利です。Executor フレームワークを使用してスレッド プールを作成する方法については、この記事をお読みください。
java.util.concurrent.BlockingQueue の特性は、キューが空の場合はキューから要素を取得または削除する操作がブロックされ、キューがいっぱいの場合はキューに要素を追加する操作がブロックされます。 。
ブロッキング キューは null 値を受け入れません。キューに null 値を追加しようとすると、NullPointerException がスローされます。
ブロッキング キューの実装はスレッドセーフであり、すべてのクエリ メソッドはアトミックであり、内部ロックまたは他の形式の同時実行制御を使用します。
BlockingQueue インターフェイスは Java コレクション フレームワークの一部であり、主にプロデューサーとコンシューマーの問題を実装するために使用されます。
ブロッキング キューを使用してプロデューサーとコンシューマーの問題を実装する方法については、この記事をお読みください。
Java 5 では、同時実行パッケージに java.util.concurrent.Callable インターフェースが導入されました。これは Runnable インターフェースに非常によく似ていますが、オブジェクトを返したり、例外をスローしたりできます。
Callable インターフェイスは、ジェネリックスを使用して戻り値の型を定義します。 Executors クラスは、スレッド プール内の Callable 内でタスクを実行するための便利なメソッドをいくつか提供します。 Callable タスクは並列であるため、それが返す結果を待つ必要があります。 java.util.concurrent.Future オブジェクトは、この問題を解決します。スレッド プールが Callable タスクを送信すると、Future オブジェクトが返されます。これを使用すると、Callable タスクのステータスを確認し、Callable によって返される実行結果を取得できます。 Future は、Callable が終了するのを待ってその実行結果を取得できるように get() メソッドを提供します。
Callable、Future に関するその他の例については、この記事をお読みください。
FutureTask は Future の基本的な実装であり、Executor とともに使用して非同期タスクを処理できます。通常、FutureTask クラスを使用する必要はありませんが、Future インターフェイスの一部のメソッドをオーバーライドし、元の基本実装を維持することを計画している場合には、このクラスが非常に役立ちます。それを継承して、必要なメソッドをオーバーライドするだけです。その使用方法については、Java FutureTask の例を読んでください。
Java コレクション クラスはフェイルファストです。つまり、コレクションが変更され、スレッドが反復子を使用してコレクションを走査すると、反復子の next() メソッドが ConcurrentModificationException 例外をスローします。
同時コンテナは、同時走査と同時更新をサポートします。
主なクラスは ConcurrentHashMap、CopyOnWriteArrayList、および CopyOnWriteArraySet です。ConcurrentModificationException を回避する方法については、この記事をお読みください。
Executor は、Executor、ExecutorService、ScheduledExecutorService、ThreadFactory、および Callable クラスにいくつかのユーティリティ メソッドを提供します。
エグゼキュータを使用すると、スレッド プールを簡単に作成できます。
原文:journaldev.com 翻訳:ifeve 翻訳者:Zheng Xudong