Javaのマルチスレッドプログラミング(5) - スレッド間通信

A.待機と通知

  いくつかのケースでは、手順が実行する特定の条件(以下、統一された呼び出しそれ保護条件)を満たすように実行される必要があります。シングルスレッドプログラミングでは、私たちは頻繁に会うが開始された場合には、裁判官を満たすために続けた場合の条件は、保護を満たしているかどうかを判断することを達成するためのポーリング方法を使用することができます。しかし、マルチスレッドプログラミングでは、これは間違いなく非常に非効率的な方法です。スレッドは、資源の無駄になりますCPUを解放せずに無意味を決定するために継続している場合は、裁判官へのタイミングは、CPUのリリースに保護条件を満足する場合、頻繁にコンテキストスイッチが発生します。要するに、マルチスレッドプログラミングで使用するポーリングしないことをお勧めの方法。
  待機通知するメカニズムである:保護条件が満たされない場合、現在のスレッドは中断されてもよい。保護条件が満たされた場合、このスレッドを覚まします。その保護の条件のためのスレッドが満たされていないと、一時停止プロセスは、保護条件がときに、他のスレッドが通知を呼ばれて中断されているこれらのスレッドをきっかけに適合するように処理するようにスレッドを待つために呼び出されます。

1.wait

  Javaプラットフォームでは、Object.waitメソッドで待機を達成するために使用することができ、ここでは3つのオーバーロードされたメソッドのwaitメソッドです。

  • 無効待機(ロングtimeoutMillis)
    メソッドは、待機時間の終了または別のスレッドの呼び出しが通知したり、そのオブジェクトののnotifyAllメソッドは、スレッドを覚ますだろうと、TIMED_WAITING状態にスレッドう呼び出します。
  • 空待ち時間(長いtimeoutMillis、int型またはnanos )
    このメソッドは、ナノ秒レベルに正確であるように見えますが、そうでもありません。値が0〜nanos値999999の間である場合は、timeoutMillisを与えるプラス1、そして(timeoutMillis)待機を呼び出します。
  • 無効待機()
    待ち(0)に対応し、すなわち、期限切れになることはありません。別のスレッドの呼び出しを通知したり、そのオブジェクトのメソッドのnotifyAllまで、現在のスレッドを呼び出した後、待ち状態になります。

  最初は待機メカニズムを実現するための図で紹介:

  前回の記事では、我々は、JVMは、オブジェクトロック内ストアアプリケーションのスレッドに使用される各オブジェクトのエントリ(エントリーセット)のセットを維持していることを学びました。さらに、JVMは、待ちキューの集合と呼ぶことにする(セットを待って)スレッドがキューのオブジェクトストアを待って、各オブジェクトに対して維持されます。ときwaitメソッドは、現在のスレッドのリリースは、ロック、(ここでは、オブジェクトAを呼び出す)スレッドでオブジェクトと呼ばれ、内部またはTIMED_WAITING待ち状態に入り、その後、待機セットにされます。別のスレッドがメソッドオブジェクトAを通知呼び出すと、スレッドは、集中ウェイクアップのためにと、待機セットの外に待機します。このスレッドは、即座にロック中に入ることができるだけでなく、内部ロックするまで、入口ヘッダにロック内競争の故障に起因し得ます。再取得ロック内部後、待機メソッドは、現在のスレッドがコードの後ろを実行し続けて戻ります。
  待ち方法は、内部ロックを解除するので、それほどウェイト処理は、現在のスレッドがオブジェクトの待機方法と呼ばれる内部ロックを保持しているかどうかを決定します。現在のスレッドがオブジェクトの内部ロックを保持していない場合は、JVMはないIllegalMonitorStateException例外がスローされます。従って、現在のスレッドの待機方法を呼び出し、オブジェクトの内部ロックを保持しなければならない、即ち、呼待ち方法は、同期シンクブロックを置かれた物体によって導かれなければなりません。要約すると、この方法は、擬似コード以下のコードテンプレートを待っている待機を使用して実装しました:

