JavaRush /Java Blog /Random-JA /スレッドで Java を破壊することはできません: パート II - 同期
Viacheslav
レベル 3

スレッドで Java を破壊することはできません: パート II - 同期

Random-JA グループに公開済み

導入

そのため、Java にはスレッドがあることがわかりました。これについては、「スレッドで Java を台無しにすることはできません: パート I - スレッド」のレビューで読むことができます。スレッドは同時に作業を行うために必要です。したがって、スレッドが何らかの形で相互作用する可能性が非常に高くなります。これがどのように起こるのか、そしてどのような基本的な制御があるのか​​を理解しましょう。 スレッドで Java を破壊することはできません: パート II - 同期 - 1

収率

Thread.yield()メソッドは謎めいていて、ほとんど使用されません。インターネット上にはさまざまな説明が存在します。スレッドが優先順位を考慮して下に移動する、ある種のスレッドのキューについて書いている人もいます。スレッドのステータスが実行中から実行可能に変更されると誰かが書いています (ただし、これらのステータスには分割がなく、Java はそれらを区別しません)。しかし実際には、すべてははるかに未知であり、ある意味ではより単純です。 スレッドで Java を破壊することはできません: パート II - 同期 - 2メソッドのドキュメントに関しては、yieldバグ「JDK-6416721: (仕様スレッド) Thread.yield() javadoc を修正する」があります。これを読むと、実際にはこのメソッドがyieldJava スレッド スケジューラに、このスレッドに与える実行時間を短縮できるという推奨事項を伝えているだけであることがわかります。しかし、実際に何が起こるか、スケジューラが推奨事項を聞くかどうか、そしてスケジューラが一般的に何を行うかは、JVM とオペレーティング システムの実装によって異なります。あるいは、他の要因によるものかもしれません。すべての混乱は、Java 言語の開発中にマルチスレッドを再考したことが原因である可能性が最も高くなります。詳細については、レビュー「Java Thread.yield() の概要」を参照してください。

Sleep - 眠りにつくスレッド

