Condition.signal 之后还要争锁:用可关闭通道验证等待条件
signal已经执行,等待线程仍然没有继续,这不一定是信号丢失。等待者还需要重新取得同一把锁;发信号的线程若仍然持锁,接收者就不能进入受保护区。把signal理解成“立即把执行权交给另一线程”,会让并发日志顺序看起来十分反常。
更关键的是,线程被唤醒不代表它等待的业务条件已经成立。signal只是一次通知,真正决定是否继续的是共享状态。所以正确等待通常写成while检查条件,而不是if后面只await一次。
先发一条没有改变状态的通知
下面程序故意先signal而不设置ready。等待者醒来后检查仍为false,重新等待;第二次设置ready并signal时,主线程暂时保持锁,确认Future还没有完成,随后释放锁,等待者才返回。
代码还给出容量为一的Channel,使用notEmpty与notFull分开等待。Java 17保存为ConditionChannel.java后运行java ConditionChannel.java:
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.*;
public class ConditionChannel {
static final class Channel {
private final ReentrantLock lock=new ReentrantLock();
private final Condition notEmpty=lock.newCondition(),notFull=lock.newCondition();
private final ArrayDeque<String> values=new ArrayDeque<>();
private boolean closed;
void put(String value)throws InterruptedException {
Objects.requireNonNull(value);
lock.lockInterruptibly();
try {
while(values.size()==1&&!closed)notFull.await();
if(closed)throw new IllegalStateException("channel closed");
values.addLast(value);notEmpty.signal();
}finally{lock.unlock();}
}
Optional<String> take()throws InterruptedException {
lock.lockInterruptibly();
try {
while(values.isEmpty()&&!closed)notEmpty.await();
if(values.isEmpty())return Optional.empty();
String result=values.removeFirst();notFull.signal();
return Optional.of(result);
}finally{lock.unlock();}
}
void close() {
lock.lock();
try{closed=true;notEmpty.signalAll();notFull.signalAll();}
finally{lock.unlock();}
}
}
static void await(CountDownLatch latch)throws InterruptedException {
if(!latch.await(3,TimeUnit.SECONDS))throw new IllegalStateException("coordination timeout");
}
public static void main(String[] args)throws Exception {
ExecutorService worker=Executors.newSingleThreadExecutor();
ReentrantLock lock=new ReentrantLock();Condition changed=lock.newCondition();
boolean[] ready={false};AtomicInteger waits=new AtomicInteger();
CountDownLatch started=new CountDownLatch(1),rewaited=new CountDownLatch(1);
try {
Future<String> waiting=worker.submit(()->{
lock.lockInterruptibly();
try {
while(!ready[0]) {
if(waits.incrementAndGet()==1)started.countDown();else rewaited.countDown();
changed.await();
}
return "ready";
}finally{lock.unlock();}
});
await(started);
lock.lock();try{changed.signal();}finally{lock.unlock();}
await(rewaited);
lock.lock();
try{ready[0]=true;changed.signal();System.out.println("signalled while holding lock, done="+waiting.isDone());}
finally{lock.unlock();}
System.out.println("waiter="+waiting.get(3,TimeUnit.SECONDS)+", waited again="+(waits.get()>=2));
Channel channel=new Channel();channel.put("A");
CountDownLatch putting=new CountDownLatch(1);
Future<?> second=worker.submit(()->{putting.countDown();channel.put("B");return null;});
await(putting);
try{second.get(40,TimeUnit.MILLISECONDS);throw new AssertionError("full channel should wait");}
catch(TimeoutException expected){System.out.println("full channel=waiting");}
System.out.println("take first="+channel.take().orElseThrow());
second.get(3,TimeUnit.SECONDS);channel.close();
System.out.println("drain after close="+channel.take().orElseThrow());
System.out.println("closed and empty="+channel.take().isEmpty());
try{channel.put("C");throw new AssertionError("closed put");}
catch(IllegalStateException expected){System.out.println(expected.getMessage());}
Channel empty=new Channel();
Future<Optional<String>> stop=worker.submit(empty::take);
empty.close();
System.out.println("empty close released consumer="+stop.get(3,TimeUnit.SECONDS).isEmpty());
}finally{
worker.shutdownNow();
if(!worker.awaitTermination(3,TimeUnit.SECONDS))throw new IllegalStateException("worker active");
}
}
}
signalled while holding lock, done=false
waiter=ready, waited again=true
full channel=waiting
take first=A
drain after close=B
closed and empty=true
channel closed
empty close released consumer=true
第一项done=false由主线程仍然持有锁来保证,不依赖输出机器足够慢。waited again说明不满足条件的通知没有让等待者误闯后续逻辑。这个实验没有制造或声称测出了虚假唤醒,而是主动发送一条状态未改变的通知,验证同一套循环检查同样必要。
Condition允许虚假唤醒,其他等待者也可能在当前线程重新得到锁前先消费掉资源。即使业务代码保证“只有满足条件才signal”,也不能由这句话删掉while。通知不是一份资源预订,醒来的线程必须根据自己实际观察到的状态重新决定。
await 暂时交回锁,返回之前再取回来
若等待者拿着锁原地睡眠,生产者也就没法取得锁修改条件。await将等待与释放关联锁协调起来,结束等待时重新竞争锁,返回或抛相应中断异常之前遵守接口规定的锁重获过程。调用方仍要在finally释放自己持有的锁。
不能在没有持有关联锁时随意调用ReentrantLock产生的Condition进行await或signal。共享状态检查、修改和通知都放在同一把锁下,才不会形成“检查之后、真正开始等待之前”漏掉状态转换的窗口。单独把ready改成volatile,也不能替代这组等待协议。
程序中的计数器和门闩只是让测试看到等待经过了哪里,不承担Channel的业务同步。Channel只依赖它自己的锁和两个条件;如果把演示钩子、调试计数或sleep变成正确性所必需的步骤,就说明真实合同还没有建立好。
有界通道怎样表达满、空和结束
put在容量已满时等待notFull,take在没有元素时等待notEmpty。取走元素后通知生产者,放入元素后通知消费者。容量固定为一使两种路径都容易观察,实际通道可扩展成经过校验的容量参数,但仍需保持条件与状态对应。
关闭采用“停止新写,允许排空”的政策。close以后put拒绝,已有B仍可被取走;关闭且为空才返回Optional.empty。它没有用null充当正常数据,因此消费者可以明确区分拿到内容与流已结束。若产品需要立即丢弃剩余数据,那是另一项关闭政策,不能在这个实现上悄悄改掉。
close对两个条件都signalAll,因为等待中的生产者需要知道无法再写,消费者也需要知道不会再来新元素。只叫醒一个角色或只叫醒一条线程,其他等待者可能再也没有后续通知。普通一次放入或取走只改变一个槽位,使用signal与这份状态变化相匹配。
示例验证正常背压、关闭后排空、关闭后拒绝写入和空通道关闭返回。最后一个消费者可能在close前或后进入take,两条次序都应得到结束;它不是在声称测试固定证明了消费者已经阻塞。要专门断言“已阻塞者被唤醒”,还需像前半段一样设置受控等待观察点。
中断与超时必须和状态检查放在一起设计
put与take允许等待被中断,调用者可以放弃本次操作。中断并不会自动关闭整个Channel,其他生产者和消费者仍按原状态运行。如果一个工作者决定退出,应由拥有通道生命周期的一方判断是否需要关闭,不能让任意一次取消都摧毁共享资源。
带超时的版本应循环计算剩余预算,例如围绕awaitNanos返回值继续检查条件,而不是每次唤醒又重新等待完整超时。等待预算还不能覆盖用户任意处理函数;拿到数据以后做长时间业务,是下一阶段的责任。
这一实现适合解释条件协议,普通生产者消费者代码可以优先采用现成有界队列。若使用自定义通道,关闭、排空、取消和确认都应成为可测试合同。signal负责让等待者有机会重查,业务状态负责决定能不能继续,两者缺一项,循环就可能在偶然调度下看似正常。