Lock中的Condition条件对象使用案例。


文章目录

  • Condition
  • Condition接口的方法
  • await
  • awaitUninterruptibly
  • awaitNanos(long nanosTimeout)
  • await(long time, TimeUnit unit)
  • awaitUntil(Date deadline)
  • signal
  • signalAll
  • Condition使用案例


Condition

通过之前的学习,我们知道可以通过synchronized关键字结合Object类的wait/notify/notifyAll方法来实现多个线程之间的协调与通信,整个过程的底层逻辑都是由JVM实现的,固化在底层C++代码里,开发者无需干预也无法干预。

从JDK1.5开始,Java的并发包提供了Lock、Condition来实现多个线程之间的协调与通信,整个过程都是由开发者编码来控制的,相比传统方式更加灵活和强大。

如果说Lock是用来替代synchronized的,那么Condition就是用来替代Object类的监视器相关方法的。

Condition接口提供了类似于Object类中用于进行线程间通信的wait/notify/notifyAll方法的替代方法await/signal/signalAll。

Lock接口中提供了一个newCondition()方法返回一个Condition对象实例。

下面我们就来介绍Condition相关的知识。

Condition将Object监视器方法(wait /notify和notifyAll )分解,使其能作用在不同的条件对象上,达到每个锁对象具有多个等待集的效果。

调用条件对象Condition的await/signal/signalAll方法时,必须要先获得与这些条件对象关联的锁。因此本质上这些条件对象都要与一个Lock绑定。

条件(也称为条件队列或条件变量)为一个线程提供了一种挂起(等待)的方式,直到另一个线程通知某个条件现在已经为true。 因为对这个共享状态信息的访问发生在不同的线程中,它必须能够被安全地并发访问,所以条件(Condition)总是和某个锁(Lock)相关联。

Condition实例只是普通的Java对象,它们本身也可以用作synchronized语句中的目标对象。 获取Condition实例的监视器锁(monitor)与调用该实例的任何监视器相关方法(await/signal/signalAll)没有特定的关系。 建议不要在synchronized方法或者代码块中使用Condition实例作为锁定的对象,以避免混淆。

Condition接口的方法
public interface Condition {

    void await() throws InterruptedException;

    void awaitUninterruptibly();

    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();
}
await

使调用该方法的线程等待,直到有其它线程调用了该条件对象的signal方法或者线程被中断了。

与此Condition对象相关的Lock锁将会被原子地释放掉,当前线程会暂停调度,进入休眠状态。直到下列事件发生:

1)其它线程调用了该Condition对象的signal方法,且当前线程恰好被选中唤醒

2)其它线程调用了该Condition对象的signalAll方法

3)其它线程中断了当前线程,且支持线程挂起中断

4)发生"虚假"唤醒

在所有情况下,在await方法可以返回当前线程之前,必须重新获取与此Condition条件关联的锁。 当线程返回时,它必须保证持有这个锁。

注意,调用Condition对象的await方法时,必须保证先获得此Condition关联的锁。否则会抛异常。这些特性和Object的wait方法类似。

Tip:sleep()方法和await()方法的区别

Thread.sleep方法和Condition.await(或者Object.wait)方法的区别:前者不会释放锁,而后者会释放锁。

并且在调用Condition.signal方法后等待线程还需要重新获得锁才能继续执行。该行为和Object.wait/notify一致。

awaitUninterruptibly

此方法和await的区别在于不能响应中断,即在等待锁的过程中不会被其它线程中断。其它特性和await方法基本一致。

awaitNanos(long nanosTimeout)

此方法和await方法的区别在于可以指定一个纳秒级的等待时间。除了那4种事件外,当指定的等待时间过去时,线程也会重新被调度执行。

该方法会返回剩余的纳秒数。比如调用时传入1000,即等待1000纳秒,如果在第300纳秒时线程获得锁返回,则该方法返回700。

该返回值是个近似值,小于或等于零的值表示没有剩余时间。

await(long time, TimeUnit unit)

该方法可以看作是awaitNanos方法的泛化版,可以指定时间的单位。该方法等价于 awaitNanos(unit.toNanos(time)) > 0;

awaitUntil(Date deadline)

导致当前线程等待,直到它被其它线程调用signal释放或被中断,或者指定的截止日期过去。

signal

有加锁操作,就必然有释放锁的操作。signal方法就是用于释放锁。

signal方法调用后,会唤醒该Condition对象上的一个等待线程。如果有任何线程在此条件下等待,则选择其中一个线程唤醒。

该线程必须在从await返回之前重新获取锁。此特性和Object类的notify方法一致。

signalAll

唤醒该Condition实例对象上的所有等待该条件的线程。

每个线程必须重新获取锁才能从await返回。该特性和Object的notifyAll方法一致。

注意:线程被唤醒和能够继续执行不是一回事,这之间还有一个重新获取锁的过程。

Condition使用案例

需求描述:实现一个有界的容器,可以支持多线程往里面put数据和从里面take数据。

public class BoundedContainer {
    private String[] elements = new String[10];//容器底层的数据结构
    private Lock lock = new ReentrantLock();//锁对象
    private Condition notFullCondition = lock.newCondition();//不为满条件
    private Condition notEmptyCondition = lock.newCondition();//不为空条件
    private int elementCount;//数组elements中的元素数量
    private int putIndex;//写指针
    private int takeIndex;//读指针

    /**
     * 放数据
     *
     * @param element
     * @throws InterruptedException
     */
    public void put(String element) throws InterruptedException {
        lock.lock();
        try {
            while (elementCount == elements.length) {
                notFullCondition.await();
            }
            elements[putIndex] = element;
            if (++putIndex == elements.length) {
                putIndex = 0;
            }
            elementCount++;
            System.out.println("after put:" + Arrays.asList(elements));
            notEmptyCondition.signal();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 取数据
     *
     * @return
     * @throws InterruptedException
     */
    public String take() throws InterruptedException {
        lock.lock();
        try {
            while (elementCount == 0) {
                notEmptyCondition.await();
            }
            String element = elements[takeIndex];
            elements[takeIndex] = null;
            if (++takeIndex == elements.length) {
                takeIndex = 0;
            }
            elementCount--;
            System.out.println("after take:" + Arrays.asList(elements));
            notFullCondition.signal();
            return element;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        //启动10个读线程和10个写线程
        BoundedContainer boundedContainer = new BoundedContainer();

        IntStream.range(0, 10).forEach(i -> new Thread(() -> {
            try {
                boundedContainer.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start());

        IntStream.range(0, 10).forEach(i -> new Thread(() -> {
            try {
                boundedContainer.put("hi");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start());

    }
}

运行上述代码,发现整个读写过程中数据的变化都完整展示了。

也可以改变线程数量,比如读写都启动20个线程;也可以启动10个写线程,8个读线程,或者反过来。看看各种情况下的输出。

通过设置不同数量的线程,可以看到不同的运行结果,有助于理解Lock锁的执行原理。

Tip:关于Java中各种锁的讲解与使用案例也可以参见:《Java中的锁》