スレッドは実行中にスリープ状態になる場合があります。これは、他のスレッドとの最も単純なタイプの対話です。Java 仮想マシンがインストールされ、Java コードが実行されるオペレーティング システムには、スレッド スケジューラと呼ばれる独自のスレッド スケジューラがあります。どのスレッドをいつ実行するかを決定するのは彼です。プログラマは Java コードからこのスケジューラと直接対話することはできませんが、JVM を通じてスケジューラにスレッドをしばらく一時停止し、「スリープ状態にする」ように要求できます。詳細については、記事「Thread.sleep()」および「マルチスレッドの仕組み」を参照してください。さらに、Windows OS でスレッドがどのように動作するかについては、「 Windows スレッドの内部」を参照してください。今度は自分の目で見てみましょう。次のコードをファイルに保存してみましょうHelloWorldApp.java
class HelloWorldApp {
    public static void main(String []args) {
        Runnable task = () -> {
            try {
                int secToWait = 1000 * 60;
                Thread.currentThread().sleep(secToWait);
                System.out.println("Waked 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 を確認し、以下を使用してスレッドのリストを開きますjvisualvm --openpid pidПроцессаスレッドで Java を破壊することはできません: パート II - 同期 - 3ご覧のとおり、スレッドはスリープ状態に入っています。実際、現在のスレッドをスリープ状態にすると、より美しく実行できます。
try {
	TimeUnit.SECONDS.sleep(60);
	System.out.println("Waked up");
} catch (InterruptedException e) {
	e.printStackTrace();
}
おそらく、あらゆる場所で処理が行われていることにお気づきでしょうかInterruptedException? その理由を理解しましょう。

スレッドまたは Thread.interrupt の中断

問題は、スレッドがスリープ状態で待機している間に、誰かがこの待機を中断したい可能性があるということです。この場合、そのような例外を処理します。これは、メソッドがThread.stop非推奨と宣言された後に行われました。時代遅れで使用には望ましくない。その理由は、メソッドが呼び出されたときにstopスレッドが単純に「強制終了」されたためであり、これは非常に予測不可能でした。フローがいつ停止するかはわかりませんし、データの一貫性も保証できませんでした。ファイルにデータを書き込んでいるときにストリームが破壊されたと想像してください。したがって、フローを停止するのではなく、フローを中断する必要があることを通知する方が論理的であると判断しました。これにどう反応するかは流れそのものにかかっています。詳細については、Oracle の「Why is Thread.stop deprecated?」を参照してください。例を見てみましょう:
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();
}
上の例では、スレッドが外部から中断されるまでループが実行されることがわかりますwhileisInterruptedフラグについて知っておくべき重要な点は、これをキャッチするとInterruptedExceptionフラグがisInterruptedリセットされ、isInterruptedfalse が返されるということです。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 を通して見ると、スレッドの状態は次のようになります。 スレッドで Java を破壊することはできません: パート II - 同期 - 4監視ツールのおかげで、スレッドで何が起こっているかを確認できます。このメソッドは、呼び出されたスレッドが生きている間にjoin実行される単純な Java コードを含むメソッドであるため、非常に単純です。waitスレッドが終了すると (終了時)、待機は終了します。それがこのメソッドの魔法のすべてですjoin。したがって、最も興味深い部分に移りましょう。

コンセプトモニター

マルチスレッドにはモニターというものがあります。一般に、モニターという言葉はラテン語から「監督者」または「監督者」と翻訳されます。この記事の枠組みの中で本質を覚えていきたいと思いますので、必要な方はリンク先から内容を読んで詳細をご覧ください。Java 言語仕様、つまり JLS:「17.1. 同期」から始めましょう。それは次のように述べています: スレッドで Java を破壊することはできません: パート II - 同期 - 5Java はスレッド間の同期を目的として、「モニター」と呼ばれる特定のメカニズムを使用していることがわかりました。各オブジェクトにはモニターが関連付けられており、スレッドはそれをロックまたはロック解除できます。次に、Oracle の Web サイトにあるトレーニング チュートリアル「 Intrinsic Locks and Synchronization 」を見つけます。このチュートリアルでは、Java の同期が固有ロックまたはモニター ロックと呼ばれる内部エンティティを中心に構築されていることを説明します。多くの場合、このようなロックは単に「モニター」と呼ばれます。また、Java のすべてのオブジェクトには、それに関連付けられた固有のロックがあることもわかります。「Java - 組み込みロックと同期」を読むことができます。次に、Java のオブジェクトをモニターにどのように関連付けることができるかを理解することが重要です。Java の各オブジェクトにはヘッダーがあります。ヘッダーは、プログラマがコードから利用できるものではありませんが、仮想マシンがオブジェクトを正しく処理するために必要な一種の内部メタデータです。オブジェクト ヘッダーには、次のような MarkWord が含まれています。 スレッドで Java を破壊することはできません: パート II - 同期 - 6

https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

ここでは、Habr の記事「マルチスレッドはどのように機能しますか? パート I: 同期」が非常に役立ちます。この記事には、JDK バグテイカー「 JDK-8183909 」のタスク ブロックの概要からの説明を追加する価値があります。「 JEP-8183909 」でも同じことがわかります。したがって、Java では、モニターはオブジェクトに関連付けられており、スレッドはこのスレッドをブロックすることができます。あるいは、「ロックを取得する」とも言います。最も単純な例:
public class HelloWorld{
    public static void main(String []args){
        Object object = new Object();
        synchronized(object) {
            System.out.println("Hello World");
        }
    }
}
したがって、キーワードを使用して、synchronized現在のスレッド (これらのコード行が実行される) は、オブジェクトに関連付けられたモニターを使用してobject、「ロックを取得」または「モニターをキャプチャ」しようとします (2 番目のオプションの方が望ましい)。モニターに競合がない場合 (つまり、同じオブジェクト上で同期を希望する人が他にいない場合)、Java は「バイアス ロック」と呼ばれる最適化の実行を試みることができます。Mark Word のオブジェクトのタイトルには、対応するタグと、モニターがどのスレッドに接続されているかの記録が含まれます。これにより、モニターをキャプチャする際のオーバーヘッドが軽減されます。モニターがすでに別のスレッドに関連付けられている場合、このロックだけでは十分ではありません。JVM は次のロック タイプである基本ロックに切り替わります。比較交換 (CAS) 操作を使用します。同時に、Mark Word のヘッダーには Mark Word 自体が保存されなくなりましたが、基本ロックを使用していることを JVM が理解できるように、そのストレージへのリンクとタグが変更されました。複数のスレッドのモニターに競合がある場合 (1 つはモニターをキャプチャし、2 番目のスレッドはモニターが解放されるのを待っています)、Mark Word のタグが変更され、Mark Word はモニターへの参照を次のように保存し始めます。オブジェクト - JVM の内部エンティティ。JEP に記載されているように、この場合、このエンティティを格納するためのスペースがネイティブ ヒープ メモリ領域に必要です。この内部エンティティの保存場所へのリンクは、Mark Word オブジェクト内にあります。したがって、ご覧のとおり、モニターは実際には、共有リソースへの複数のスレッドのアクセスの同期を確保するためのメカニズムです。JVM が切り替えるこのメカニズムの実装はいくつかあります。したがって、わかりやすくするために、モニターについて話すときは、実際にはロックについて話します。 スレッドで Java を破壊することはできません: パート II - 同期 - 7

同期してロックで待機中

前に見たように、モニターの概念は「同期ブロック」 (クリティカル セクションとも呼ばれます) の概念と密接に関連しています。例を見てみましょう:
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 では、次のようになります。 スレッドで Java を破壊することはできません: パート II - 同期 - 8ご覧のとおり、スレッドがブロックされておりモニターを占有することができないため、JVisualVM のステータスは「モニター」と呼ばれます。コード内でスレッドの状態を確認することもできますが、この状態の名前は JVisualVM の用語と似ていますが、一致しません。この場合、th1.getState()ループはBLOCKEDforを返します。ループの実行中、モニターはスレッドによって占有され、スレッドはブロックされ、ロックが返されるまで作業を続行できません。同期ブロックに加えて、メソッド全体を同期できます。たとえば、クラスのメソッドは次のようになります。 lockmainth1HashTable
public synchronized int size() {
	return count;
}
1 単位時間内に、このメソッドは 1 つのスレッドによってのみ実行されます。でもロックは必要ですよね?はい、必要です。オブジェクト メソッドの場合、ロックは になりますthisこのトピックについては、「同期ブロックの代わりに同期メソッドを使用する利点はありますか? 」という興味深い議論があります。メソッドが静的である場合、ロックはロックされずthis(静的メソッドの場合は が存在できないためthis)、クラス オブジェクト (たとえば、Integer.class) になります。

モニターを見ながら待ちます。Notice メソッドと NotifyAll メソッド

スレッドには別の待機メソッドがあり、モニターに接続されています。sleepや とは異なりjoin、単に呼び出すことはできません。そして彼の名前は ですwait。このメソッドは、waitモニター上で待機するオブジェクト上で実行されます。例を見てみましょう:
public static void main(String []args) throws InterruptedException {
	    Object lock = new Object();
	    // task будет ждать, пока его не оповестят через lock
	    Runnable task = () -> {
	        synchronized(lock) {
	            try {
	                lock.wait();
	            } catch(InterruptedException e) {
	                System.out.println("interrupted");
	            }
	        }
	        // После оповещения нас мы будем ждать, пока сможем взять лок
	        System.out.println("thread");
	    };
	    Thread taskThread = new Thread(task);
	    taskThread.start();
        // Ждём и после этого забираем себе лок, оповещаем и отдаём лок
	    Thread.currentThread().sleep(3000);
	    System.out.println("main");
	    synchronized(lock) {
	        lock.notify();
	    }
}
JVisualVM では、次のようになります。 これがどのように機能するかを理解するには、メソッドがを参照しているスレッドで Java を破壊することはできません: パート II - 同期 - 10ことを覚えておく必要があります。スレッド関連のメソッドが にあるのは奇妙に思えます。しかし、ここに答えがあります。覚えているとおり、Java のすべてのオブジェクトにはヘッダーがあります。ヘッダーには、モニターに関する情報、ロック状態に関するデータなど、さまざまなサービス情報が含まれています。そして、覚えているように、各オブジェクト (つまり、各インスタンス) は、固有ロックと呼ばれる内部 JVM エンティティ (モニターとも呼ばれます) と関連付けられています。上の例では、タスクは、 に関連付けられたモニター上で同期ブロックに入ることを記述しています。このモニターでロックを取得できる場合は、。このタスクを実行するスレッドはモニターを解放しますが、モニター上で通知を待機しているスレッドのキューに参加します。このスレッドのキューは WAIT-SET と呼ばれ、本質をより正確に反映しています。行列というよりはセットですね。スレッドはタスク task で新しいスレッドを作成し、それを開始して 3 秒間待機します。これにより、高い確率で、新しいスレッドがそのスレッドよりも先にロックを取得し、モニター上でキューに入ることが可能になります。その後、スレッド自身が同期ブロックに入り、モニター上でスレッドの通知を行います。通知が送信された後、スレッドはモニターを解放し、モニターが解放されるのを待った後、新しいスレッド (以前待機していた) が実行を継続します。通知を 1 つのスレッドにのみ送信することも ( )、キュー内のすべてのスレッドに一度に送信することもできます ( )。詳細については、「 Java のnotify() とnotifyAll() の違い」を参照してください。通知の順序は JVM 実装に依存することに注意することが重要です。詳細については、「 notify とnotifyall を使用して飢餓を解決する方法? 」を参照してください。オブジェクトを指定せずに同期を実行できます。これは、コードの個別のセクションではなく、メソッド全体が同期される場合に実行できます。たとえば、静的メソッドの場合、ロックはクラス オブジェクトになります ( を介して取得されます)。 waitnotifyjava.lang.ObjectObjectlockwaitlocklockmainmainmainlockmainlocklocknotifynotifyAll.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 になります。 スレッドで Java を破壊することはできません: パート II - 同期 - 11

スレッドのライフサイクル

これまで見てきたように、流れは人生の過程でその状態を変化させます。本質的に、これらの変更はスレッドのライフサイクルです。スレッドが作成されたばかりの場合、ステータスは NEW になります。この位置では、スレッドはまだ開始されておらず、Java スレッド スケジューラは新しいスレッドについてまだ何も認識していません。スレッド スケジューラがスレッドについて認識するには、 を呼び出す必要がありますthread.start()。その後、スレッドは RUNNABLE 状態になります。インターネット上には、実行可能状態と実行中状態が分離されている誤ったスキームが多数存在します。しかし、これは間違いです、なぜなら... Java は、「実行準備完了」ステータスと「実行中」ステータスを区別しません。スレッドは生きているがアクティブではない (実行可能ではない) 場合、次の 2 つの状態のいずれかになります。
  • BLOCKED - 保護されたセクションへの入場を待機しています。つまり、synchonizedブロックへ。
  • WAITING - 条件に基づいて別のスレッドを待ちます。条件が true の場合、スレッド スケジューラはスレッドを開始します。
スレッドが時間に従って待機している場合、そのスレッドは TIMED_WAITING 状態になります。スレッドが実行されなくなった場合 (正常に完了または例外が発生した場合)、スレッドは TERMINATED ステータスになります。スレッドの状態 (その状態) を確認するには、 メソッドが使用されますgetStateisAliveスレッドには、スレッドが終了していない場合に true を返す メソッドもあります。

LockSupport とスレッドパーキング

Java 1.6 以降、 LockSupportと呼ばれる興味深いメカニズムがありました。 スレッドで Java を破壊することはできません: パート II - 同期 - 12このクラスは、「許可」またはそれを使用する各スレッドに許可を関連付けます。許可が利用可能な場合、メソッド呼び出しはpark直ちに戻り、呼び出し中に同じ許可を占有します。それ以外の場合はブロックされます。このメソッドを呼び出すと、unpark許可がまだ利用可能でない場合に利用可能になります。Permit は 1 つだけです。Java API では、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) {
            // Просим разрешение и ждём, пока не получим его
            e.printStackTrace();
        }
        System.out.println("Hello, World!");
    }
}
セマフォの許可が 0 になっているため、このコードは永久に待機します。コード内で呼び出された場合acquire(つまり、許可を要求した場合)、スレッドは許可を受け取るまで待機します。待っているので処理する義務がありますInterruptedException。興味深いことに、セマフォは別のスレッド状態を実装します。JVisualVM を見ると、状態が待機ではなくパークであることがわかります。 スレッドで Java を破壊することはできません: パート II - 同期 - 13別の例を見てみましょう。
public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            //Запаркуем текущий поток
            System.err.println("Will be Parked");
            LockSupport.park();
            // Как только нас распаркуют - начнём действовать
            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 はwaitfromsynchronizedparkfromを区別しますLockSupport。なぜこれがそれほど重要なのでしょうかLockSupport? 再び Java API に戻って、スレッド状態 WAITINGを見てみましょう。ご覧のとおり、それに入る方法は 3 つしかありません。2 つの方法 - これwaitjoin。そして3つ目は ですLockSupport。Java のロックは同じ原則に基づいて構築されておりLockSupport、より高レベルのツールを表します。一つ使ってみましょう。たとえば、次を見てみましょう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誰かがリソースを解放するのを待ちます。JVisualVM を見ると、mainスレッドがロックを与えるまで新しいスレッドがパークされることがわかります。ロックの詳細については、「Java 8 でのマルチスレッド プログラミング。パート 2。変更可能なオブジェクトへのアクセスの同期」および「Java ロック API。理論と使用例」を参照してください。ロックの実装をよりよく理解するには、「 Phaser クラス」の概要で Phazer について読むと役立ちます。また、さまざまなシンクロナイザーについては、Habré の記事「Java.util.concurrent.* Synchronizers Reference」を必ずお読みください。

合計

このレビューでは、Java でスレッドが対話する主な方法を検討しました。追加資料: #ヴィアチェスラフ
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION