Java multi-threaded programming (5) - Inter-thread communication

A. Waiting and notification

  In some cases, the procedure needs to be performed to meet certain conditions (hereinafter unified call it protective conditions) to perform. In a single-threaded programming, we can use polling way to achieve that frequently determine whether conditions meet the protection, if it continues to meet the judge, if the meet is started. But in multi-threaded programming, this is undoubtedly a very inefficient way. If a thread is ongoing to determine meaningless without releasing CPU, which will result in waste of resources; and if the timing to judge, to satisfy the protection conditions on the release of CPU, will cause frequent context switching. In short, not the recommended way to use polling in multithreaded programming.
  Waiting and notification is a mechanism: when the protective conditions are not met, the current thread may be suspended; and when the protective conditions are met, then wake up this thread. A thread for its protection conditions are not satisfied and the suspended process is called to wait a thread so that the protective conditions when other threads to process to meet the wake of those threads are suspended is called a notice.

1.wait

  In the Java platform, Object.wait method can be used to achieve wait, here are three overloaded method wait method:

  • void wait (long timeoutMillis)
    call the method will thread into the TIMED_WAITING state, when the end of the waiting time or another thread calls notify or notifyAll method of that object will wake up the thread.
  • void wait (long timeoutMillis, int nanos )
    This method appears to be accurate to the nanosecond level, but not really. If the value is between 0 ~ nanos 999999, give timeoutMillis plus 1, then calls wait (timeoutMillis).
  • void wait ()
    which corresponds to the wait (0), i.e. never expires. After calling the current thread will enter WAITING state until another thread calls notify or notifyAll method of that object.

  First introduced by a diagram to realize the wait mechanism:

  In the previous article, we learned that JVM maintains a set of entry (Entry Set) for each object is used to store application thread inside the object lock. Furthermore, the JVM will be referred to as a set of waiting queue (Wait Set) is maintained for each object, the thread waiting on the object store for the queue. When the wait method is called an object in the thread (here we call the object A), the current thread releases the lock and enter inside or TIMED_WAITING WAITING state, and then into the wait set. When another thread invokes the notify method object A, a thread will wait for centralized wake up and out of the waiting set. This thread may immediately get inside the lock, but also may be due to the failure of competition within the lock into the inlet header, until the internal lock. After re-acquire the lock inside, wait method will return the current thread continues to execute the code behind.
  Because the wait method will release the internal locks, so the wait process will determine whether the current thread holds an internal lock is called the wait method of the object. If the current thread does not hold internal lock of the object, JVM will throw a IllegalMonitorStateException exception. Thus, the current thread wait method must hold internal lock of the object in the call, i.e., call wait method must be guided by the object placed synchronized sync block. In summary, the method implemented using wait waiting code templates following pseudocode:

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

  Here's why instead of using a while if that notification thread may just update the shared variable protection conditions, but will not necessarily protect the conditions are met; even notice the thread can guarantee protection conditions are met, but the thread from the wait set into the entrance within the set and then get into the interior of the lock during this time, other threads may still lead to update the shared variable protection conditions are not met. Although the thread because the condition is not satisfied and protection methods into the wait, but the wait method of return does not mean protection condition has been established. Therefore, after the method returns need to wait again to determine if the conditions are met to protect the next operation is performed, or should continue to enter the wait method. It is based on this consideration, we should wait to call the method in a while loop instead if judgment.

2.notify/notifyAll

  The figure is the notify mechanism implemented:

  and wait the same method, the notify method must hold the internal lock of the object, when executed, otherwise IllegalMonitorStateException exception is thrown, it must be placed so notify method also guided by the target sync block synchronized . The method of any notify a thread will wait set out queue. And wait method is different, notify method itself does not release the internal locks, but the implementation is automatically released after the completion of the critical section of code. Therefore, in order to wait for the thread to get inside the lock after it wakes up as soon as possible, it should notify local calls placed close to the end of the critical zone as much as possible.
  The wake-up call to notify method corresponding thread is a thread waiting on any subject, but this thread to be awakened may not be what we really want to wake up that thread. So, sometimes we need to use notifyAll, and notify its methods only difference is that it can wake up all waiting threads on the corresponding object.

3. premature wake-up problem

  N is assumed that the notification thread waiting threads W1 and W2 and synchronization on the object obj, W1 and W2 protect the conditions C1 and C2 are dependent on the state obj instance variables, but the determination of the content C1 and C2 are not the same. C1 and C2 are not established in the initial state. At some point, when the thread N updates the shared variable state to be established so that the protective conditions C1, W1 at this time to wake up and executed obj.notifyAll () method (call obj.notify () does not necessarily wake W1). Because notifyAll wake up all waiting threads on obj, W2 will thus be awakened, W2 protection even if the condition is not satisfied. This makes the W2 need to continue to wait after being awakened. This waiting thread under the protection conditions are not set up to be awakened phenomenon known as premature wake. Wake up early so that those who do not need to wait for the thread to be awakened also awakened, resulting in a waste of resources. Early wake-up problem can use the Condition interface in the following section to resolve.

II. Condition variables Condition

  总的来说,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号到达终点!
比赛结束!

五.总结

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

Guess you like

Origin www.cnblogs.com/maconn/p/11960079.html