禅与计算机 禅与计算机
首页
  • 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中的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技术原理
  • 深入理解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技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到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

    • 从Lucene到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插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
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中的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技术原理
  • 深入理解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技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到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

    • 从Lucene到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插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础

  • 并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • 浅谈传统并发编程的优化思路
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • 浅谈池化技术的优雅关闭
    • 浅谈守护线程与进程优雅关闭
    • 浅谈并发编程等待通知模型
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 深入理解synchronized同步机制
    • 详解JUC包下的锁
    • 详解JUC包下各种锁的使用
      • 写在文章开头
      • Java中的锁
      • 详解Lock接口
        • 为什么需要Lock接口式的锁
        • Lock接口的基本规范
        • 使用Lock的优雅姿势
        • tryLock
        • 可被中断的lock
        • Lock锁的可见性保证
      • 详解不同分类的锁以及使用
        • 按照是否锁住资源分类
        • 悲观锁
        • 乐观锁
        • 悲观锁和乐观锁的比较
        • 按照是否可重入进行锁分类
        • 可重入锁示例
        • 不可重入锁
        • 源码解析可重入锁和非可重入锁区别
        • 公平锁和非公平锁
        • 公平锁代码示例
        • 通过源码查看两者实现逻辑
        • 小结一下公平锁和非公平锁
        • 共享锁和非共享锁
        • 读写锁使用示例
        • 读写锁非公平场景下的插队问题
        • 源码解析非公平锁插队原理
        • 自旋锁和非自旋锁
        • 自旋锁代码示例
        • 可中断锁和非可中断锁
      • 使用锁的注意事项
        • 缩小同步代码块,尽量不要锁住方法,减少锁的粒度
        • 锁中尽量不要包含锁
        • 选择合适的锁类型
      • 参考文献
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • 详解Java并发流程控制工具
    • Java并发容器总结
    • 深入解析CopyOnWriteArrayList
    • 详解Java并发编程volatile关键字
    • 聊聊JVM中安全点的概念
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步IO密集型任务最佳实践
    • CompletableFuture异步多任务最佳实践
    • CompletableFuture组合流水线任务实践
    • 实现一个简单实用的的并发同步模型
    • 解决Java并发问题的常见思路
    • 来聊一个有趣的限流器RateLimiter
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 记一个ConcurrentHashMap使用不当导致的并发事故
    • JS设计模式总结笔记
    • 小程序笔记
    • 《JavaScript教程》笔记
  • JVM相关

  • 深入理解Spring框架

  • Java核心技术
  • 并发编程
sharkchili
2026-03-25
目录

详解JUC包下各种锁的使用

[toc]

# 写在文章开头

在多线程编程中,线程安全是一个无法回避的核心问题。Java的java.util.concurrent(JUC)包为开发者提供了一系列强大的工具,其中各种锁的实现尤为关键。从经典的ReentrantLock到高效的StampedLock,再到灵活的ReadWriteLock,JUC包中的锁机制不仅能够帮助我们解决并发问题,还能显著提升程序的性能和可扩展性。然而,面对如此丰富的锁类型,如何选择合适的锁、如何避免常见的并发陷阱,以及如何优化锁的使用,都是开发者需要深入掌握的技能。本文将详细解析JUC包中各种锁的使用场景、实现原理以及最佳实践,助你在多线程编程中游刃有余,轻松应对复杂的并发挑战。

# Java中的锁

我们日常开发过程中为了保证临界资源的线程安全可能会用到synchronized,但是synchronized局限性也是很强的,它无法做到以下几点:

  1. 让当前线程立刻释放锁。
  2. 判断线程持有锁的状态。
  3. 线程争抢的公平争抢。

所以为了保证用户能够在合适的场景找到合适的锁,Java设计者按照不同的维度为我们提供了各种锁,锁的分类按照不同的特征分为以下几种:

在这里插入图片描述

# 详解Lock接口

# 为什么需要Lock接口式的锁

锁是一种解决资源共享问题的解决方案,相比于synchronized锁,Lock锁的自类增加了一些更高级的功能:

  1. 锁等待。
  2. 锁中断。
  3. 可随时中断释放。
  4. 锁重入。

但这并不能表明,Lock锁是synchronized锁的替代品,它俩都有各自的适用场合。

# Lock接口的基本规范

宏观角度来看Lock接口不仅支持简单的上锁和释放锁,还支持超时等待锁、上可中断锁,锁中断等操作:

public interface Lock {
	//上锁
    void lock();

   	//上一把可中断的锁
    void lockInterruptibly() throws InterruptedException;

 	//非阻塞尝试取锁
    boolean tryLock();

  	//超时等待锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

  	//锁释放
    void unlock();

 	//......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 使用Lock的优雅姿势

我们以ReentrantLock来演示一下Lock类的加锁和解锁操作。细心的读者在阅读源码时可能会发现下面这样一段注释,这就是lock类上锁的解锁的基本示例了。

*  <pre> {@code
 * class X {
 *   private final ReentrantLock lock = new ReentrantLock();
 *   // ...
 *
 *   public void m() {
 *     lock.lock();  // block until condition holds
 *     try {
 *       // ... method body
 *     } finally {
 *       lock.unlock()
 *     }
 *   }
 * }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

所以我们也按照上面这段示例编写一下一段demo代码。注意lock锁必须手动释放,所以为了保证释放的安全我们常常会在finally语句块中进行锁释放,如官方给出的代码示例一样:

ReentrantLock lock = new ReentrantLock();
        //上锁
        lock.lock();

        try {
            System.out.println("当前线程" + Thread.currentThread().getName() + "获得锁,进行异常操作");
            int i = 1 / 0;

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //语句块中优雅释放
            lock.unlock();
        }

        log.info("当前锁是否被锁定:{}", lock.isLocked());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

对应的我们也给出输出结果:

当前线程main获得锁,进行异常操作
15:41:05.499 [main] INFO com.sharkChili.Main - 当前锁是否被锁定:false
java.lang.ArithmeticException: / by zero
	at com.sharkChili.Main.main(Main.java:23)
1
2
3
4

# tryLock

相比于普通的lock来说,tryLock相对更加强大一些,tryLock可以根据当前线程是否取得锁进行一些定制化操作。 而且tryLock可以立即返回或者在一定时间内取锁,如果拿得到就拿锁并返回true,反之返回false。

我们现在创建一个任务给两个线程使用,逻辑很简单,在每个线程在while循环中,flag为1的先取锁1,flag为2的先取锁2。 flag为1的先在规定时间内获取锁1,获得锁1后再获取锁2,如果锁2获取失败则释放锁1休眠一会。让另一个先获取锁2在获取锁1的线程执行完再进行获取锁。

public class TryLockDemo implements Runnable {

    //注意使用static 否则锁的粒度用错了会导致无法锁住彼此
    private static Lock lock1 = new ReentrantLock();
    private static Lock lock2 = new ReentrantLock();

    //flag为1的先取锁1再去锁2,反之先取锁2在取锁1
    private int flag;


    public int getFlag() {
        return flag;
    }

    public void setFlag(int flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        while (true) {
            //flag为1先取锁1再取锁2
            if (flag == 1) {
                try {
                    //800ms内尝试取锁,如果失败则直接输出尝试获取锁1失败
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println(Thread.currentThread().getName()+"拿到了第一把锁lock1");
                            //睡一会,保证线程2拿锁锁2
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println(Thread.currentThread().getName()+"取到锁2");
                                    System.out.println(Thread.currentThread().getName()+"拿到两把锁,执行业务逻辑了。。。。");
                                    break;
                                } finally {
                                    lock2.unlock();

                                }
                            } else {
                                System.out.println(Thread.currentThread().getName()+"获取第二把锁锁2失败");
                            }
                        } finally {
                            //休眠一会再次获取锁
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));

                        }


                    } else {
                        System.out.println(Thread.currentThread().getName()+"尝试获取锁1失败");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            } else {
                try {
                    //3000ms内尝试获取锁2,如果娶不到直接输出失败
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        try{
                            System.out.println(Thread.currentThread().getName()+"先拿到了锁2");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println(Thread.currentThread().getName()+"取到锁1");
                                    System.out.println(Thread.currentThread().getName()+"拿到两把锁,执行业务逻辑了。。。。");
                                    break;
                                } finally {
                                    lock1.unlock();

                                }
                            } else {
                                System.out.println(Thread.currentThread().getName()+"获取第二把锁1失败");
                            }
                        }finally {
                            //休眠一会,顺便把锁释放让其他线程获取
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));

                        }

                    } else {
                        System.out.println(Thread.currentThread().getName()+"获取锁2失败");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

测试代码

public class TestTryLock {
    public static void main(String[] args) {
        //先获取锁1
        TryLockDemo t1 = new TryLockDemo();
        t1.setFlag(1);

        //先获取锁2
        TryLockDemo t2 = new TryLockDemo();
        t2.setFlag(2);

        new Thread(t1,"t1").start();
        new Thread(t2,"t2").start();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出结果如下,可以看到tryLock的存在使得我们可以不再阻塞的去获取锁,而是可以根据锁的持有情况进行下一步逻辑。

t1拿到了第一把锁lock1
t2先拿到了锁2
t1获取第二把锁锁2失败
t2取到锁1
t2拿到两把锁,执行业务逻辑了。。。。
t1拿到了第一把锁lock1
t1取到锁2
t1拿到两把锁,执行业务逻辑了。。。。
1
2
3
4
5
6
7
8

# 可被中断的lock

为避免synchronized这种获取锁过程无法中断,进而出现死锁的情况。JUC包下的锁提供了lockInterruptibly方法,即在获取锁过程中的线程可以被打断。

public class LockInterruptiblyDemo implements Runnable {


    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 尝试取锁");
        try {
            //设置为可被中断的获取锁
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + " 取锁成功");
                Thread.sleep(5000);

            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 执行业务逻辑时被中断");

            } finally {
                lock.unlock();
            }

        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "尝试取锁时被中断");
        }

    }
}

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

