并发工具:Condition

显式锁Lock可以用来替代synchronized关键字,那么Condition接口将会很好地替代传统的、通过对象监视器调用wait()、notify()、notifyAll()线程间的通信方式
Condition对象是由某个显式锁Lock创建的,一个显式锁Lock可以创建多个Condition对象与之关联,Condition的作用在于控制锁并且判断某个条件(临界值)是否满足,如果不满足,那么使用该锁的线程将会被挂起等待另外的线程将其唤醒,与此同时被挂起的线程将会进入阻塞队列中并且释放对显式锁Lock的持有,这一点与对象监视器的wait()方法非常类似。

1 Condition入门

Condition接口提供了比传统线程间通信方式(对象monitor方法)更多的操作方法,Condition不能被直接创建,只能与某个显式锁Lock进行创建并且与之关联

写个demo,简单体会一下使用:

  • 我们将有两个线程分别进行数据的读与写。
  • 当数据发生改变时,读取数据的线程才会对其进行读取和做进一步的处理,当数据未发生改变时读取数据的线程将会等待。
  • 当数据未被读取时,修改数据的线程将会进入阻塞等待,直到该数据被使用过才会进一步地产生出新的数据。
public class ConditionExample1 {
    /**
     * 定义共享数据
     */
    private static int shareData = 0;

    /**
     * 定义布尔变量标识当前的共享数据是否已经被使用
     */
    private static boolean dataDataUsed = false;

    /**
     * 定义锁
     */
    private static final Lock lock = new ReentrantLock();

    /**
     * 使用显式锁创建Condition对象并且与之关联
     */
    private static final Condition condition = lock.newCondition();

