禅与计算机 禅与计算机
首页
  • Java基础

    • 一文搞懂Java核心技术
    • Java面向对象知识点大总结,建议收藏
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
    • Java异常:从原理到实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
    • 探索JVM的隐秘角落:元空间详解
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 元认知

    • 摩擦感:AI时代的写作自省
    • 从断墨寻径浅谈程序员的元学习能力
    • AI时代专注力培养
    • 如何阅读一本书:技术书籍的读书笔记方法论
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • Elasticsearch核心原理与架构设计
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
  • AI工具链

    • Claude Code 实战指南:从安装配置到企业级开发流程
    • 一次 Claude Code 启动失败的 AI 辅助排查复盘
    • Claude Code 记忆管理:CLAUDE.md 最佳实践
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 一文搞懂Java核心技术
    • Java面向对象知识点大总结,建议收藏
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
    • Java异常:从原理到实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
    • 探索JVM的隐秘角落:元空间详解
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 元认知

    • 摩擦感:AI时代的写作自省
    • 从断墨寻径浅谈程序员的元学习能力
    • AI时代专注力培养
    • 如何阅读一本书:技术书籍的读书笔记方法论
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • Elasticsearch核心原理与架构设计
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
  • AI工具链

    • Claude Code 实战指南:从安装配置到企业级开发流程
    • 一次 Claude Code 启动失败的 AI 辅助排查复盘
    • Claude Code 记忆管理:CLAUDE.md 最佳实践
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础

  • 并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • 浅谈传统并发编程的优化思路
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • 浅谈池化技术的优雅关闭
    • 浅谈守护线程与进程优雅关闭
    • 浅谈并发编程等待通知模型
      • 引言
      • 同步锁下的等待通知模型
        • 状态依赖性的管理
        • 基于异常式的队列模型
        • 轮询检测式的等待唤醒
        • 基于条件等待的有界缓存
      • 关于条件谓词的一些探讨
        • 条件谓词的使用方式
        • 过早的唤醒或错误唤醒
        • notify下的信号丢失问题
        • 基于条件变量下的等待通知模型
      • 小结
      • 参考
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 深入理解synchronized同步机制
    • 详解JUC包下的锁
    • 详解JUC包下各种锁的使用
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • 详解Java并发流程控制工具
    • Java并发容器总结
    • 深入解析CopyOnWriteArrayList
    • 详解Java并发编程volatile关键字
    • 聊聊JVM中安全点的概念
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步IO密集型任务最佳实践
    • CompletableFuture异步多任务最佳实践
    • CompletableFuture组合流水线任务实践
    • 实现一个简单实用的的并发同步模型
    • 解决Java并发问题的常见思路
    • 来聊一个有趣的限流器RateLimiter
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 记一个ConcurrentHashMap使用不当导致的并发事故
    • ArrayBlockingQueue源码分析
    • PriorityQueue源码分
    • DelayQueue源码解析
  • JVM相关

  • 深入理解Spring框架

  • Java核心技术
  • 并发编程
sharkchili
2025-11-17
目录

浅谈并发编程等待通知模型

# 引言

为避免轮询条件为真的开销,并发编程中常用等待通知模型来优化这一点,而本文将针对等待通知模型这一知识点进行深入剖析,希望对你有所启发。

你好,我是 SharkChili ,禅与计算机程序设计艺术布道者,希望我的理念对您有所启发。

📝 我的公众号:写代码的SharkChili
在这里,我会分享技术干货、编程思考与开源项目实践。

🚀 我的开源项目:mini-redis
一个用于教学理解的 Redis 精简实现,欢迎 Star & Contribute:
https://github.com/shark-ctrl/mini-redis (opens new window)

👥 欢迎加入读者群
关注公众号,回复 【加群】 即可获取联系方式,期待与你交流技术、共同成长!

# 同步锁下的等待通知模型

# 状态依赖性的管理