测试代码如下,我们先让线程1获取锁成功,此时线程2取锁就会失败,我们可以手动通过interrupt将其打断。

public class LockInterruptiblyTest {
    public static void main(String[] args) throws InterruptedException {
        LockInterruptiblyDemo lockInterruptiblyDemo = new LockInterruptiblyDemo();
        //线程1先获取锁,会成功
        Thread thread0 = new Thread(lockInterruptiblyDemo);
        thread0.start();

        //线程2获取锁失败,不会中断
        Thread thread1 = new Thread(lockInterruptiblyDemo);
        thread1.start();


        Thread.sleep(5000);

        //手动调用interrupt将线程中断
        thread1.interrupt();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Lock锁的可见性保证

可能很多人会对这些操作有这样的疑问,我们lock的结果如何对之后操作该资源的线程保证可见性呢?

其实根据happens-before原则,前一个线程操作的结果,对后一个线程是都可见的原理即可保证锁操作的可见性。

在这里插入图片描述

# 详解不同分类的锁以及使用

# 按照是否锁住资源分类

# 悲观锁

悲观锁认为自己在修改数据过程中,其他人很可能会过来修改数据,为了保证数据的准确性,他会在自己修改数据时候持有锁,在释放锁之前,其他线程是无法持有这把锁。在Java中synchronized锁和lock锁都是典型的悲观锁。

在这里插入图片描述

对应的我们给出悲观锁的使用示例:

public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        Thread t1 = new Thread(()->{
            doSomething();
            countDownLatch.countDown();
        });
        Thread t2 = new Thread(()->{
            doSomething();
            countDownLatch.countDown();
        });

        t1.start();
        t2.start();
        countDownLatch.await();


    }

