目录
线程间的通信方式有很多,比如使用volatile关键字修饰变量,使用synchronized和wait()、notify()、notifyAll()组合,还可以使用LockSupport提供的park()、unpark()方法。
JUC包下也提供了很多线程通信,比如基于AQS条件等待模式实现的Condition,基于共享模式的CountDownLathch、Semaphore,还有间接实现AQS的CyclicBarrier。
这些工具类都是开发中用的比较多的,所有勾勾好好学习了每一个类的用法,学习文章也会很快更新喔。
今天勾勾和大家一起学习AQS条件等待模式Condition。
AQS的源码在《AQS条件等待模式》中讲的比较详细,这里我们更多的关注工具类的使用。对原理还迷惑的小朋友要先自己学习再看文章。
小伙伴们一定要主动学习,自己总结,不要被动的去接受。别人的文章只能作为参考的工具,或者有疑问时看看文章中是否能给出答案。
Condition初印象
话不多说,先看个例子。
有一个计算线程cal在计算数据,有一个取数线程take需要取计算之后的数据,那取数线程take不知道计算线程cal什么时候算好,在需要用到数据的时候就让出锁让计算线程cal先计算,算好了之后通知取数线程take。
public class ConditionTest {
//共享资源
private Map<String, Integer> map = new HashMap<>();
//锁
private final ReentrantLock lock = new ReentrantLock();
//创建条件
private final Condition condition = lock.newCondition();
public void fun() {
/**
* 取数线程,可以先并发做其他业务,等需要依赖cal_value的时候再阻塞
*/
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "等待");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "被唤醒");
sleep(2L);
map.get("cal_value");
} finally {
lock.unlock();
}
}, "take").start();
/**
* 计算线程,当把cal_value值计算结束后通知依赖cal_value的线程
*/
new Thread(()->{
lock.lock();
try {
map.put("cal_value", new Random().nextInt(100));
condition.signal();
System.out.println(Thread.currentThread().getName() + "通知其他线程");
//模拟后续的业务处理
sleep(2L);
map.put("next_cal", new Random().nextInt(1000));
System.out.println(Thread.currentThread().getName() + "计算结束");
} finally {
lock.unlock();
}
}, "cal").start();
}
//测试
public static void main(String[] args) {
ConditionTest test = new ConditionTest();
test.fun();
}
/**
* 通过休眠模拟业务逻辑
* @param time
*/
public static void sleep(Long time){
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述代码中我们使用ReentrantLock创建了Condition对象,Condition对象不能直接创建,只能由显式锁创建并与锁关联,一个ReentrantLock对象可以创建多个Condition。
take线程调用condition的await()方法之后,会释放所有的锁并加入条件等待队列。
cal线程调用condition的signal()方法之后唤醒条件等待队列中的第一个有效等待节点将其加入同步队列,加入同步队列的线程就可以参与锁的竞争了。
我们来看下main方法运行的结果:
从结果看take等待后,cal线程获取了锁开始执行业务逻辑,然后唤醒了take线程。但是take线程唤醒后并没有立即获取锁执行业务,而是等cal线程执行结束后才获取到锁,说明signal方法的调用并没有释放锁。
揭开Condition面纱
接下来我们学习Condition提供的方法。
void await() throws InterruptedException;
当前线程调用该方法会进入阻塞状态直到有其他线程唤醒,或者当前线程被中断。
当前线程调用await()会进入阻塞状态时,会被添加到阻塞队列中,并且释放对锁的持有。这个与object的wait()方法是一样的,当前线程调用wait()方法会进入阻塞状态,并添加到waitSet队列中。
void awaitUninterruptibly();
当前线程调用该方法进入阻塞状态,且忽略中断,只能等待其他线程将其唤醒。
long awaitNanos(long nanosTimeout) throws InterruptedException;
当前线程调用该方法同样会进入阻塞状态,但是在设置的纳秒时间后线程还没有被唤醒或者被中断,那么线程将退出阻塞状态。
boolean await(long time, TimeUnit unit) throws InterruptedException;
当前线程调用该方法会进入阻塞状态,并且在设置的等待时间后还没被唤醒或者被中断,则退出阻塞状态,其与awaitNanos主要的区别是根据boolean类型的返回值可以判断线程是正常唤醒还是超时退出。
boolean awaitUntil(Date deadline) throws InterruptedException;
当前线程调用该方法也是会进入阻塞状态,直到被唤醒、被打断或者到达指定的date。也即是我们可以指定到哪一个时间点退出阻塞。
void signal();
当前线程调用该方法,会唤醒Condition队列中的一个节点。它的唤醒操作是将等待队列中的节点添加到同步队列中,且是遵循FIFO,从第一个节点开始唤醒且只能唤醒一个节点。
void signalAll();
当前线程调用该方法唤醒等待队列中的所有节点。
我们看下图,展示了AQS中的同步队列和两个Condition等待队列。
同步队列中获取锁的线程执行结束后,调用不同Condition的signal()方法从等待队列将第一个节点添加到同步队列中,在同步队列中的节点就可以参与锁的竞争了。
从Condition提供的方法看,Condition更适合两个线程之间的等待,对多个线程等待一个线程的场景使用并不是很友好。
如果我们想顺序执行多个线程,就可以创建多个Condition,D阻塞等待C,C阻塞等待B,B阻塞等待A,A运行之后唤醒B,B运行之后唤醒C,C运行之后唤醒D,这样可以实现对共享资源的同步访问。
Condition实现生产者消费者
public class ConditionTest2 {
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition condition_full = lock.newCondition();
private static final Condition condition_empty = lock.newCondition();
private static List<Integer> list = new ArrayList<>();
private static final int capacity = 100;
private static int index = 0;
public static void main(String[] args) {
//生产者
IntStream.range(0,20).forEach(item ->
new Thread(()->{
for (; ;) {
produce();
sleep();
}
},"produce-"+item).start()
);
//消费者
IntStream.range(0,5).forEach(item ->
new Thread(()->{
for (; ;) {
consume();
sleep();
}
},"consume-"+item).start()
);
}
private static void produce() {
lock.lock();
try {
//如果队列满了,则等待
while (list.size() >= capacity){
System.out.println("生产者等待");
condition_full.await();
}
//队列不满就一直生产
index ++;
list.add(index);
System.out.println(Thread.currentThread().getName()+"生产了:"+index);
condition_empty.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private static void consume() {
lock.lock();
try {
//如果队列空了,则等待
while (list.isEmpty()){
System.out.println("消费者等待");
condition_empty.await();
}
//队列不空就消费数据
int value = list.remove(0);
System.out.println(Thread.currentThread().getName()+"消费了:"+value);
condition_full.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private static void sleep() {
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Condition总结
Condition必须由显式锁创建,且与锁绑定。
一个锁可以创建多个Condition,调用不同的Condition对象唤醒不同等待队列中的线程。
Condition包括了wait()、notify()、notifyAll()的所有功能,而且能提供更高级的应用:每个锁上可以存在多个等待、条件等待也可以是中断的、不中断的、基于时长的等待,以及公平锁和非公锁的等待队列。所以在现在的开发中已经完全替代了wait()、notify()、notifyAll()。相比于对象监视器,Condition更加高效,避免了无所谓的线程切换。
我是勾勾,一直在努力的程序媛!感谢您的点赞、转发和关注!