transferstack与transferqueue的本质区别在于匹配模式:transferstack采用lifo栈结构实现非公平匹配,后到线程可插队;transferqueue采用fifo队列结构实现公平匹配,严格按等待顺序配对,且节点状态管理更复杂。

TransferStack 和 TransferQueue 的本质区别在哪
SynchronousQueue 不是传统意义的“队列”,它没有容量,不存储元素,只做线程间瞬时移交。底层用两种模式实现移交逻辑:栈(LIFO)和队列(FIFO),分别对应 TransferStack(默认单线程场景)和 TransferQueue(多线程高竞争场景)。选哪种,取决于构造时是否传 fair 参数:new SynchronousQueue(true) 用 TransferQueue,false 或无参用 TransferStack。
-
TransferStack基于 Treiber 栈,入栈/出栈快,但后到的线程可能“插队”匹配先到的阻塞线程,导致不公平; -
TransferQueue是带哨兵节点的双向链表,严格 FIFO,匹配顺序可预测,适合要求公平性的生产者-消费者配对; - 两者都靠
UNSAFE.compareAndSetObject实现无锁操作,但TransferQueue多一层节点状态管理(REQUEST/DATA/FULFILLING),开销略高。
为什么 put/take 配对失败时会自旋 + 阻塞两次
SynchronousQueue 的 put() 和 take() 不是简单等待,而是分阶段推进移交:
- 第一阶段:尝试无锁配对(比如
put()线程在栈顶看到一个take()线程的Node,直接 CAS 设置值并唤醒); - 若失败(无人等待),当前线程把自己包装成
Node推入栈/队列,并调用LockSupport.park()挂起; - 第二阶段:被唤醒后,检查是否真被匹配(比如值已被其他线程取走),若否,可能再次 park —— 这就是“两次阻塞”的来源。
常见错误现象:Thread.getState() 查到线程长期处于 WAITING,但实际已配对成功,只是还没完成值拷贝或唤醒信号丢失;这通常是因为 JVM 层面的 park/unpark 语义弱保证,必须依赖 Node 的 match 字段双重校验。
如何验证当前用的是 TransferStack 还是 TransferQueue
不能只看构造参数,得看运行时行为。最直接的方式是反射读取 transferer 字段:
Field f = SynchronousQueue.class.getDeclaredField("transferer");
f.setAccessible(true);
Object t = f.get(yourSynchronousQueueInstance);
System.out.println(t.getClass().getSimpleName()); // 输出 TransferStack 或 TransferQueue
- 注意:JDK 9+ 模块系统会拦截反射,需加
--add-opens java.base/java.util.concurrent=ALL-UNNAMED; - 更稳妥的实操建议:在高并发压测下,用 JFR 或 async-profiler 抓取
TransferQueue.transfer()调用频次,若显著高于TransferStack.transfer(),说明公平模式生效; - 别依赖
toString()或文档注释——它们不反映真实调度路径。
配对移交中容易被忽略的状态竞争点
SynchronousQueue 的移交不是原子动作,而是一组状态跃迁:idle → waiting → fulfilling → done。最容易出问题的是 fulfilling 状态的竞态:
- 两个
put()线程同时发现一个take()节点,都试图将其标记为FULFILLING,只有一个成功,另一个必须回退重试; - 如果
fulfilling线程在设置match前被中断,会导致对方线程永久 park(JDK 8u202 之前有此 bug,现已修复,但自定义封装时仍要检查Thread.interrupted()); -
TransferQueue中,哨兵节点的next和prev更新必须成对 CAS,漏掉任意一个都会导致链表断裂,后续所有操作 hang 住。
真正难调试的,从来不是“没配对”,而是“配对了但值没传过去”或者“配对成功却没人唤醒”。这时候得盯住 Node.match 字段是否非空、LockSupport.getBlocker() 是否指向正确 Node,而不是只看线程状态。










