マルチスレッドプログラミング:wait、notify、join、yieldは何の役に立ちますか?


マルチスレッドは開発知識の中で重要な部分ですが、実際の生産では、マルチスレッドプログラミングの複雑な詳細と問題を自分で処理する必要があることはめったにありません.多くの場合、「アーキテクチャ」や「フレームワーク」があり、多くの分業プログラマーがマルチスレッドの詳細を隠しているため、多くの場合、さまざまなビジネスロジックを簡単に実現すればよいだけです.
今日はwait,notify,join,yieldの4つの方法の役割を整理します.
この4つの方法は,wait,notifyともにObjectの方法であり,joinはThreadのインスタンス法であり,yieldはThreadの静的方法である.
wait,notifyは前の文章:xxxxで述べたように、waitはスレッドをWaiting状態に変換し、notifyはWaiting状態のスレッドを呼び覚ます.
私たちは一人一人に話しましょう.
Object.wait
ドキュメントには次のように記述されています.
Causes the current thread to wait until either another thread invokes the Object#notify() method or the Object#notifyAll() method for this object, or a specified amount of time has elapsed.
これは、別のスレッドがnotifyメソッドまたはnotifyAllメソッドを呼び出して起動するか、待機時間を指定するまで、現在のスレッドがwaitingに入るようにします.
すなわちwaitのパラメータ付きリロード方法wait(long)では,スレッドを最大一定の時間待つことができ,この時間が過ぎるとスレッドは自らrunnable状態に戻る.
正しい使い方はsynchronizedで使用し、ループを使って包みます.
synchronized (lock) {
    while (!condition) {
        lock.wait() //    waiting   ,                
    }
}

なぜループを使うのですか?通常、論理的な要求がある条件に達する前に、私のこのスレッドは仕事をしないので、条件が満たされた後、他のスレッドが私に知らせてくれて、他のスレッドが私に知らせてくれた後、私はまたこの条件が満たされているかどうかをチェックして、不満があれば、waiting状態に入り続けなければならないので、論理的には完備しています.
Object.notify
Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object's monitor by calling one of the {@code wait} methods.
waiting状態のスレッドを呼び覚ますには、このスレッドは、同じロックでwaiting状態に入る必要があります.
したがって、notifyメソッドの一般的な使用方法は、
synchronized (lock) {
    lock.notify()
}

waitとnotifyはsynchronizedで使わないとどうなるかと聞かれるかもしれません.私もこの点を知りたくて、それから実験して、異常を投げることを発見して、運行時に直接間違いを報告します
java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)

Thread.join
joinメソッドは、ドキュメントの定義を参照するインスタンスメソッドです.
//Waits for this thread to die.
public final void join() throws InterruptedException

つまり、threadA.join()のスレッドを呼び出し、スレッドthreadAが実行されるまでwaiting状態にします.たとえば
public static void main() {
       Thread t1 = new Thread(…);
       t1.join();
       //         t1      ,    
}

Thread.yield
A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore
this hint. Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilize a CPU.
Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect.
public static native void yield();
この方法は,現在のスレッドがcpuの使用を放棄し,cpuを他のスレッドに譲ることをスケジューラに伝えることを意味する.
人の話では、「あら、私はもうそんなに長い間動いていますから、他の人にチャンスを残してください.cpu、早く他のスレッドを実行してください.ちょっと休みます.」
しかし、ドキュメントの説明によると、これはスケジューラに対する暗示にすぎません.つまり、具体的に何が起こるかは、スケジューラがどのように処理されているかによって決まります.
だから私はまた需要を捏造しに来ました.まず、次のコードで何が起こるか見てみましょう.
public static void main(String[] arg) {
    
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }
    });
    
    t1.start();
    t2.start();
    
}

2つのスレッドは一緒にcountに10000回増加し、ロックされていないため、自己増加も原子操作ではないため、2つのスレッドはいずれも10000回増加し、最後のcountの結果は、20000未満の数になるに違いない.
待って!ちょっと待って!自増は原子操作ではなくどういうことなのか、このコードは1行しかないのではないでしょうか.
コードは最終的に命令に翻訳され、cpuによって実行されることはよく知られています.1つの命令は原子ですが、1行のコードが複数の命令に翻訳されると、複数のスレッドが交互に行われます.これはマルチスレッドプログラミングでよくある問題です.
自己増加コードはideaのツールで表示できます.
GETSTATIC thread/TestThreadFunction.count : I
ICONST_1
IADD
PUTSTATIC thread/TestThreadFunction.count : I

4つの実行に分割されて実行されることがわかります.
このコードの実行結果は,最後の結果が20000未満である.
では、私たちは今設計して、私は上述の方法を通じて、2つのスレッドを交互に実行することを望んで、このように安定して20000に増加することができますか?
具体的には、次のコードを見てください.
public static int count = 0;

public static final Object object = new Object();