    /**
     * synchronized 悲观锁,保证上锁成功后才能操作临界资源
     */
    public synchronized static void doSomething() {
        log.info("{} do something", Thread.currentThread().getName());
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 乐观锁

乐观锁认为自己的修改数据时不会有其他人会修改数据,所以他每次修改数据后会判断修改前的数据是否被修改过,如果没有就将更新结果写入,反之重新拉取数据的最新结果进行更新再重复之前步骤完成写入,在Java中乐观锁常常用CAS原子类来实现:

在这里插入图片描述

如下代码所示,原子类就是通过CAS乐观锁实现的:

 public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.incrementAndGet();
    }
1
2
3
4

我们可以看看cas原子类getAndIncrement的源码,它会调用unsafe的getAndAddInt,将this和偏移量,还有1传入。

public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
1
2
3

查看getAndAddInt的工作流程我们即可知晓CAS乐观锁操作的实现细节:

  1. 通过getIntVolatile方法获取到需要操作的变量的地址。
  2. 通过compareAndSwapInt的方式查看原有的值是否发生变化,如果没有则将修改后的结果v + delta写入到变量地址空间中。
  3. 如果发生变化则compareAndSwapInt会返回false,继续从步骤1开始,直到修改操作成功。

在这里插入图片描述

对应的我们给出getAndAddInt的源码实现:

public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
        	//拉取操作变量最新值
            v = getIntVolatile(o, offset);
            //比对拉取结果与最新结果是否一致,若一致则写入最新结果,反之继续循环直到修改成功
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }
1
2
3
4
5
6
7
8
9

# 悲观锁和乐观锁的比较

该问题我们可以从以下两个角度进行说明

  1. 从资源开销的角度:悲观锁的开销远高于乐观锁,但它确实一劳永逸的,临界区持有锁的时间就算越来越长也不会对互斥锁有任何的影响。反之乐观锁假如持有锁的时间越来越长的话,其他等待线程的自选时间也会增加,从而导致资源消耗愈发严重。
  2. 从场景适用角度:悲观更适合那些经常操作修改的场景,而乐观锁更适合读多修改少的情况。

# 按照是否可重入进行锁分类

# 可重入锁示例

代码如下所示,我们创建一个MyRecursionDemo ,这个类的逻辑很简单,让当前线程通过递归的方式连续获得锁5次。

public class MyRecursionDemo {
    private ReentrantLock lock = new ReentrantLock();

    public void accessResource() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 第" + lock.getHoldCount() + "次处理资源中");

            if (lock.getHoldCount() < 5) {
                System.out.println("当前线程是否是持有这把锁的线程" + lock.isHeldByCurrentThread());
                System.out.println("当前等待队列长度" + lock.getQueueLength());
                System.out.println("再次递归处理资源中........................................");
                //再次递归调用该方法,尝试重入这把锁
                accessResource();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("处理结束,释放可重入锁");
            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

测试代码

public class MyRecursionDemoTest {
    public static void main(String[] args) {
        MyRecursionDemo myRecursionDemo=new MyRecursionDemo();
        myRecursionDemo.accessResource();
    }
}

1
2
3
4
5
6
7

从输出结果来看main线程第一次成功取锁之后,在不释放的情况下,连续尝试取ReentrantLock 5次都是成功的,是支持可重入的。

main 第1次处理资源中
当前线程是否是持有这把锁的线程true
当前等待队列长度0
再次递归处理资源中........................................
main 第2次处理资源中
当前线程是否是持有这把锁的线程true
当前等待队列长度0
再次递归处理资源中........................................
main 第3次处理资源中
当前线程是否是持有这把锁的线程true
当前等待队列长度0
再次递归处理资源中........................................
main 第4次处理资源中
当前线程是否是持有这把锁的线程true
当前等待队列长度0
再次递归处理资源中........................................
main 第5次处理资源中
处理结束,释放可重入锁
处理结束,释放可重入锁
处理结束,释放可重入锁
处理结束,释放可重入锁
处理结束,释放可重入锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 不可重入锁

NonReentrantLock就是典型的不可重入锁,代码示例如下:

public class NonReentrantLockDemo {
    public static void main(String[] args) {
        NonReentrantLock lock=new NonReentrantLock();
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"第一次获取锁成功");
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"第二次获取锁成功");
    }
}