    /**
     * 对数据的写操作
     */
    private static void change() {
        // 获取锁,如果锁被其他线程获取,当前线程就会进入阻塞
        lock.lock();
        try {

            try {
                while (!dataDataUsed) {
                    // 如果当前数据没有被使用过,那么就进入等待队列
                    condition.await();
                }
                // 数据已经被使用过了,就修改数据
                TimeUnit.SECONDS.sleep(current().nextInt(5));
                shareData++;
                // 修改标记
                dataDataUsed = false;
                System.out.println(Thread.currentThread().getName() + " produce the new value: " + shareData);
                // 通知并唤醒在wait队列中的其他线程——数据使用线程
                condition.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * 使用数据
     */
    private static void use() {
        // 获取锁
        lock.lock();
        try {

            while (dataDataUsed) {
                // 如果当前数据已经使用,则当前线程将进入wait队列,并且释放lock
                condition.await();
            }
            // 数据没有被使用,那就使用
            TimeUnit.SECONDS.sleep(current().nextInt(5));
            // 更新标记
            dataDataUsed = true;
            System.out.println(Thread.currentThread().getName() + " use the share data: " + shareData);
            // 通知并唤醒wait队列中的其他线程——数据修改线程
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        // 创建并启动两个匿名线程
        new Thread(() -> {
            for (; ; ) {
                change();
            }
        }, "Producer").start();
        new Thread(() -> {
            for (; ; ) {
                use();
            }
        }, "Consumer").start();
    }
}

会看到数据的更改与使用交替输出,不会出现数据未更改但多次使用的情况,以及数据未使用但多次更改的情况。输出结果:

Consumer use the share data: 0
Producer produce the new value: 1
Consumer use the share data: 1
Producer produce the new value: 2
Consumer use the share data: 2
Producer produce the new value: 3
Consumer use the share data: 3
Producer produce the new value: 4
Consumer use the share data: 4
Producer produce the new value: 5
Consumer use the share data: 5
Producer produce the new value: 6
Consumer use the share data: 6
Producer produce the new value: 7
Consumer use the share data: 7
Producer produce the new value: 8
Consumer use the share data: 8
Producer produce the new value: 9
Consumer use the share data: 9
Producer produce the new value: 10
Consumer use the share data: 10
Producer produce the new value: 11
Consumer use the share data: 11
  • shareData和dataUsed标识变量都是我们在该程序中的共享数据(资源),同时dataUsed也是临界值,数据一致性的保护主要是针对这两个变量的。
  • 我们创建了显式锁Lock,该锁的作用主要是用于保护数据的一致性,然后使用该显式锁创建与之关联的Condition对象。
  • 在change()方法中,首先应获取对共享数据的访问权限(获取锁),然后判断共享数据是否未被使用,如果还未被使用,那么当前线程将调用condition的await()方法进入阻塞队列,以阻塞等待被其他线程唤醒,调用condition的await()方法之后,当前线程会释放对显式锁Lock的持有,由于我们使用两个线程进行操作,因此这里的while循环完全可以使用if进行替代。
  • 当共享数据已经被使用,change()方法会进一步地修改共享数据,然后将状态标识设置为false,并且通知其他线程(主要是数据使用线程)对其进行使用
  • 在use()方法中,同样是首先获取对共享数据的访问权限(获取锁),然后判断共享数据是否已经被使用,如果数据已经被使用,那么当前线程会进入wait队列等待修改共享数据的线程将其唤醒。
  • 当正常使用了最新的共享数据时,当前线程则会通知数据更新线程可以继续对数据进行修改了。

通过对Condition的简单使用以及运行过程的分析,我们对比对象monitor方式的线程间通信,可以发现两者在使用的过程中非常的相似,

在这里插入图片描述

2 Condition相关详解

2.1 await方法

/**
当前线程调用该方法会进入阻塞状态直到有其他线程对其进行唤醒,或者对当前线程执行中断操作。
当前线程会被加入到阻塞队列中,并且释放对显式锁的持有,object monitor的wait()方法被执行后同样会加入一个虚拟的容器waitset(线程休息室)中,waitset是一个虚拟的概念,JVM(虚拟机)规范并未强制要求其采用什么样的数据结构,Condition的wait队列则是由Java程序实现的FiFO队列。
**/
void await() throws InterruptedException;
/**
该方法与await()方法类似,只不过该方法比较固执,它会忽略对它的中断操作,一直等待有其他线程将它唤醒。
**/
void awaitUninterruptibly();
/**
调用该方法同样会使得当前线程进入阻塞状态,但是可以设定阻塞的最大等待时间,
如果在设定的时间内没有其他线程将它唤醒或者被执行中断操作,
那么当前线程将会等到设定的纳秒时间后退出阻塞状态。
**/
long awaitNanos(long nanosTimeout) throws InterruptedException;

/**
执行方法awaitNanos(),如果到达设定的纳秒数则当前线程会退出阻塞,并且返回实际等待的纳秒数,
但是程序很难判断线程是否被正常唤醒,
因此该方法的作用除了可以指定等待的最大的单位时间,另外,还可以返回在单位时间内被正常唤醒而且还是由于超时而退出的阻塞。
**/
boolean await(long time, TimeUnit unit) throws InterruptedException;

/**
调用该方法同样会导致当前线程进入阻塞状态直到被唤醒、被中断或者到达指定的Date。
**/
boolean awaitUntil(Date deadline) throws InterruptedException
  • 会释放对显式锁的持有
  • Condition的wait队列则是由Java程序实现的FiFO队列。

2.2 signal方法

/**
唤醒Condition阻塞队列中的一个线程,
Condition的wait队列采用FiFO的方式,
因此在wait队列中,第一个进入阻塞队列的线程将会被首先唤醒
**/
void signal();

/**
唤醒Condition wait队列中的所有线程。
**/
void signalAll();

2.3 Lock中与Condition相关的方法

在显式锁ReentrantLock、ReentrantReadWriteLock中与Condition有关的方法:

/**
该方法的作用是查询是否有线程由于执行了await方法而进入了与condition关联的wait 队列之中,
若有线程在wait队列中则返回true,否则返回false。
**/
hasWaiters(Condition condition);
/**
该方法的作用是查询与condition关联的wait队列数量。
**/
getWaitQueueLength(Condition condition);

上面这两个方法比较简单,但是使用它们的前提是必须获得对显式锁Lock的持有,否则将会出现IllegalMonitorStateException异常

public class ConditionExample2 {

    public static void main(String[] args) throws InterruptedException {
        final ReentrantLock lock = new ReentrantLock();
        final Condition condition = lock.newCondition();
        new Thread(() -> {
            lock.lock();
            try {
                // 调用await,进入wait队列,并释放锁
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "T1").start();

        // 休眠1s,确保T1线程启动,进入wait队列
        TimeUnit.SECONDS.sleep(1);

        // 未获得锁,就调用hasWaiter方法将会抛出异常
        try {
            lock.hasWaiters(condition);
        } catch (Exception e) {
            System.out.println("抛出的异常是:" + e.getClass().getSimpleName());
        }

        // 未获得锁,就调用hasWaiter方法将会抛出异常
        try {
            lock.getWaitQueueLength(condition);
        } catch (Exception e) {
            System.out.println("抛出的异常是:" + e.getClass().getSimpleName());
        }

        // 获得锁
        lock.lock();
        try {
            // 获得锁就不会抛出异常
            System.out.println(lock.hasWaiters(condition));
            System.out.println(lock.hasWaiters(condition));
        }finally {
            lock.unlock();
        }
    }
}

这段代码,T1线程会会先获得锁,进入wait队列,并释放锁(await方法会释放锁),主线程在没有获取锁的情况下调用hasWaitersgetWaitQueueLength方法抛出IllegalMonitorStateException,之后获得锁,在执行这两个方法边不会抛出异常了,由于T1之前进入了wait队列,所以此时这两个返回分别返回true和1,输出如下:

抛出的异常是:IllegalMonitorStateException
抛出的异常是:IllegalMonitorStateException
true
1

使用await()、signal()、signalAll()方法同样需要lock锁的加持才可以,就像使用wait()、notify()、notifyAll()方法必须在同步方法或者同步代码块中一样,否则也会出现运行时异常。

3 实现生产者消费者

3.1 实现

import java.util.LinkedList;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;

/**
 * @author wyaoyao
 * @date 2021/4/25 8:41
 */
public class ConditionExample3 {

    /**
     * 定义显示锁
     */
    private static final ReentrantLock lock = new ReentrantLock();

    /**
     * 创建与显式锁Lock关联的Condition对象
     */
    private static final Condition condition = lock.newCondition();

    /**
     * 定义long型数据的链表(队列)
     */
    private static final LinkedList<Long> LINKED_LIST = new LinkedList<>();

    /**
     * 链表的最大容量为100
     */
    private static final int CAPACITY = 100;

    /**
     * 定义数据的初始值为0
     */
    private static long i = 0;

    /**
     * 生产数据的方法
     */
    private static void produce() {
        // 获取锁
        lock.lock();
        try {
            while (LINKED_LIST.size() >= CAPACITY) {
                // 当队列的数据大于最大值的时候,就等待
                condition.await();
            }
            // 当链表中的数据量不足最大值的时,生产新的数据
            i++;
            // 将数据放到链表尾部
            LINKED_LIST.addLast(i);
            System.out.println(Thread.currentThread().getName() + " produce data -> " + i);
            // 通知其他线程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 消费数据的方法
     */
    private static void consume() {
        // 获取锁
        lock.lock();
        try {
            while (LINKED_LIST.isEmpty()) {
                // 链表为空是另外一个临界值,当list中的数据为空时,消费者线程将被阻塞加入与condition关联的wait队列中
                condition.await();
            }
            // 队列不是空,就消费数据
            Long value = LINKED_LIST.removeFirst();
            System.out.println(Thread.currentThread().getName() + " consume data -> " + value);
            // 通知其他线程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 测试
     */
    public static void main(String[] args) {
        // 10个生产者
        IntStream.range(0, 10).forEach(i -> {
            new Thread(() -> {
                while (true) {
                    produce();
                    try {
                        TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(5));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "Producer-" + i).start();
        });

        // 5个消费者
        IntStream.range(0, 5).forEach(i -> {
            new Thread(() -> {
                while (true) {
                    consume();
                    try {
                        TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(5));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "Consumer-" + i).start();
        });
    }

}

运行上面的程序,会发现生产者和消费者线程在交替地运行,进行数据的生产与消费,何之前使用synchronized方法没有太大区别

Producer-0 produce data -> 1
Producer-1 produce data -> 2
Producer-3 produce data -> 3
Producer-2 produce data -> 4
Producer-4 produce data -> 5
Producer-5 produce data -> 6
Producer-6 produce data -> 7
Producer-7 produce data -> 8
Producer-8 produce data -> 9
Producer-9 produce data -> 10
Consumer-3 consume data -> 1
Consumer-0 consume data -> 2
Consumer-1 consume data -> 3

3.2 改进

上面的程序虽然能够正常运行,但是仍然存在一些不足之处,比如在唤醒线程的时候,此刻的唤醒动作唤醒的是与Condition关联的阻塞队列中的所有阻塞线程。

由于我们使用的是唯一的一个Condition实例,因此生产者唤醒的有可能是与Condition关联的wait队列中的生产者线程,假设当生产者线程被唤醒后抢到了CPU的调度获得执行权,但是又发现队列已满再次进入阻塞。这样的线程上下文开销实际上是没有意义的,甚至会影响性能(多线程下的线程上下文开销其实是一个非常大的性能损耗,一般针对高并发程序的调优就是在减少上下文切换发生的概率)。

可以使用两个Condition对象,一个用于对队列已满临界值条件的处理,另外一个用于对队列为空的临界值条件的处理,这样一来,在生产者中唤醒的阻塞线程只能是消费者线程,在消费者中唤醒的也只能是生产者线程:

/**
 * 定义显示锁
 */
private static final ReentrantLock lock = new ReentrantLock();

/**
 * 创建与显式锁Lock关联的Condition对象
 */
private static final Condition FULL_CONDITION  = lock.newCondition();

private static final Condition EMPTY_CONDITION = lock.newCondition();

/**
 * 定义long型数据的链表(队列)
 */
private static final LinkedList<Long> LINKED_LIST = new LinkedList<>();

/**
 * 链表的最大容量为100
 */
private static final int CAPACITY = 100;

/**
 * 定义数据的初始值为0
 */
private static long i = 0;

/**
 * 生产数据的方法
 */
private static void produce() {
    // 获取锁
    lock.lock();
    try {
        while (LINKED_LIST.size() >= CAPACITY) {
            // 当队列的数据大于最大值的时候,就等待,生产者线程进入FULL_CONDITION wait队列中
           FULL_CONDITION.await();
        }
        // 当链表中的数据量不足最大值的时,生产新的数据
        i++;
        // 将数据放到链表尾部
        LINKED_LIST.addLast(i);
        System.out.println(Thread.currentThread().getName() + " produce data -> " + i);
        // 通知其他线程,通知的消费者线程
        EMPTY_CONDITION.signalAll();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}

/**
 * 消费数据的方法
 */
private static void consume() {
    // 获取锁
    lock.lock();
    try {
        while (LINKED_LIST.isEmpty()) {
            // 链表为空是另外一个临界值,当list中的数据为空时,消费者线程将被阻塞加入与condition关联的wait队列中
            EMPTY_CONDITION.await();
        }
        // 队列不是空,就消费数据
        Long value = LINKED_LIST.removeFirst();
        System.out.println(Thread.currentThread().getName() + " consume data -> " + value);
        // 通知消费者
        FULL_CONDITION.signalAll();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}

采用两个Condition对象的方式就很好地解决了生产者线程除了唤醒消费者线程以外,还唤醒生产者线程而引起的无效线程上下文切换的情况,大家思考一下,使用传统的对象监视器(Object Monitor)的方式是很难这样优雅地解决这样的问题的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值