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中的锁》