1
2
3
4
5
6
7
8
9
10

从输出结果来看,第一次获取锁之后就无法再次重入锁了。

main第一次获取锁成功

1
2

# 源码解析可重入锁和非可重入锁区别

查看ReentrantLock可重入锁源码可知,可重入锁进行锁定逻辑时,会判断持有锁的线程是否是当前线程,如果是则将c(即count的缩写)自增:

 final boolean nonfairTryAcquire(int acquires) {
          .....
            //如果当前线程仍然持有这把锁,记录一下持有锁的次数 并返回拿锁成功
            else if (current == getExclusiveOwnerThread()) {
            //增加上锁次数
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                    //更新当前锁上锁次数
                setState(nextc);
                return true;
            }
            return false;
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

相比之下不可重入锁的逻辑就比较简单了,如下源码NonReentrantLock所示,通过CAS修改取锁状态,若成功则将锁持有者设置为当前线程。 同一个线程再去取锁时并没有重入的处理,仍然是进行CAS操作,很明显这种情况是会失败的。

 	@Override
    protected final boolean tryAcquire(int acquires) {
    // 通过CAS修改锁状态
        if (compareAndSetState(0, 1)) {
        //若成功则将锁持有者设置为当前线程
            owner = Thread.currentThread();
            return true;
        }
        return false;
    }
1
2
3
4
5
6
7
8
9
10

# 公平锁和非公平锁

公平锁可以保证线程持锁顺序会有序进行,在线程争抢锁的过程中如果上锁失败是会统一提交到等待队列中,后续由队列统一管理唤醒:

在这里插入图片描述

非公平锁的设计初衷也很明显,非公平锁的设计就是为了在线程唤醒期间的空档期让其他线程可以插队,所以即使等待队列中有线程,其他的不在队列中的线程依然可以持有这把锁:

在这里插入图片描述

# 公平锁代码示例

我们先创建一个任务类的代码,run方法逻辑很简单,上一次锁打印输出一个文件,这里会上锁两次打印两次。构造方法中要求传一个布尔值,这个布尔值如果为true则说明ReentrantLock 为公平,反之为非公平。

public class MyPrintQueue implements Runnable {


    private boolean fair;

    public MyPrintQueue(boolean fair) {
        this.fair = fair;
    }

    /**
     * true为公平锁 false为非公平锁
     */
    private ReentrantLock lock = new ReentrantLock(fair);

    /**
     * 上锁两次打印输出两个文件
     */
    public void printStr() {
        lock.lock();
        try {
            int s = new Random().nextInt(10) + 1;
            System.out.println("正在打印第一份文件。。。。当前打印线程:" + Thread.currentThread().getName() + " 需要" + s + "秒");
            Thread.sleep(s * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        lock.lock();
        try {
            int s = new Random().nextInt(10) + 1;
            System.out.println("正在打印第二份文件。。。。当前打印线程:" + Thread.currentThread().getName() + " 需要" + s + "秒");
            Thread.sleep(s * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
        printStr();
    }
}

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

测试代码

public class FairLockTest {
    public static void main(String[] args) {

        //创建10个线程分别执行这个任务
        MyPrintQueue task=new MyPrintQueue(true);
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(task);
        }

        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
            try{
                Thread.sleep(100);
            }catch (Exception e){

            }
        }

    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

从输出结果来看,线程是按顺序执行的

正在打印第一份文件。。。。当前打印线程:Thread-0 需要2秒
正在打印第二份文件。。。。当前打印线程:Thread-0 需要8秒
正在打印第一份文件。。。。当前打印线程:Thread-1 需要1秒
正在打印第二份文件。。。。当前打印线程:Thread-1 需要8秒
正在打印第一份文件。。。。当前打印线程:Thread-2 需要2秒
正在打印第二份文件。。。。当前打印线程:Thread-2 需要9秒
正在打印第一份文件。。。。当前打印线程:Thread-3 需要10秒
正在打印第二份文件。。。。当前打印线程:Thread-3 需要2秒
正在打印第一份文件。。。。当前打印线程:Thread-4 需要10秒
正在打印第二份文件。。。。当前打印线程:Thread-4 需要1秒
正在打印第一份文件。。。。当前打印线程:Thread-5 需要5秒
正在打印第二份文件。。。。当前打印线程:Thread-5 需要8秒
正在打印第一份文件。。。。当前打印线程:Thread-6 需要9秒
正在打印第二份文件。。。。当前打印线程:Thread-6 需要6秒
正在打印第一份文件。。。。当前打印线程:Thread-7 需要9秒
正在打印第二份文件。。。。当前打印线程:Thread-7 需要8秒
正在打印第一份文件。。。。当前打印线程:Thread-8 需要6秒
正在打印第二份文件。。。。当前打印线程:Thread-8 需要6秒
正在打印第一份文件。。。。当前打印线程:Thread-9 需要6秒
正在打印第二份文件。。。。当前打印线程:Thread-9 需要4秒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

非公平锁将标志调整为false即可,这里就不多做演示了。

# 通过源码查看两者实现逻辑

如下所示,我们可以在构造方法中看到公平锁和非公平锁是如何根据参数决定的。

 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
1
2
3

我们不妨看看ReentrantLock公平锁的内部类公平锁FairSync的源码,如下所示,可以看到,他的取锁逻辑必须保证当前取锁的节点没有前驱节点才能抢锁,这也就是为什么我们的线程会排队取锁。

 static final class FairSync extends Sync {
       
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            //当前节点没有前驱节点的情况下才能进行取锁
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

相比之下,非公平锁就很粗暴了,我们看看ReentrantLock内部类NonfairSync,只要CAS成功就行了,所以锁一旦空闲,所有线程都可以随机争抢。

final void lock() {
			//无论队列情况,直接CAS成功后即可持有锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
1
2
3
4
5
6
7

# 小结一下公平锁和非公平锁

相对之下公平锁由于是有序执行,所以相对非公平锁来说执行更慢,吞吐量更小一些。 而非公平锁可以在特定场景下实现插队,所以很有可能出现某些线程被频繁插队而导致"线程饥饿"的情况。

# 共享锁和非共享锁

共享锁最常见的使用就是ReentrantReadWriteLock,他的读锁就是共享锁,当某一线程使用读锁时,其他线程也可以使用读锁,因为读不会修改数据,无论多少个线程读都可以。

而写锁就是独占锁的典型,当某个线程执行写时,为了保证数据的准确性,其他线程无论使用读锁还是写锁,都得阻塞等待当前正在使用写锁的线程释放锁才能执行。

在这里插入图片描述

# 读写锁使用示例

代码的逻辑也很简单,获取读锁读取数据,获取写锁修改数据。

public class BaseRWdemo {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    //读锁
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    //写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        //获取读锁,读取数据
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁");
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(Thread.currentThread().getName() + "释放了读锁");
            readLock.unlock();
        }

    }


    private static void write() {
        //获取写锁,写数据
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁");
            Thread.sleep(1000);
        } catch (Exception e) {

        }finally {
            System.out.println(Thread.currentThread().getName() + "释放了写锁");
            writeLock.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

测试代码:

public static void main(String[] args) {
        //读锁可以一起获取
        new Thread(() -> read(), "thread1").start();
        new Thread(() -> read(), "thread2").start();

        
        //等上面读完写锁才能用 从而保证线程安全问题
        new Thread(() -> write(), "thread3").start();
        //等上面写完 才能开始写 避免线程安全问题
        new Thread(() -> write(), "thread4").start();
    }
1
2
3
4
5
6
7
8
9
10
11

从输出结果不难看出,一旦资源被上了读锁,写锁就无法操作,只有读锁操作结束,写锁才能操作资源。

thread1得到读锁
thread2得到读锁
thread1释放了读锁
thread2释放了读锁


# 写锁必须等读锁释放了才能操作

thread3得到写锁
thread3释放了写锁
thread4得到写锁
thread4释放了写锁
1
2
3
4
5
6
7
8
9
10
11
12

# 读写锁非公平场景下的插队问题

读写锁ReentrantReadWriteLock设置为true,即公平锁,其底层也很上述的ReentrantLock类似,同样是通过AQS管理线程流程控制,同样是非公平情况下任意线程都可以直接尝试争抢锁而非,对应的代码示例如下,可以看到笔者初始化读写锁ReentrantReadWriteLock并将fair标识设置为true即公平锁:

//设置为false之后 非公平 等待队列前是读锁 就可以让读锁插队
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);

    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
1
2
3
4
5

同时我们也给出读写锁的实用代码,逻辑比较简单,上锁后休眠1000毫秒,同时在尝试上锁、得到锁、释放锁附近打印日志:

private static void read() {
        log.info("尝试获取读锁");
        readLock.lock();
        try {
            log.info("获取读锁成功,执行业务逻辑......");
            ThreadUtil.sleep(1000);
        } catch (Exception e) {
            //......
        } finally {
            readLock.unlock();
            log.info("释放读锁");
        }

    }


    private static void write() {
        log.info("尝试获取写锁");
        writeLock.lock();
        try {
            log.info("获取写锁成功,执行业务逻辑......");
            Thread.sleep(1000);
        } catch (Exception e) {

        } finally {
            writeLock.unlock();
            log.info("释放写锁");
        }

    }
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

对应的我们也给出使用的代码示例,感兴趣的读者可以基于标识自行调试一下:

public static void main(String[] args) {

        new Thread(() -> write(), "t0").start();
        new Thread(() -> read(), "t1").start();
        new Thread(() -> read(), "t2").start();
        new Thread(() -> write(), "t3").start();
        new Thread(() -> read(), "t4").start();

        ThreadUtil.sleep(1, TimeUnit.DAYS);
    }
1
2
3
4
5
6
7
8
9
10

# 源码解析非公平锁插队原理

我们可以看到一个tryAcquireShared方法,因为我们设置的是非公平锁,所以代码最后只能会走到NonfairSync 的tryAcquireShared。

 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }
1
2
3
4
5
6
7
8
9
10
11

可以看到逻辑也很简单,一旦队首节点释放锁之后,就会通知其他节点进行争抢,而其他节点都会走到这段逻辑,只要判断到没有人持有锁,就直接进行CAS争抢。这就应证了我们上述的观点,等待队列首节点是写锁占有锁的情况下,一旦写锁释放之后,后续的线程可以任意插队抢占并上读锁或者写锁,这也就是为什么我们上文的线程3先于线程2上了读锁的原因。

final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                //如果小于0则说明没有人持有可以直接通过CAS进行争抢
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
1
2
3
4
5
6
7
8
9
10

# 自旋锁和非自旋锁

我们都知道Java阻塞或者唤醒一个线程都需要切换CPU状态的,这样的操作非常耗费时间,而很多线程切换后执行的逻辑仅仅是一小段代码,为了这一小段代码而耗费这么长的时间确实是一件得不偿失的事情。对此java设计者就设计了一种让线程不阻塞,原地"稍等"即自旋一下的操作。

# 自旋锁代码示例

如下代码所示,我们通过AtomicReference原子类实现了一个简单的自旋锁,通过compareAndSet尝试让当前线程持有资源,如果成功则执行业务逻辑,反之循环等待。

public class MySpinLock {
    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void lock() {
        Thread curThread = Thread.currentThread();
        //使用原子类自旋设置原子类线程,若线程设置为当前线程则说明当前线程上锁成功
        while (!sign.compareAndSet(null, curThread)) {
            System.out.println(curThread.getName() + "未得到锁,自旋中");
        }
    }

    public void unLock() {
        Thread curThread = Thread.currentThread();
        sign.compareAndSet(curThread, null);
        System.out.println(curThread.getName() + "释放锁");

    }

    public static void main(String[] args) {
        MySpinLock mySpinLock = new MySpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "尝试获取自旋锁");
                mySpinLock.lock();
                System.out.println(Thread.currentThread().getName() + "得到了自旋锁");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    mySpinLock.unLock();
                    System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
                }

            }
        };

        Thread t1=new Thread(runnable,"t1");
        Thread t2=new Thread(runnable,"t2");
        t1.start();
        t2.start();
    }
}

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

输出结果

t1尝试获取自旋锁
t2尝试获取自旋锁
t1得到了自旋锁
t2未得到锁,自旋中
t2未得到锁,自旋中
t2未得到锁,自旋中
t2未得到锁,自旋中
t2未得到锁,自旋中
t2未得到锁,自旋中
t1释放锁
t2得到了自旋锁
t1释放了自旋锁
t2释放锁
t2释放了自旋锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 可中断锁和非可中断锁

可中断锁上文lockInterruptibly上文已经演示过了,这里就不多做赘述了。


public class LockInterruptiblyDemo implements Runnable {


//设置为static,所有对象共享
    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 尝试取锁");
        try {
        //设置锁可以被打断
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + " 取锁成功");
                Thread.sleep(5000);

            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 执行业务逻辑时被中断");

            } finally {
                lock.unlock();
            }

        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "尝试取锁时被中断");
        }

    }
}

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) throws InterruptedException {
        LockInterruptiblyDemo lockInterruptiblyDemo = new LockInterruptiblyDemo();
        //线程1启动
        Thread thread0 = new Thread(lockInterruptiblyDemo);
        thread0.start();
        //线程2启动
        Thread thread1 = new Thread(lockInterruptiblyDemo);
        thread1.start();
        //主线程休眠,让上述代码执行,然后执行打断线程1逻辑 thread0.interrupt();
        Thread.sleep(2000);
        thread0.interrupt();
    }
