如何通过 LinkedBlockingQueue 的双锁队列设计理解生产者与消费者的并行操作

冬瑶姑娘_2242

冬瑶姑娘_2242

2026-04-20

396人浏览

原创

putLock 和 takeLock 实现真正并行,因生产与消费被拆分为两个独立临界区:putLock 管插入、计数增、唤醒生产者,takeLock 管移除、计数减、唤醒消费者;只要队列非满非空,二者可同时执行,且 count 用 AtomicInteger 无锁保证原子性与可见性。

如何通过 linkedblockingqueue 的双锁队列设计理解生产者与消费者的并行操作

为什么 putLock 和 takeLock 能让生产者和消费者真正并行?

LinkedBlockingQueue 不是靠“一个锁 + 等待条件”来回切换角色,而是把生产和消费拆成两个独立的临界区:

  • putLock 只管插入、扩容、计数更新(count.getAndIncrement())、唤醒其他生产者(notFull.signal())
  • takeLock 只管移除、头节点推进、计数减量、唤醒其他消费者(notEmpty.signal())

只要队列既不满也不空,两个线程就能同时执行 put() 和 take(),互不抢锁。这不是“看起来并行”,是真实 CPU 时间片上的并发执行。

常见错误现象:

  • 误以为“用了 BlockingQueue 就自动高并发”——其实 ArrayBlockingQueue 只有一把锁,put() 和 take() 是互斥的
  • 在压测中发现吞吐量卡在某个值上不上升,没意识到是锁粒度问题,而不是 CPU 或 GC 瓶颈

关键点在于:双锁 ≠ 双倍开销。两把 ReentrantLock 各自管理自己的 Condition,没有共享状态竞争,也没有锁升级或重入冲突。

count 字段为什么必须是 AtomicInteger?

count 是唯一被两个锁共同读写的共享变量,但它不加锁访问——全靠 AtomicInteger 的 CAS 保证原子性与可见性。

  • put() 中调用 count.getAndIncrement():先读当前值,再原子+1,返回旧值(用于后续判断是否需唤醒)
  • take() 中调用 count.getAndDecrement():同理,返回旧值用于判断是否需唤醒

如果换成普通 int + 手动同步:

  • 每次读写都要进锁,反而把双锁优势抵消掉
  • 还可能因锁顺序引发死锁(比如某线程先持 putLock 再试图获取 takeLock)

性能影响很直接:在 16 核机器上实测,LinkedBlockingQueue 的吞吐量比单锁队列高 2.3 倍左右(JDK 17,100 万元素/秒级压测),主要就赢在 count 的无锁读写上。

signalNotEmpty() 和 signalNotFull() 为什么不能省略?

这两个方法不是“锦上添花”,而是防止线程永久挂起的核心机制:

  • signalNotEmpty() 在 put() 结束时调用,仅当 c == 0(即插入前队列为空)才触发:意味着至少有一个消费者正在 notEmpty.await() 等待,必须唤醒它
  • signalNotFull() 在 take() 结束时对称调用,仅当 c == capacity(即取出前队列已满)才触发

容易踩的坑:

  • 自己手写双锁队列时,漏掉“插入后队列从空变非空”这个边界判断,导致消费者一直阻塞,哪怕队列里已有数据
  • 把 signal() 放在锁外执行(比如放在 finally 块之后):可能因异常跳过,造成唤醒丢失
  • 用 signalAll() 替代 signal():唤醒所有等待线程,但多数是无效唤醒,徒增调度开销

注意:signal() 必须在对应锁持有状态下调用,否则抛 IllegalMonitorStateException。

容量为 Integer.MAX_VALUE 时,双锁还有意义吗?

有,而且更关键。

默认无界构造(new LinkedBlockingQueue())本质是容量 = Integer.MAX_VALUE,此时:

  • put() 永远不会触发 notFull.await(),但 putLock 仍要保护链表尾插、count 更新、last 指针移动
  • take() 仍可能阻塞(队空时),takeLock 仍要保护头节点摘除、count 减量、head 推进

也就是说:无界 ≠ 无锁竞争。链表操作本身不是原子的,Node 插入需要保证 last 指针更新的可见性与顺序性,这依然依赖 putLock 的排他语义。

被忽略的复杂点:

  • last 和 head 都是 volatile 字段,但它们的更新时机由各自锁控制,不是靠 volatile 本身实现线程安全
  • 如果真想彻底避免锁(比如追求极致吞吐),得换 ConcurrentLinkedQueue,但它不提供阻塞语义,take() 类操作只能轮询 + Thread.sleep(),延迟不可控

双锁设计的价值,从来不在“能不能无限扩容”,而在于“能不能让生产与消费真正错开临界区”。这点,无论容量大小,始终成立。

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

2023.06.15

11277

6

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

2023.07.05

8402

9

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

2023.07.31

7232

8

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

2023.08.01

1204

3

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

2023.08.02

968

3

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

2023.08.02

1456

5

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

2023.08.02

2969

5

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

2023.08.03

20231

3

配置java环境变量
配置java环境变量

配置Java环境变量是为了让操作系统能够识别和使用Java的相关命令和功能。本专题为大家提供配置java环境变量相关文章,帮助大家解决问题。

2023.08.03

1315

8

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
热门推荐
/
最新课程
phpStudy极速入门视频教程
phpStudy极速入门视频教程

共6课时 | 54.7万人学习

独孤九贱(4)_PHP视频教程
独孤九贱(4)_PHP视频教程

共89课时 | 134万人学习