序章
したがって、Java にはスレッドがあることがわかります。これについては、「Better together: Java and the Thread class」というタイトルのレビューで読むことができます。パート I — 実行のスレッド。スレッドは並行して作業を実行するために必要です。これにより、スレッドが何らかの形で相互作用する可能性が高くなります。これがどのように起こるのか、そしてどのような基本的なツールがあるのかを見てみましょう。
収率
Thread.yield()は不可解であり、ほとんど使用されません。インターネット上ではさまざまな方法で説明されています。スレッドの優先順位に基づいてスレッドが降順される、スレッドのキューが存在すると書いている人もいます。他の人は、スレッドのステータスが「実行中」から「実行可能」に変更されると書いています (これらのステータスには区別がないにもかかわらず、つまり Java はステータスを区別しません)。実際のところ、それはあまり知られていませんが、ある意味ではもっと単純です。
yield()
メソッドのドキュメントに記録されています。読んでみると一目瞭然ですが、yield()
このメソッドは実際には、このスレッドの実行時間を短縮できるという推奨事項を Java スレッド スケジューラに提供するだけです。しかし、実際に何が起こるか、つまりスケジューラが推奨に基づいて動作するかどうか、またスケジューラが一般的に何を行うかは、JVM の実装とオペレーティング システムによって異なります。また、他の要因にも依存する可能性があります。すべての混乱は、Java 言語の発展に伴ってマルチスレッドが見直されたことが原因である可能性が最も高くなります。概要の詳細については、「Java Thread.yield() の概要」を参照してください。
寝る
スレッドは実行中にスリープ状態になることがあります。これは、他のスレッドとの対話の最も簡単なタイプです。Java コードを実行する Java 仮想マシンを実行するオペレーティング システムには、独自のスレッドスケジューラがあります。どのスレッドをいつ開始するかを決定します。プログラマは、Java コードからこのスケジューラを直接操作することはできず、JVM を介してのみ操作できます。ユーザーは、スケジューラにスレッドをしばらく一時停止する、つまりスレッドをスリープさせるように要求できます。詳細については、Thread.sleep()およびHow Multithreading worksの記事を参照してください。Windows オペレーティング システムでスレッドがどのように動作するかを確認することもできます: Internals of Windows Thread。そして今度は自分の目で見てみましょう。次のコードを という名前のファイルに保存しますHelloWorldApp.java
。
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
ご覧のとおり、60 秒間待機するタスクがあり、その後プログラムが終了します。コマンド「javac HelloWorldApp.java
」を使用してコンパイルし、「 」を使用してプログラムを実行しますjava HelloWorldApp
。プログラムを別のウィンドウで起動することをお勧めします。たとえば、Windows では次のようになりますstart java HelloWorldApp
。jps コマンドを使用して PID (プロセス ID) を取得し、「 」でスレッドのリストを開きます。jvisualvm --openpid pid
ご覧 
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
あちこちで扱っていることに気づきましたかInterruptedException
?その理由を理解しましょう。
Thread.interrupt()
問題は、スレッドが待機中またはスリープ中に、誰かが中断しようとする可能性があるということです。この場合、 を処理しますInterruptedException
。このメカニズムは、Thread.stop()
メソッドが非推奨と宣言された後に作成されました。つまり、時代遅れで望ましくないものです。その理由はstop()
、メソッドが呼び出されたときにスレッドが単純に「強制終了」されたためであり、これは非常に予測不可能でした。スレッドがいつ停止されるかはわかりませんし、データの一貫性も保証できませんでした。スレッドが強制終了されている間にファイルにデータを書き込んでいると想像してください。Java の作成者は、スレッドを強制終了するよりも、スレッドを中断するように指示する方が論理的であると判断しました。この情報にどのように対応するかは、スレッド自体が決定する問題です。詳細については、「Thread.stop が非推奨になるのはなぜですか?」を参照してください。オラクルのウェブサイトで。例を見てみましょう:
public static void main(String []args) {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
この例では、60 秒は待ちません。代わりに、すぐに「中断されました」と表示されます。interrupt()
これは、スレッド上でメソッドを呼び出したためです。このメソッドは、「割り込みステータス」と呼ばれる内部フラグを設定します。つまり、各スレッドには直接アクセスできない内部フラグがあります。ただし、このフラグを操作するためのネイティブ メソッドがあります。しかし、それが唯一の方法ではありません。スレッドは実行中であり、何かを待っているのではなく、単にアクションを実行しているだけである可能性があります。しかし、他の人が特定の時間に作業を終了したいと考えるかもしれません。例えば:
public static void main(String []args) {
Runnable task = () -> {
while(!Thread.currentThread().isInterrupted()) {
// Do some work
}
System.out.println("Finished");
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
上の例では、while
スレッドが外部から中断されるまでループが実行されます。フラグに関してはisInterrupted
、 をキャッチするとInterruptedException
isInterrupted フラグがリセットされ、isInterrupted()
false が返されることを知っておくことが重要です。Thread クラスには、現在のスレッドにのみ適用される静的なThread.interrupted()メソッドもありますが、このメソッドはフラグを false にリセットします。詳細については、 「スレッドの中断」というタイトルの章を参照してください。
参加 (別のスレッドが終了するまで待ちます)
最も単純なタイプの待機は、別のスレッドが終了するのを待機することです。public static void main(String []args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println("Finished");
}
この例では、新しいスレッドは 5 秒間スリープします。同時に、メインスレッドは、スリープ状態のスレッドが目覚めて作業を完了するまで待機します。JVisualVM でスレッドの状態を見ると、次のようになります。 
join
実行される Java コードを含むメソッドにすぎないため、非常に単純です。wait()
スレッドが終了すると (作業が終了すると)、待機は中断されます。これがこのメソッドの魔法のすべてですjoin()
。それでは、最も興味深いことに移りましょう。
モニター
マルチスレッドにはモニターの概念が含まれています。モニターという言葉は、16 世紀のラテン語を経て英語に伝わり、「プロセスを観察、チェック、または継続的に記録するために使用される機器または装置」を意味します。この記事では、基本的な事項について説明します。詳細が必要な場合は、リンク先の資料を参照してください。まずは Java 言語仕様 (JLS): 17.1 から始めます。同期。それは次のように述べています:
lock()
、 で解放したりできますunlock()
。次に、Oracle の Web サイトにあるチュートリアル「Intrinsic Locks and Synchronization」を見つけます。。このチュートリアルでは、Java の同期は、固有ロックまたはモニター ロックと呼ばれる内部エンティティを中心に構築されていると述べています。このロックは、単に「モニター」と呼ばれることがよくあります。また、Java のすべてのオブジェクトには、それに関連付けられた固有のロックがあることもわかります。「Java 組み込みロックと同期」を参照してください。次に、Java のオブジェクトをモニターにどのように関連付けることができるかを理解することが重要です。Java では、各オブジェクトにはヘッダーがあり、プログラマがコードからは利用できないが、仮想マシンがオブジェクトを正しく操作するために必要な内部メタデータが格納されます。オブジェクト ヘッダーには、次のような「マーク ワード」が含まれています。 
https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
public class HelloWorld{
public static void main(String []args){
Object object = new Object();
synchronized(object) {
System.out.println("Hello World");
}
}
}
ここで、現在のスレッド (これらのコード行が実行されるスレッド) は、synchronized
キーワードを使用して、object"\
ロックを取得/取得するための変数。他にモニターを争う人がいない場合 (つまり、同じオブジェクトを使用して同期コードを実行している人が他にいない場合)、Java は「バイアス ロック」と呼ばれる最適化を実行しようとする場合があります。関連するタグと、どのスレッドがモニターのロックを所有しているかに関するレコードが、オブジェクト ヘッダーのマーク ワードに追加されます。これにより、モニターをロックするために必要なオーバーヘッドが軽減されます。モニターが以前に別のスレッドによって所有されていた場合、そのようなロックだけでは十分ではありません。JVM は次のタイプのロック、「基本ロック」に切り替えます。比較交換 (CAS) 操作を使用します。さらに、オブジェクト ヘッダーのマーク ワード自体はマーク ワードを保存するのではなく、マーク ワードが保存されている場所への参照を保存するようになり、基本ロックを使用していることを JVM が理解できるようにタグが変更されます。複数のスレッドがモニターをめぐって競合 (競合) する場合 (1 つはロックを取得し、もう 1 つはロックが解放されるのを待っている)、マーク ワード内のタグが変更され、マーク ワードはモニターへの参照を保存するようになります。オブジェクトとして - JVM の内部エンティティ。JDK Enchancement Proposal (JEP) で述べられているように、この状況では、このエンティティを保存するためにメモリのネイティブ ヒープ領域にスペースが必要です。この内部エンティティのメモリ位置への参照は、オブジェクト ヘッダーのマーク ワードに保存されます。したがって、モニターは実際には、複数のスレッド間で共有リソースへのアクセスを同期するためのメカニズムです。JVM は、このメカニズムのいくつかの実装を切り替えます。したがって、わかりやすくするために、モニターについて話すときは、実際にはロックについて話していることになります。次に、マーク ワード内のタグが変更され、マーク ワードはモニターへの参照をオブジェクト (JVM の内部エンティティ) として保存します。JDK Enchancement Proposal (JEP) で述べられているように、この状況では、このエンティティを保存するためにメモリのネイティブ ヒープ領域にスペースが必要です。この内部エンティティのメモリ位置への参照は、オブジェクト ヘッダーのマーク ワードに保存されます。したがって、モニターは実際には、複数のスレッド間で共有リソースへのアクセスを同期するためのメカニズムです。JVM は、このメカニズムのいくつかの実装を切り替えます。したがって、わかりやすくするために、モニターについて話すときは、実際にはロックについて話していることになります。次に、マーク ワード内のタグが変更され、マーク ワードはモニターへの参照をオブジェクト (JVM の内部エンティティ) として保存します。JDK Enchancement Proposal (JEP) で述べられているように、この状況では、このエンティティを保存するためにメモリのネイティブ ヒープ領域にスペースが必要です。この内部エンティティのメモリ位置への参照は、オブジェクト ヘッダーのマーク ワードに保存されます。したがって、モニターは実際には、複数のスレッド間で共有リソースへのアクセスを同期するためのメカニズムです。JVM は、このメカニズムのいくつかの実装を切り替えます。したがって、わかりやすくするために、モニターについて話すときは、実際にはロックについて話していることになります。そして、マーク ワードは、モニターへの参照をオブジェクト (JVM の内部エンティティ) として保存するようになりました。JDK Enchancement Proposal (JEP) で述べられているように、この状況では、このエンティティを保存するためにメモリのネイティブ ヒープ領域にスペースが必要です。この内部エンティティのメモリ位置への参照は、オブジェクト ヘッダーのマーク ワードに保存されます。したがって、モニターは実際には、複数のスレッド間で共有リソースへのアクセスを同期するためのメカニズムです。JVM は、このメカニズムのいくつかの実装を切り替えます。したがって、わかりやすくするために、モニターについて話すときは、実際にはロックについて話していることになります。そして、マーク ワードは、モニターへの参照をオブジェクト (JVM の内部エンティティ) として保存するようになりました。JDK Enchancement Proposal (JEP) で述べられているように、この状況では、このエンティティを保存するためにメモリのネイティブ ヒープ領域にスペースが必要です。この内部エンティティのメモリ位置への参照は、オブジェクト ヘッダーのマーク ワードに保存されます。したがって、モニターは実際には、複数のスレッド間で共有リソースへのアクセスを同期するためのメカニズムです。JVM は、このメカニズムのいくつかの実装を切り替えます。したがって、わかりやすくするために、モニターについて話すときは、実際にはロックについて話していることになります。この内部エンティティのメモリ位置への参照は、オブジェクト ヘッダーのマーク ワードに保存されます。したがって、モニターは実際には、複数のスレッド間で共有リソースへのアクセスを同期するためのメカニズムです。JVM は、このメカニズムのいくつかの実装を切り替えます。したがって、わかりやすくするために、モニターについて話すときは、実際にはロックについて話していることになります。この内部エンティティのメモリ位置への参照は、オブジェクト ヘッダーのマーク ワードに保存されます。したがって、モニターは実際には、複数のスレッド間で共有リソースへのアクセスを同期するためのメカニズムです。JVM は、このメカニズムのいくつかの実装を切り替えます。したがって、わかりやすくするために、モニターについて話すときは、実際にはロックについて話していることになります。 
同期済み (ロックを待機中)
前に見たように、「同期ブロック」(または「クリティカル セクション」) の概念はモニターの概念と密接に関連しています。例を見てみましょう:public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = () -> {
synchronized(lock) {
System.out.println("thread");
}
};
Thread th1 = new Thread(task);
th1.start();
synchronized(lock) {
for (int i = 0; i < 8; i++) {
Thread.currentThread().sleep(1000);
System.out.print(" " + i);
}
System.out.println(" ...");
}
}
ここで、メインスレッドは最初にタスク オブジェクトを新しいスレッドに渡し、すぐにロックを取得して、それに対して長い操作 (8 秒) を実行します。この間ずっと、タスクはsynchronized
ロックがすでに取得されているためブロックに入ることができないため、続行できません。スレッドがロックを取得できない場合、スレッドはモニターを待ちます。ロックを取得するとすぐに実行を続行します。スレッドがモニターを終了すると、ロックが解放されます。JVisualVM では、次のようになります。 
th1.getState()
for ループ内のステートメントはBLOCKEDを返します。これは、ループが実行されている限り、lock
オブジェクトのモニターがスレッドによって占有されmain
、th1
スレッドはブロックされ、ロックが解放されるまで続行できないためです。同期されたブロックに加えて、メソッド全体を同期することもできます。たとえば、HashTable
クラスのメソッドは次のとおりです。
public synchronized int size() {
return count;
}
このメソッドは、常に 1 つのスレッドによってのみ実行されます。本当にロックが必要ですか? はい、必要です。インスタンス メソッドの場合、「この」オブジェクト (現在のオブジェクト) がロックとして機能します。このトピックに関する興味深い議論がここにあります:同期ブロックの代わりに同期メソッドを使用する利点はありますか? 。メソッドが静的である場合、ロックは「this」オブジェクトではなく (静的メソッドには「this」オブジェクトがないため)、Class オブジェクト (たとえば、Integer.class
) になります。
待ちます(モニターを待っています)。Notice() メソッドと NotifyAll() メソッド
Thread クラスには、モニターに関連付けられた別の待機メソッドがあります。sleep()
やとは異なりjoin()
、このメソッドは単純に呼び出すことはできません。その名はwait()
。このwait
メソッドは、待機するモニターに関連付けられたオブジェクト上で呼び出されます。例を見てみましょう:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// The task object will wait until it is notified via lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// After we are notified, we will wait until we can acquire the lock
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// We sleep. Then we acquire the lock, notify, and release the lock
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
JVisualVM では、次のようになります。 
wait()
およびnotify()
メソッドが に関連付けられていることを思い出してくださいjava.lang.Object
。スレッド関連のメソッドがObject
クラス内にあるのは奇妙に思えるかもしれません。しかし、その理由が今明らかになりました。Java のすべてのオブジェクトにはヘッダーがあることを思い出してください。ヘッダーには、モニターに関する情報、つまりロックのステータスなどのさまざまなハウスキーピング情報が含まれています。各オブジェクトまたはクラスのインスタンスは、固有ロックまたはモニターと呼ばれる JVM の内部エンティティに関連付けられていることに注意してください。上の例では、タスク オブジェクトのコードは、オブジェクトに関連付けられたモニターの同期ブロックに入ることを示していますlock
。このモニターのロックの取得に成功すると、wait()
と呼ばれます。タスクを実行しているスレッドはlock
オブジェクトのモニターを解放しますが、オブジェクトのモニターからの通知を待つスレッドのキューに入りますlock
。このスレッドのキューは WAIT SET と呼ばれ、その目的をより適切に反映しています。つまり、キューというよりもセットに近いものです。スレッドmain
はタスク オブジェクトを使用して新しいスレッドを作成し、開始して 3 秒待機します。これにより、新しいスレッドがスレッドよりも先にロックを取得しmain
、モニターのキューに入る可能性が高くなります。その後、main
スレッド自身がオブジェクトの同期ブロックに入りlock
、モニターを使用してスレッド通知を実行します。通知が送信された後、main
スレッドはlock
lock
オブジェクトのモニターが解放されると、オブジェクトのモニターが解放されるのを待っていた新しいスレッドが実行を継続します。notify()
通知を 1 つのスレッドにのみ送信することも ( )、キュー内のすべてのスレッドに同時に送信することもできます( notifyAll()
)。詳細については、「Java の Notice() と NoticeAll() の違い」を参照してください。通知の順序は JVM の実装方法によって異なることに注意することが重要です。詳細はこちらをご覧ください:通知と通知を使用して飢餓を解決するにはどうすればよいですか? 。オブジェクトを指定せずに同期を実行できます。これは、単一のコード ブロックではなくメソッド全体が同期される場合に実行できます。たとえば、静的メソッドの場合、ロックは Class オブジェクトになります ( を介して取得.class
)。
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
ロックの使用という点では、どちらの方法も同じです。instance
メソッドが静的でない場合、同期は現在の 、つまり を使用して実行されますthis
。ところで、先ほど、getState()
メソッドを使用してスレッドのステータスを取得できると述べました。wait()
たとえば、モニターを待機しているキュー内のスレッドの場合、メソッドでタイムアウトが指定されている 場合、ステータスは WAITING または TIMED_WAITING になります。
https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java
スレッドのライフサイクル
スレッドのステータスは、その存続期間中に変化します。実際、これらの変更はスレッドのライフサイクルを構成します。スレッドが作成されるとすぐに、そのステータスは NEW になります。この状態では、新しいスレッドはまだ実行されておらず、Java スレッド スケジューラはそれについてまだ何も認識していません。スレッド スケジューラがスレッドについて学習するには、thread.start()
メソッドを呼び出す必要があります。その後、スレッドは RUNNABLE 状態に移行します。インターネットには、「実行可能」状態と「実行中」状態を区別する誤った図がたくさんあります。しかし、これは間違いです。Java は「動作準備完了」(実行可能) と「動作中」(実行中) を区別しないからです。スレッドは生きているがアクティブではない (実行可能ではない) 場合、次の 2 つの状態のいずれかになります。
- BLOCKED — クリティカルセクション、つまり
synchronized
ブロックに入るのを待っています。 - WAITING — 別のスレッドが何らかの条件を満たすのを待っています。
getState()
メソッドを使用します。スレッドにはisAlive()
、スレッドが終了していない場合に true を返すメソッドもあります。
LockSupport とスレッドパーキング
Java 1.6 から、LockSupportと呼ばれる興味深いメカニズムが登場しました。
park()
許可が利用可能な場合、メソッドの呼び出しはすぐに戻り、その過程で許可を消費します。それ以外の場合はブロックされます。このunpark
メソッドを呼び出すと、許可がまだ利用可能でない場合に利用可能になります。許可証は1つだけです。の Java ドキュメントでは、LockSupport
このSemaphore
クラスについて言及しています。簡単な例を見てみましょう。
import java.util.concurrent.Semaphore;
public class HelloWorldApp{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
try {
semaphore.acquire();
} catch (InterruptedException e) {
// Request the permit and wait until we get it
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
現在セマフォの許可が 0 であるため、このコードは常に待機します。acquire()
コード内で が呼び出される (つまり、許可を要求する) と、スレッドは許可を受け取るまで待機します。待っているので、 を処理する必要がありますInterruptedException
。興味深いことに、セマフォは別のスレッド状態を取得します。JVisualVM を見ると、状態が「待機」ではなく「パーク」であることがわかります。 
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Park the current thread
System.err.println("Will be Parked");
LockSupport.park();
// As soon as we are unparked, we will start to act
System.err.println("Unparked");
};
Thread th = new Thread(task);
th.start();
Thread.currentThread().sleep(2000);
System.err.println("Thread state: " + th.getState());
LockSupport.unpark(th);
Thread.currentThread().sleep(2000);
}
スレッドのステータスは WAITING になりますが、JVisualVM はキーワードwait
によるものsynchronized
とpark
クラスによるものを区別しますLockSupport
。なぜこれがLockSupport
それほど重要なのでしょうか? もう一度 Java ドキュメントに戻って、WAITINGスレッドの状態を見てみましょう。ご覧のとおり、それに入る方法は 3 つしかありません。そのうちの 2 つの方法は とwait()
ですjoin()
。そして3つ目は ですLockSupport
。Java では、ロックを上に構築してLockSuppor
、より高レベルのツールを提供することもできます。一つ使ってみましょう。たとえば、次を見てくださいReentrantLock
。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{
public static void main(String []args) throws InterruptedException {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
System.out.println("Thread");
lock.unlock();
};
lock.lock();
Thread th = new Thread(task);
th.start();
System.out.println("main");
Thread.currentThread().sleep(2000);
lock.unlock();
}
}
前の例と同様に、ここではすべてが単純です。オブジェクトlock
は誰かが共有リソースを解放するのを待ちます。main
JVisualVM を見ると、スレッドがロックを解放するまで新しいスレッドがパークされることがわかります。ロックの詳細については、「Java 8 StampedLocks vs. ReadWriteLocks and Synchronized and Lock API in Java」を参照してください。ロックの実装方法をよりよく理解するには、この記事「 Guide to the Java Phaser」で Phaser について読むと役立ちます。また、さまざまなシンクロナイザーについて言えば、 Java シンクロナイザーに関する DZone の記事を必ずお読みください。
結論
このレビューでは、Java でスレッドが対話する主な方法を調べました。追加資料:- Java と Thread クラスを組み合わせるとさらに効果的です。パート I — 実行スレッド
- https://dzone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions
GO TO FULL VERSION