在经典的生产者和消费者模式中,我们经常用到ArrayBlockingQueue作为并发安全的有界缓存,而该有界缓解进行读写操作时都必须严格按照如下两个条件谓语时机执行,即:

  1. 针对阻塞队列进行元素存操作时,有界缓存必须有空闲空间,即可非满
  2. 针对阻塞队列进行取操作时,有界队列必须有元素,即非空

基于上述的说法,我们基于同步锁synchronized实现了一个数组形式的环形阻塞队列的核心方法模板,大体思路为:

  1. 当进行元素存操作时,互斥调用doPut函数,判断是否到达数组末端,若到达则直接将元素存到索引0,并累加count
  2. 进行元素取操作时,互斥上锁执行doTake,同样执行步骤1的边界判断,完成后扣减count
  3. 基于count判断非空和非满

我们的环形有界队列是用数组实现的,所以笔者也用数组直观的展现这个流程,当然读者可以在逻辑上将数组首位相接,即可构成一个环形队列:

对应的笔者也给出这个环形队列的抽象模板,核心函数思路和上述基本一致,读者可结合图文注释了解大体流程,后文将基于该模板落地一个支持阻塞等待空闲通知线程存取元素的缓存队列:

public abstract class BaseBoundedBuffer<V> {
    private final V[] items;
    private int head;
    private int tail;
    private int count;

    /**
     * 初始化环形有界队列
     *
     * @param capacity 容量
     */
    protected BaseBoundedBuffer(int capacity) {
        items = (V[]) new Object[capacity];
    }

    protected synchronized final void doPut(V v) throws InterruptedException {
        //尾节点添加元素
        items[tail] = v;
        //如果到达数组末端,则重新从0开始
        if (++tail == items.length) {
            tail = 0;
        }
        //累加元素个数
        count++;
    }

    protected synchronized final V doTake() throws InterruptedException {
        //头节点取元素
        V v = items[head];
        //头节点置空实现删除
        items[head] = null;
        if (++head == items.length) {//如果到达边界,则循环从0开始
            head = 0;
        }
        //减元素个数
        count--;
        return v;
    }

    public synchronized final boolean isFull() {
        return count == items.length;
    }