synchronized(someObject) {
    while(!someCondition) {
        someObject.wait();
    }
    doSomething();
}

  ここでは、なぜ、代わりにその通知スレッドがちょうど共有変数の保護状態を更新する可能性がある場合しばらく使用してのですが、必ずしも条件が満たされた保護されません。でも、保護条件が満たされている保証できるスレッドに気づくが、入り口に待機セットからスレッドそして、この時間の間、ロックの内部に入ると、セット内で、他のスレッドがまだ共有変数保護条件が満たされていない更新する可能性があります。スレッドが、条件が待機に満足し、保護方法ではなく、リターンの待機方法は、平均保護条件が確立されていません。ので、したがって、この方法の後に戻って、条件は次の操作が実行され、またはwaitメソッドを入力し続けなければならない保護するために満たされているかどうかを判断するために、再び待つ必要があります。それは、この考えに基づいており、我々は判断ならば代わりにwhileループでメソッドを呼び出すために待機しなければなりません。

2.notify /のnotifyAll

  図は、機構が実装通知される:

  と同じ方法を待って、そうでない場合は、IllegalMonitorStateException例外がスローされ、そうも同期目標の同期ブロックにより案内方法を通知配置する必要があり、実行されると方法は、物体の内部ロックを保持しなければならない通知します。いずれかの方法は、スレッドが定めキューをお待ちしております通知します。そして、waitメソッドは、メソッド自体が内部ロックを解放しません通知し、異なっているが、実装は自動的にコードのクリティカルセクションが完了した後に放出されます。したがって、それは可能な限り早く起床した後、ロックの中に入るために、スレッドを待つために、それは可能な限りの重要なゾーンの端部近傍に配置されたローカル電話を通知しなければなりません。
  メソッド対応するスレッドを通知するウェイクアップコールは、任意の被写体に待機しているスレッドですが、目覚めたことには、このスレッドは、私たちは本当にそのスレッドをウェイクアップしたいものではないかもしれません。だから、時には我々はのnotifyAllを使用し、唯一の違いは、それが対応するオブジェクト上で待機中のスレッドをすべて目を覚ますことができるということですそのメソッドを通知する必要があります。

3.未熟ウェイクアップ問題

  Nは、オブジェクトobjにスレッドW1とW2との同期待ちの通知スレッドは、W1及びW2は、C1とC2は、状態OBJインスタンス変数に依存する条件を守ることが想定されるが、コンテンツC1及びC2の決意は同じではありません。C1とC2は、初期状態では確立されていません。スレッドNは、共有変数の状態を更新するときにいくつかの点で、このときの保護条件C1、W1は、ウェイクアップするように確立さobj.notifyAll()メソッドを((obj.notify呼び出し)必ずしもW1をウェイクしない)を実行します。notifyAllがobj上のすべての待機しているスレッドを覚ますので、W2は、このように条件を満たしていない場合でも、W2保護、目覚めされます。これは、目覚めた後に待機し続けることW2の必要になります。保護条件下でこの待機しているスレッドは、早期覚醒と呼ばれる現象を起こされるようにセットアップされていません。早くそうそうでない人も、資源の無駄が生じ、目覚め目覚めなければスレッドを待つ必要があることが目を覚まします。早期ウェイクアップの問題が解決するには、次のセクションの条件インターフェースを使用することができます。

