原创

SynchronousQueue 的 size 为零,为什么 offer 仍会失败

队列size为零,offer却失败了。调用者因此认定“队列空着还不让写,一定是并发可见性问题”。如果对象是SynchronousQueue,这个结果恰好符合合同:它不保存待消费元素,每次插入都要与接收者直接配对。

它的零容量不是“当前正好没数据”,而是没有储存元素的位置。容量为一的ArrayBlockingQueue还能先放入一项再等消费者;SynchronousQueue不能。把构造器换成它以后,生产者与消费者之间的节奏也随之改变,不能只按队列名字替换。

用一次真实交接看等待位置

下面的Java 17程序先在没有接收者时offer,再让生产者put等待,最后由主线程接收。保存为DirectHandoff.java,执行java DirectHandoff.java。40毫秒只用于触发样例分支,不是对操作系统调度精度的测量。

import java.util.concurrent.*;
public class DirectHandoff {
    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 {
        SynchronousQueue<String> handoff=new SynchronousQueue<>();
        System.out.println("offer without receiver="+handoff.offer("early"));
        System.out.println("capacity="+handoff.remainingCapacity());
        ExecutorService producer=Executors.newSingleThreadExecutor();
        try {
            CountDownLatch entered=new CountDownLatch(1);
            Future<String> sending=producer.submit(()->{entered.countDown();handoff.put("job");return "delivered";});
            await(entered);
            try { sending.get(40,TimeUnit.MILLISECONDS);throw new AssertionError("put must wait"); }
            catch(TimeoutException expected) { System.out.println("put waiting=true, size="+handoff.size()); }
            String received=handoff.poll(3,TimeUnit.SECONDS);
            if (!"job".equals(received)) throw new AssertionError("handoff failed");
            System.out.println("received="+received+", producer="+sending.get(3,TimeUnit.SECONDS));
            System.out.println("timed offer without receiver="+handoff.offer("late",40,TimeUnit.MILLISECONDS));
            CountDownLatch waiting=new CountDownLatch(1),stopped=new CountDownLatch(1);
            Future<?> cancelled=producer.submit(()->{
                waiting.countDown();
                try { handoff.put("cancelled"); }
                catch(InterruptedException e) { Thread.currentThread().interrupt(); }
                finally { stopped.countDown(); }
            });
            await(waiting);cancelled.cancel(true);await(stopped);
            System.out.println("cancelled future="+cancelled.isCancelled()+", worker exited=true");
        } finally { producer.shutdownNow();producer.awaitTermination(3,TimeUnit.SECONDS); }
        CountDownLatch release=new CountDownLatch(1),running=new CountDownLatch(1);
        ThreadPoolExecutor direct=new ThreadPoolExecutor(0,1,30,TimeUnit.SECONDS,
                new SynchronousQueue<>(),new ThreadPoolExecutor.AbortPolicy());
        try {
            direct.execute(()->{running.countDown();try{release.await();}catch(InterruptedException e){Thread.currentThread().interrupt();}});
            await(running);
            try { direct.execute(()->{});throw new AssertionError("rejection expected"); }
            catch(RejectedExecutionException expected) { System.out.println("busy direct pool=rejected"); }
            System.out.println("executor queue size="+direct.getQueue().size());
        } finally { release.countDown();direct.shutdownNow();direct.awaitTermination(3,TimeUnit.SECONDS); }
    }
}
offer without receiver=false
capacity=0
put waiting=true, size=0
received=job, producer=delivered
timed offer without receiver=false
cancelled future=true, worker exited=true
busy direct pool=rejected
executor queue size=0

立即offer没有找到接收者,返回false;put会等到有人接收。等待期间size仍然是零,不能通过size推断没有线程正在交接。主线程poll取得job以后,生产者才返回delivered;随后的限时offer又因为缺少伙伴而失败。

限时offer与put表达了两种不同的等待政策。前者允许等待一段时间后由调用方决定丢弃、重试或拒绝,后者在成功交接或中断之前持续等待。立即offer则通常用于调用方已经愿意接受“现在没有接收者就失败”的场合。它们不是同一个操作的性能选项。

样例不使用sleep猜测生产者已经开始。entered只表明任务进入了发送路径,而发送Future在没有消费者时不可能正常完成;后续get超时因此能验证交接尚未发生。测试再由主线程真正取走元素,确认不是让一个失败任务冒充了等待。

取消路径另外使用stopped门闩确认工作者已经退出。Future.isCancelled只描述Future状态,不必然证明任意后台任务已经停止;这里的put支持中断,捕获后停止工作,才让取消请求落实为实际退出。若处理器之后进入不响应中断的外部调用,就需要它自己的取消协议。

零长度工作队列,为什么执行器还会拒绝

程序后半段创建最大线程数为一的ThreadPoolExecutor,并使用直接移交队列。第一项任务占住唯一工作者,第二项任务没有空闲工作者可以接收,线程数量又不能再增加,所以触发AbortPolicy。

此时执行器工作队列size依然为零。监控若只盯着排队长度,会误以为服务仍很空闲;实际拒绝已经发生。应同时观察活动线程、完成速度、拒绝数量与调用耗时,而不是拿一个零积压值代替容量判断。

执行器使用工作队列时,并不是简单调用put一直等到有消费者。提交过程会结合队列offer、工作者创建和拒绝策略决定下一步,所以业务自己调用SynchronousQueue.put的等待行为,不能直接套到executor.execute上。理解这层差异,才能解释为什么直接移交队列常与可扩展线程数量一起出现。

把最大线程数调得很大,可以让更多任务没有接收者时新建工作者,但这并不等于整体有容量限制。任务还可能占用数据库连接、远端并发名额、堆内存或文件句柄;少了队列积压,压力可以换一种形式出现。是否采用直接移交,应从服务允许的同时执行数量与拒绝合同出发。

若业务允许短时间排队,可以选择有界储存队列,并定义队列满时的处理。若业务要求没有空闲消费者就快速失败,直接移交就更贴近这项合同。两者都合理,不能为了让拒绝数变少,偷偷引入无限队列而不改变调用方预期。

交出去,不等于对方已经处理完

put成功意味着元素已经被接收,不表示消费者已经完成业务动作。比如消费者取走下载任务后才开始写文件,生产者从put返回时,文件可能还没有创建。需要处理确认时,应另设结果Future、确认消息或其他明确通道,而不是依赖队列交换时点。

同样,元素是可变对象时,交接后的所有权需要业务约定。生产者继续修改已交出的缓冲区,可能与消费者读取冲突;队列建立的是交接前后可见性的同步关系,不会替你克隆数据或阻止后续别名写入。样例使用不可变字符串,把这个变量固定住。

构造器的公平参数影响等待线程的配对顺序,默认不承诺FIFO公平;它不会让零容量变成一个可缓存元素的队列。也不应拿集合遍历去查看“当前待交接对象”,这类集合视图按照该类的无存储语义表现,不能代替专门的任务状态记录。

最后检查shutdown的顺序:样例先释放业务门闩,再请求线程池停止,有限等待回收。真正服务关闭时也要先阻止新提交,再处理仍在等待交接或仍在执行的工作。把队列看成空,就直接宣布没有待处理任务,会把同步中的发送者与已被接走的业务一并漏掉。

正文到此结束
评论插件初始化中...
Loading...
本文目录