    public synchronized final boolean isEmpty() {
        return count == 0;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 基于异常式的队列模型

我们先来看看第一个有界缓存的基本实现,一旦触发如下两个条件时,该缓存就会抛出异常:

  1. 获取元素时队列空
  2. 插入元素时队列满

对应落地代码如下,直接继承有界队列后落地落采用异常通知方式实现元素存取的缓存队列:

public class GrumpyBoundedBuffer extends BaseBoundedBuffer<Integer> {


    protected GrumpyBoundedBuffer(int capacity) {
        super(capacity);
    }

    public synchronized void put(int value) throws Exception {
        //队列满了,直接抛出异常
        if (isFull()) {
            throw new RuntimeException("queue is full");
        }
        //队列没满,正常入队
        doPut(value);

    }


    public synchronized int take() throws Exception {
        //队列为空,直接抛出异常
        if (isEmpty()) {
            throw new RuntimeException("queue is empty");
        }
        //队列不为空,正常出队
        return doTake();
    }


   

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

虽然这种方式使得缓存在实现非常的简单,但是这种方案对于使用者来说非常的不友好,在业务正常的情况下,即使存取消费的缓存在单位时间满即直接抛出异常告知线程不可存取,让使用者手动捕获异常进行重试:

public static void main(String[] args) {
        GrumpyBoundedBuffer grumpyBoundedBuffer = new GrumpyBoundedBuffer(1);
        ThreadUtil.execAsync(() -> {
            while (true) {
                try {
                    grumpyBoundedBuffer.put(1);
                } catch (Exception e) {
                    Console.error("队列已满,1s后重试");
                    ThreadUtil.sleep(1000);
                }
            }
        });
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

输出结果如下所示,非常的不方便:

# 轮询检测式的等待唤醒

于是我们就考虑在队列存储上在一个重试的的机制,即当队列存取失败时,进行休眠重试,直到成功后返回。

但是对于程序的性能表现而言,也是一种灾难,这种做法设计释放锁之后的休眠和循环重试,这就使得设计者需要在CPU使用率和响应性之间做好权衡:

  1. 如果设置休眠时间相对短,那么重试就会尽可能快,响应性就会越高,但是循环带来的CPU资源的开销却急剧增加。
  2. 如果休眠时间设置过长,有概率完成任务处理,但是却来响应的延迟。
public class SleepyBoundedBuffer extends BaseBoundedBuffer<Integer> {


    protected SleepyBoundedBuffer(int capacity) {
        super(capacity);
    }

    /**
     * 轮询重试,直到成功
     *
     * @param value
     * @throws InterruptedException
     */
    public synchronized void put(int value) throws InterruptedException {
        while (true) {
            synchronized (this) {
                if (!isFull()) {
                    doPut(value);
                }

            }
            Console.log("队列已满,500ms后重试");
            ThreadUtil.sleep(500);
        }

    }


    public synchronized int take() throws InterruptedException {

        while (true) {
            synchronized (this) {
                if (!isEmpty()) {
                    return doTake();
                }
            }
            Console.log("队列已空,500ms后重试");
            ThreadUtil.sleep(500);
        }
    }

   

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

这种方案一定程度解决用户手动捕获异常重试的繁琐,但也存在着如下缺点:

  1. 重试时休眠间隔500ms可能太长也可能太短,固定值等待非常不合理
  2. 频繁循环重试使得线程大量时间得到CPU时间片做一些无用功
  3. 重试多次无果后无法中断

# 基于条件等待的有界缓存

所以我们需要进行进一步的优化即通过如下两个列条件谓语避免线程无用的轮询开销:

  1. 当队列满的时候,当前存线程阻塞等待,直到队列非空时被唤醒
  2. 当队列空的时候,取线程阻塞等待,知道队列有元素时将其唤醒

总结起来就是一句话,非满的时候唤醒存线程尝试存元素,非空的时候通知取线程取元素,由此得出如下两个条件谓语isNotFull和isNotEmpty:

所以我们需要以object中对应的wait、notify和notifyAll构成内部条件队列的交互通知,当然要调用这些通知方法的前提也就是需要获取当前这个对象的锁。

以我们有界缓存存元素操作为例,我们执行添加操作时执行步骤为:

  1. 获得这个对象的锁
  2. 当发现缓存空间已满即不符合检测条件时,则调用当前对象(有界缓存)的wait方法将当前线程挂起
  3. 与此同时,线程也会释放这把锁,等待队列非满时通过notify或者notifyAll尝试将当前线程唤醒。

对应我们给出代码示例,这种方式相比于休眠的方案,改进了响应的效率和CPU使用率的开销,避免了非必要的检测步骤:

public class BoundedBuffer extends BaseBoundedBuffer<Integer> {
    protected BoundedBuffer(int capacity) {
        super(capacity);
    }


    public synchronized void put(int value) throws InterruptedException {
        if (isFull()) {
            Console.log("队列已满,等待");
            wait();
        }
        Console.log("队列非满,开始写入");
        doPut(value);

        //通知阻塞线程消费
        notifyAll();

    }

    public synchronized int take() throws InterruptedException {
        if (isEmpty()) {
            Console.log("队列已空,等待");
            wait();
        }

        int value = doTake();
        //通知阻塞线程写入    
        notifyAll();

        return value;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

对应的笔者以线程调试模式给出下面这段代码,在首先让线程1执行两次写操作,查看是否在第二次阻塞是否会在消费者线程消费后存入,所以笔者也会在两个线程执行完毕后,判断队列非空来查看是否实现这一点:

 //创建一个容量为1的缓冲区
        BoundedBuffer boundedBuffer = new BoundedBuffer(1);

        CountDownLatch countDownLatch = new CountDownLatch(2);

        //启动写入线程第一次写入成功,第二次写入阻塞,直到消费者线程完成消费
        new Thread(() -> {
            try {
                boundedBuffer.put(1);
                boundedBuffer.put(2);
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(() -> {
            try {
                ThreadUtil.sleep(1000);
                Console.log("take:{}", boundedBuffer.take());
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException();
            }
        }).start();

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //通过非空函数判断线程1第二个元素是否存成功
        Console.log("main线程结束:{}", boundedBuffer.isEmpty());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

对应输出结果如下,可以看到第二次写入因为队列满而阻塞,一旦消费者完成消费后,生产者就立刻被唤醒写入:

# 关于条件谓词的一些探讨

# 条件谓词的使用方式

要想正确的使用条件队列,就需要正确的抓住线程与条件谓语之间的关联,保证合适的条件下当线程添加至条件队列,并在合适的时机将其唤醒,以我们的本文一直在强调的有界队列:

  1. 对于put方法来说:只有条件非满的情况下,才能添加元素至队列
  2. 对于take方法来说,只有条件非空的情况下,才能取出元素

同时,每一次wait的调用都会将调用者隐式的和条件队列加以关联,例如:

  1. 调用有界缓存的take方法时,若没有元素,当前线程调用wait阻塞存入监视锁底层的waitSet
  2. 调用有界缓存put方法时,若空间已满,当前线程调用wait存入监视锁底层的waitset

当然这一切的都有一个前提,即调用者已经获取到的当前wait方法对应的对象的监视锁,这是并发互斥中等待通知模型有序协调的一个必要条件:

# 过早的唤醒或错误唤醒

对条件队列有了基本的概念之后,我们再来更进一步的探讨这套设计理念,实际上按照目前的设计来看,这套等待唤醒模型还是存在一定的缺陷,即多条件关联单监视锁导致的错误唤醒问题。

举个例子,假设基于我们要上述的有界缓存队列,我们打算增加一个关闭有界缓存的操作,即直接起一个线程查看shutdownFlag如果为false则挂起等待,当其他线程将shutdownFlag设置为true的时候将其唤醒,对应的我们也给出下面这样一段代码:

public synchronized void shutdown() {
        isShuttingDown = true;
        notifyAll();
    }

    private volatile boolean isShuttingDown = false;
    public synchronized void shutdownIfInNeed() throws InterruptedException {
        if (isShuttingDown == false) {
            wait();
            Console.log("关闭线程被唤醒");
        }

        //执行阻塞队列中断和关闭所有线程的操作
        //......
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

对此我们试想这样一个情况,我们现在有一个上界为1的有界队列,对应3个线程按如下顺序执行:

  1. 消费者线程尝试从有界缓存获取元素,阻塞等待唤醒
  2. 停止线程发现停止标识为false,阻塞等待唤醒
  3. 生产者线程存入元素,队列有新元素,调用notifyall通知消费者消费

重点来了,停止线程和消费者线程都处于当前监视锁的等待队列中,所以notifyall操作可能会误唤醒停止线程将队列消费和所有线程中断造成系统崩溃。

除此之外处于wait的线程还可能会被错误的唤醒即没有任何征兆的情况下苏醒被CPU时间片执行,引用《java并发编程实战中》的说法:

以 “早餐” 烤面包机烤面包完成后通知人们食用为例 , 这就好⽐烤⾯包机的线 路 连 接 有 问 题 , 有时候当⾯包还未烤 时 , 铃声 就 响起来了

对应的我们也给出这个案例的代码:

public static void main(String[] args) {
        //创建一个容量为1的缓冲区
        BoundedBuffer boundedBuffer = new BoundedBuffer(1);

        CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            try {
                //线程0取元素阻塞
                Console.log("take:{}", boundedBuffer.take());
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException();
            }
        }, "t0").start();


        new Thread(() -> {
            try {
                //线程1查看停止信号为false阻塞
                boundedBuffer.shutdownIfInNeed();
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1").start();


        new Thread(() -> {
            try {
                //线程2put操作队列非空执行通知操作,导致停止线程被错误的唤醒
                boundedBuffer.put(1);
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t2").start();

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        
        Console.log("main线程结束:{}", boundedBuffer.size());

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

输出结果如下,可以看到在生产者生产元素后的通知动作,把关闭线程给唤醒了,这就是经典的错误唤醒:

队列已空,take线程:t0等待
队列非满,开始写入
关闭线程被唤醒
take线程 t0被唤醒
take:-1
main线程结束:1
1
2
3
4
5
6

本质原因就是一个监视锁中的队列关联多个条件,使得在多条件的等待通知场景下存在错误通知的情况,考虑到这一点,无论是对于put、take还是shutdown方法,我们都需要进行改进,确保:

  1. 生产者被唤醒后,进行必要的非满检查,且只有将空队列存入元素后通知消费者
  2. 消费者被唤醒后,进行必要的非空检查,只有将非空队列消费空之后,通知生产者
  3. shutdown线程被唤醒后,进行必要的状态标识检查,只有状态标识为true才能停止线程

改进后的代码如下所示,可以看到笔者将if条件判断后wait的操作改为while+wait操作确保唤醒后的再确认:

public synchronized void put(int value) throws InterruptedException {
        while (isFull()) {//条件触发时循环检测一下
            wait();
        }
        //空变为非空
        boolean wasEmpty = isEmpty();
        doPut(value);

        if (wasEmpty) {//仅当空变为非空时才通知
            notifyAll();
        }
    }

    public synchronized int take() throws InterruptedException {
        while (isEmpty()) {
            wait();
        }
        //满变为非满才通知
        boolean wasFull = isFull();
        int value = doTake();
        if (wasFull) {
            notifyAll();
        }
        return value;
    }


    public synchronized void shutdownIfInNeed() throws InterruptedException {
        while (isShuttingDown == false) {
            wait();
            Console.log("关闭线程被唤醒");
        }

        //执行阻塞队列中断和关闭所有线程的操作
        //......
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# notify下的信号丢失问题

我们再来说说通知的哲学,刚接触java这门语言的时候,都会了解到notify和notifyAll的区别,这一点我们也可以直接从源码的注释上了解这一点,即前者仅仅通知监视锁下的单个线程而后者则是所有线程:

1. notify:Wakes up a single thread that is waiting on this object's monitor.
2. notifyAll:Wakes up all threads that are waiting on this object's monitor. A thread waits on an object's monitor by calling one of the wait methods.
1
2

所以这也就是为什么笔者在实现上述通知这个动作的时候,使用的是notifyAll而非notify,即notify存在信号丢失问题,还是用我上述的生产者-消费者和异步关闭线程的例子,试想下述场景:

  1. 有界队列元素空间为1
  2. 线程1取元素为空,阻塞
  3. 线程2查看停止标识为false,阻塞
  4. 线程0添加元素,元素非空,notify选中了线程2
  5. 本该处理元素的线程1因为没收到通知,造成了一种信号丢失的情况

这本质就是同步锁和wait以及条件谓语上一种设计缺陷,即一个同步锁只能关联一组条件队列,而条件队列无法做区分。

所以基于上述条件队列的案例,我们通过条件通知的方式进行比对保证更高效的准确的通知,避免每次操作之后都非常激进的通知所有线程造成非必要的上下文切换开销,当然读者在进行这样的优化时务必记得,只有保证程序可以使用的情况下,在进行优化的哲学:

# 基于条件变量下的等待通知模型

内置队列存在一个内置锁关联多个条件队列的情况,这使得很多线程被错误的唤醒,导致非必要的CPU时钟消耗和上下文切换和并发竞争锁的开销。针对上述的问题,我们必须到借由一种工具保证同一把锁下的各个条件的线程都放置到不同的队列中,从而保证正确的唤醒,即:

  1. 等待队列非满的生产者线程存到一个队列,待消费者完成元素消费后通知这个队列
  2. 等待队列非空的消费者线程存到一个等待队列,待生产者完成元素投递后通知这个队列

所以,通过juc包下的锁即可实现将条件放到不同的条件队列中,同时它还能可以实现队列内部公平的唤醒,以保证等待唤醒模型调度的有序性,以及减小非必要的上下文切换的开销:

public class ConditionBoundedBuffer<V> {


    private final V[] items;
    private int head;
    private int tail;
    private int count;
    //下述两个条件队列关联同一把锁,线程按照各自条件与队列关联
    private final ReentrantLock lock = new ReentrantLock();
    //生产者等待队列非满的等待队列
    private final Condition notFull = lock.newCondition();
    //消费者等待队列非空的等待队列
    private final Condition notEmpty = lock.newCondition();

    public ConditionBoundedBuffer(int capacity) {
        this.items = (V[]) new Object[capacity];
    }


    public boolean isFull() {
        return count == items.length;
    }

    public boolean isEmpty() {
        return count == 0;
    }


    public void put(V v) throws InterruptedException {
        lock.lock();
        try {

            while (isFull()) {//轮询检测非满
                notFull.await();
            }
            //添加元素
            items[tail++] = v;
            count++;
            if (tail == items.length) {
                tail = 0;
            }
            notEmpty.signal();


        } finally {
            lock.unlock();
        }
    }


    public V take() throws InterruptedException {

        lock.lock();
        try {
            while (isEmpty()) {//轮询检测非空
                notEmpty.await();
            }
            //消费元素
            V v = items[head];
            items[head] = null;
            head++;
            count--;
            if (head == items.length) {
                head = 0;
            }
            notFull.signal();
            return v;
        } finally {
            lock.unlock();
        }
    }


  

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

对应的我们也给出压测代码,最终断言也是正确的:

ConditionBoundedBuffer<Integer> conditionBoundedBuffer = new ConditionBoundedBuffer<>(1);

        ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1);


        for (int i = 0; i < 100_0000; i++) {
            //提交1一个元素
            threadPool.execute(() -> {
                try {
                    conditionBoundedBuffer.put(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            //消费一个元素
            threadPool.execute(() -> {
                try {
                    conditionBoundedBuffer.take();

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        threadPool.shutdown();
        while (!threadPool.isTerminated()) {
        }
        //判断并发下线程是否正确的对等生产和消费
        Assert.equals(conditionBoundedBuffer.count, 0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 小结

自此我们针对并发编程中的等待通知模型中的状态管理,等待通知原则和技巧进行了深入的分析和演示,希望对你有帮助。

你好,我是 SharkChili ,禅与计算机程序设计艺术布道者,希望我的理念对您有所启发。

📝 我的公众号:写代码的SharkChili
在这里,我会分享技术干货、编程思考与开源项目实践。

🚀 我的开源项目:mini-redis
一个用于教学理解的 Redis 精简实现,欢迎 Star & Contribute:
https://github.com/shark-ctrl/mini-redis (opens new window)

👥 欢迎加入读者群
关注公众号,回复 【加群】 即可获取联系方式,期待与你交流技术、共同成长!

# 参考

《java并发编程实战》

编辑 (opens new window)
上次更新: 2025/12/21, 15:09:04
浅谈守护线程与进程优雅关闭
synchronized关键字使用指南

← 浅谈守护线程与进程优雅关闭 synchronized关键字使用指南→

最近更新
01
Claude Code 记忆管理:CLAUDE.md 最佳实践
04-24
02
Claude Code 实战指南:从安装配置到企业级开发流程
04-20
03
一次 Claude Code 启动失败的 AI 辅助排查复盘
04-18
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×