II。条件変数条件

  总的来说,Object.wait()/notify()过于底层,且Object.wait(long timeout)还存在过早唤醒和无法区分其返回是由于等待超时还是被通知线程唤醒的问题。不过,了解wait/notify有助于我们阅读部分源码,以及学习和使用Condition接口。
  Condition接口可以作为wait/notify的替代品来实现等待/通知,它为解决过早唤醒问题提供了支持,并解决了Object.wait(long timeout)无法区分其返回是由于等待超时还是被通知线程唤醒的问题。Condition接口中定义了以下方法:

  在上一篇文章中,我们在介绍Lock接口时曾经提到过它的newCondition方法,它返回的就是一个Condition实例。类似于Object.wait()/notify()要求其执行线程必须持有这些方法所属对象的内部锁,Condition.await()/signal()也要求其执行线程持有创建该Condition实例的显式锁。每个Condition实例内部都维护了一个用于存储等待线程的队列。设condition1和condition2是从一个显式锁上获取的两个不同的Condition实例,一个线程执行condition1.await()会导致其被暂停并进入condition1的等待队列。condition1.signal()会使condition1的等待队列中的一个任意线程被唤醒,而condition1.signaAll()则会使condition1的等待队列中的所有线程被唤醒,而condition2的等待队列中的线程则不受影响。
  和wait/notify类似,await/signal的使用方法如下:

public class ConditionUsage {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void waitMethod() throws InterruptedException {
        lock.lock();
        try {
            while (保护条件不成立) {
                condition.await();
            }
            // 业务逻辑
        } finally {
            lock.unlock();
        }
    }
    