public static void main(String[] arg) {

    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10000; i++) {
                    synchronized (object) {
                        object.notify();
                        object.wait();
                    }
                    count++;
                    System.out.println("t1 " + count);
                }
                synchronized (object) {
                    object.notify();
                }
            } catch (Throwable e) {

            }
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10000; i++) {
                    synchronized (object) {
                        object.notify();
                        object.wait();
                    }
                    count++;
                    System.out.println("t2 " + count);
                }
                synchronized (object) {
                    object.notify();
                }
            } catch (Throwable e) {

            }
        }
    });

    t1.start();
    t2.start();

    System.out.println("count: " + count);

}

まず最初のスレッド(t 1)が同期ロックobjectに入り、notifyメソッドを呼び出し、他のスレッドに動作を通知するが、このとき何の役にも立たず、次にwaitを呼び出し、自分をwaiting状態にする.
次に、2番目のスレッド(t 2)は自然に動作し、notifyメソッドを呼び出し、起動をトリガし、waitメソッドを呼び出してもwaiting状態に入る.
t 1はnotifyの覚醒を受け,臨界領域を脱退しcountに自己増加を開始し,今回のサイクル終了,再notify,wait後にwaiting状態に入る.
t 2はこのnotifyに呼び覚まされ、countに自己増加を開始し、今回のサイクルは終了し、その後同じプロセスを繰り返す.
……
このようにして,2つのスレッドが交互に実行される.
最終的には
count: 0
t1 1
t2 2
t1 3
t2 4
t1 5
t2 6
t1 7
t2 8
t1 9
t2 10
t1 11
... //     
t2 19998
t1 19999
t2 20000

メインスレッドの一番後ろの出力が、先に実行され、0が出力されたという問題を発見しました.これはどういうことですか.
これは,2つの作業スレッドがまだ作業を開始していないため,メインスレッドが実行済みである.では、2つのスレッドが実行された後、メインスレッドが結果を出力することを望んでいますが、これはできますか?
スレッドの作業が完了したら、実行を続けます.
これがjoinの役割ではないでしょうか.
そこで私たちのコードはstartの2つのスレッドの後にjoinを加えて出力することができます.
...//      ,  
t1.start();
t2.start();

try {
    t1.join();
    t2.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

System.out.println("count: " + count);

このような実行結果は,メインスレッドの出力が最後になる.
... //   
t1 19997
t2 19998
t1 19999
t2 20000
count: 20000

次に、Thread.yieldの実際の役割について検討します.
まずコードを以下の簡単なsynchronizedキーワードで同期する書き方に書き換えます
Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            synchronized (object) {
                count++;
                System.out.println("t111111 " + count);
            }
        }
    }
});

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            synchronized (object) {
                count++;
                System.out.println("t2 " + count);
            }
        }
    }
});

コードの出力により,スレッドのスケジューリングが非常に緊密であること,すなわち,t 1が常に一定時間実行され,t 2がさらに緊密に実行されることが観察された.
... //   
t111111 153
t111111 154
t111111 155
t111111 156
t111111 157
t111111 158
t111111 159
t111111 160
t111111 161
t111111 162
t111111 163
t111111 164
t111111 165
t111111 166
t2 167
t2 168
t2 169
t2 170
t2 171
t2 172
t2 173
t2 174
t2 175
t2 176
t2 177
t2 178
t2 179
t2 180
t2 181
t2 182
... //   

t 1は166回連続して実行され、t 2の番になった.t 2が実行を開始すると、cpuはしばらくプリエンプトされる.
Thread.yieldメソッドを追加してみましょう
for (int i = 0; i < 1000; i++) {
    synchronized (object) {
        count++;
        System.out.println("t2 " + count);
    }
    Thread.yield(); //     
}

大まかに見ると,スレッドによるcpuの占有は,より謙虚になった.
t111111 1
t2 2
t2 3
t2 4
t111111 5
t2 6
t2 7
t2 8
t111111 9
t111111 10
t2 11
t2 12
t111111 13
t111111 14
t111111 15
t2 16
t2 17
t2 18
t111111 19
t111111 20
t2 21
t111111 22
t2 23
t2 24
t111111 25
t2 26
t111111 27
t2 28
t111111 29
t111111 30
t2 31
t2 32
... //   

まとめ
Object.waitはスレッドをwait状態にし,他のスレッドを起動させる必要があるか,あるいは時間長のwaitメソッドを入力し,受信を待つ時間が長くなると自動的に起動する.
Object.notifyはいずれかの待機状態に入ったスレッドを通知し、notifyAllはすべてを通知する.
Thread.joinは、joinのスレッドが完全に実行されるまで、呼び出しスレッドをこの方法にブロックします.
Thread.yieldはスケジューラに通知し、cpuの占有を自発的に譲る.
もしあなたがこの文章が好きならば、評論を歓迎してもっと多くの干物の内容を賞賛して、私の公衆番号に注目することを歓迎します:私たちの君を好奇心があります