1
2
3
4
5
6
7
8
9
10
11
12

这里补充一下可中断锁的原理,可中断锁实现的可中断的方法很简单,通过acquireInterruptibly建立一个可中断的取锁逻辑。

public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
1
2
3

我们不如源码可以看到,对于没有获得锁的线程,判断走到interrupted看看当前线程是否被打断,如果打断了则直接抛出中断异常。

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
            //当线程被打断时,直接抛出中断异常
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
1
2
3
4
5
6
7
8

# 使用锁的注意事项

# 缩小同步代码块,尽量不要锁住方法,减少锁的粒度

假如代码块里有耗时的操作,锁的粒度过大会导致大量线程处于等待状态,影响系统执行效率。

# 锁中尽量不要包含锁

在锁住包含锁,假设线程A的业务先拿锁1再拿锁2,线程2先拿锁2再拿锁1,极有可能出现线程死锁问题,所以我们使用时尽可能不要在锁中包含锁。

# 选择合适的锁类型

结合实际场景选用合适的锁,例如对于竞争不是很激烈的业务选用自旋锁即可,尽可能去避免线程上下文切换的开销。对于竞争激烈的我们建议使用synchronized关键字。

# 参考文献

Lock锁------lockInterruptibly()方法:https://blog.csdn.net/qq_43323776/article/details/82939344 (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
详解JUC包下的锁
详解并发编程中的CAS原子类

← 详解JUC包下的锁 详解并发编程中的CAS原子类→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×