    public void notifyMethod() {
        lock.unlock();
        try {
            // 更新共享变量
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

  最后,以一个例子来结束本小节。这里我们以经典的生产者-消费者模型来举例。假设有一个生产整数的生产者,一个消费奇数的消费者和一个消费偶数的消费者。当生产奇数时,生产者会通知奇数消费者,偶数同理。下面是完整代码:


展开查看


import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
    private final Lock lock = new ReentrantLock();
    private final Condition oddCondition = lock.newCondition();
    private final Condition evenCondition = lock.newCondition();
    private final Random random = new Random();
    private volatile Integer message;
    private AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        ConditionDemo demo = new ConditionDemo();
        Thread producer = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                demo.produce();
            }
        });
        producer.start();
        Thread oddConsumer = new Thread(() -> {
            while (true) {
                try {
                    demo.consumeOdd();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread evenConsumer = new Thread(() -> {
            while (true) {
                try {
                    demo.consumeEven();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        oddConsumer.start();
        evenConsumer.start();
    }

    public void produce() {
        lock.lock();
        if (message == null) {
            message = random.nextInt(100) + 1;
            count.incrementAndGet();
            if (message % 2 == 0) {
                evenCondition.signal();
                System.out.println("Produce even : " + message);
            } else {
                oddCondition.signal();
                System.out.println("Produce odd : " + message);
            }
        }
        lock.unlock();
    }

    public void consumeOdd() throws InterruptedException {
        lock.lock();
        while (message == null) {
            oddCondition.await();
        }
        System.out.println("Consume odd : " + message);
        message = null;
        lock.unlock();
    }

    public void consumeEven() throws InterruptedException {
        lock.lock();
        while (message == null) {
            evenCondition.await();
        }
        System.out.println("Consume even : " + message);
        message = null;
        lock.unlock();
    }
}

  该程序的输出如下:

Produce even : 34
Consume even : 34
Produce odd : 43
Consume odd : 43
Produce even : 28
Consume even : 28
Produce odd : 27
Consume odd : 27
Produce even : 92
Consume even : 92
...

三.倒数计数器CountDownLatch

  有时候,我们希望一个线程在另一个或多个线程结束之后再继续执行,这时候我们最先想到的肯定是Thread.join()。有时我们又希望一个线程不一定需要其他线程结束,而只是等其他线程执行完特定的操作就继续执行。这种情况下无法使用Thread.join(),因为它会导致当前线程等待其他线程完全结束。当然,此时可以用共享变量来实现。不过,Java为我们提供了更加方便的工具类来解决上面说的这些情况,那就是CountDownLatch。
  可以将CountDownLatch理解为一个可以在多个线程之间使用的计数器。这个类提供了以下方法:

  CountDownLatch内部也维护了一个用于存放等待线程的队列。当计数器不为0时,调用await方法的线程会被暂停并进入该队列。当某个线程调用countDown方法的时候,计数器会减1。当计数器到0的时候,等待队列中的所有线程都会被唤醒。计数器的初始值是在CountDownLatch的构造方法中指定的:

public CountDownLatch(int count)

  当计数器的值达到0之后就不会再变化。此时,调用countDown方法并不会导致异常的抛出,并且后续执行await方法的线程也不会被暂停。因此,CountDownLatch的使用是一次性的。此外,由于CountDownLatch是线程安全的,因此在调用await、countDown方法时无需加锁。
  下面的例子中,主线程等待两个子线程结束之后再继续执行。这里使用了CountDownLatch来实现:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(2);
        Runnable task = () -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " finished.");
            latch.countDown();
        };
        new Thread(task, "Thread 1").start();
        new Thread(task, "Thread 2").start();
        try {
            latch.await();
        } catch (InterruptedException e) {
            return;
        }
        System.out.println("Main thread continued.");
    }
}

  该程序输出如下:

Thread 2 finished.
Thread 1 finished.
Main thread continued.

  可以看到,当线程1和线程2执行完成后,主线程才开始继续执行。
  如果CountDownLatch内部计数器由于程序的错误而永远无法达到0,那么相应实例上的等待线程会一直处于WAITING状态。避免该问题的出现有两种方法:一是确保所有对countDown方法的调用都位于代码中正确的位置,例如放在finally块中。二是使用带有时间限制的await方法。如果在规定时间内计时器值未达到0,该CountDownLatch实例上的等待线程也会被唤醒。该方法的返回值可以用于区分其返回是否是由于等待超时。

四.循环屏障CyclicBarrier

  有时候多个线程可能需要互相等待对方执行到代码中的某个地方才能继续执行。这就类似于我们在开会的时候必须等待所有与会人员都到场之后才能开始。Java中为我们提供了一个工具类CyclicBarrier,该类可以用来实现这种等待。
  使用CyclicBarrier实现等待的线程被称为参与方(Party)。参与方只需要CyclicBarrier.await()就可以实现等待。和CountDownLatch类似,CyclicBarrier也有一个计数器。当最后一个线程调用CyclicBarrier.await()时,之前的等待线程都会被唤醒,而最后一个线程本身并不会被暂停。和CountDownLatch不同的是,CyclicBarrier是可以重复使用的,这也是为什么它的类名中含有Cyclic。当所有参与方被唤醒的时候,任何线程再次执行await方法又会导致该线程被暂停。
  CyclicBarrier提供了两个构造器:

public CyclicBarrier​(int parties)
public CyclicBarrier​(int parties, Runnable barrierAction)

  可以看到,在构造CyclicBarrier​时,必须提供参与方的数量。第二个构造器还允许我们指定一个被称为barrierAction的任务(Runnable接口实例),该任务会被最后一个执行await方法的线程执行。因此,如果有需要在唤醒所有线程前执行的操作,可以使用这个构造器。
  CyclicBarrier提供了以下6个方法:

1.public int await() throws InterruptedException,BrokenBarrierException

  如果当前线程不是最后一个参与方,那么该线程在调用await()后将持续等待直到以下情况发生:

  • 最后一个线程到达;
  • 当前线程被中断;
  • 其他正在等待的线程被中断;
  • 其他线程等待超时;
  • 其他线程调用了当前屏障的reset()。

  如果当前线程在进入await()方法使已经被标记中断状态或在等待时被中断,那么await()将会抛出InterruptedException并清除当前线程的中断状态。
  如果屏障在参与方等待时被重置或被破坏,或者在调用await()时屏障已经被破坏,那么await()将会抛出BrokenBarrierException。
  如果某个线程在等待时被中断,那么其他等待线程将会抛出BrokenBarrierException并且屏障也会被标记为broken状态。
  该方法的返回值表示当前线程的到达索引,getParties()-1表示第一个到达,0表示最后一个到达。

2.public int await​(long timeout,TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException

  该方法与相当于有时间限制的await(),等待时间结束之后该线程将会抛出TimeOutException,屏障会被标记为broken状态,其他正在等待的线程则会抛出BrokenBarrierException。

3.public int getNumberWaiting()

  返回当前正在等待的参与方的数量。

4.public int getParties()

  返回总的参与方的数量。

5.public boolean isBroken()

  如果该屏障已经被破坏则返回true,否则返回false。当等待线程超时或被中断,或者在执行barrierAction时出现异常,屏障将会被破坏。

6.public void reset()

  将屏障恢复到初始状态,如果有正在等待的线程,这些线程会抛出BrokenBarrierException异常。

  下面我们通过一个例子来学习如何使用CyclicBarrier。假设现在正在举行短跑比赛,共有8名参赛选手,而场地上只有4条赛道,因此需要分为两场比赛。每场比赛必须等4名选手全都就绪才可以开始,而上一场比赛结束之后即全部选手离开赛道之后才能进行下一场比赛。该示例代码如下所示:


展开查看


import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;

public class CyclicBarrierDemo {
    private CyclicBarrier startBarrier = new CyclicBarrier(4, () -> System.out.println("比赛开始!"));
    private CyclicBarrier shiftBarrier = new CyclicBarrier(4, () -> System.out.println("比赛结束!"));
    private Runner[] runners = new Runner[8];
    private AtomicInteger next = new AtomicInteger(0);

    CyclicBarrierDemo() {
        for (int i = 0; i < 8; i++) {
            runners[i] = new Runner(i / 4 + 1, i % 4 + 1);
        }
    }

    public static void main(String[] args) {
        CyclicBarrierDemo demo = new CyclicBarrierDemo();
        for (int i = 0; i < 4; i++) {
            demo.new Track().start();
        }
    }

    private class Track extends Thread {
        private Random random = new Random();

        @Override
        public void run() {
            for (int i = 0; i < 2; i++) {
                try {
                    Runner runner = runners[next.getAndIncrement()];
                    System.out.println(runner.getGroup() + "组" + runner.getNumber() + "号准备就绪!");
                    startBarrier.await();
                    System.out.println(runner.getGroup() + "组" + runner.getNumber() + "号出发!");
                    Thread.sleep((random.nextInt(5) + 1) * 1000);
                    System.out.println(runner.getGroup() + "组" + runner.getNumber() + "号到达终点!");
                    shiftBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class Runner {
        private int group;
        private int number;

        Runner(int group, int number) {
            this.group = group;
            this.number = number;
        }

        int getGroup() {
            return group;
        }

        int getNumber() {
            return number;
        }
    }
}

  该程序输出如下:


展开查看


1组4号准备就绪!
1组2号准备就绪!
1组3号准备就绪!
1组1号准备就绪!
比赛开始!
1组4号出发!
1组2号出发!
1组1号出发!
1组3号出发!
1组3号到达终点!
1组2号到达终点!
1组4号到达终点!
1组1号到达终点!
比赛结束!
2组1号准备就绪!
2组2号准备就绪!
2组3号准备就绪!
2组4号准备就绪!
比赛开始!
2组4号出发!
2组1号出发!
2组3号出发!
2组2号出发!
2组1号到达终点!
2组4号到达终点!
2组3号到达终点!
2组2号到达终点!
比赛结束!

五.总结

  实际上,线程间的通信方式远不止上面介绍的这些,还有很多手段可以在线程间传递信息,例如阻塞队列、信号量、线程中断机制等,我们将会在之后的文章中进一步学习这部分内容。

おすすめ

転載: www.cnblogs.com/maconn/p/11960079.html