Java 多线程并发编程:从线程安全到锁与线程协作

发布时间:2026/10/25 21:09:39

Java 多线程并发编程:从线程安全到锁与线程协作 一、线程的状态Java 给线程引入了六种状态NEW创建了 Thread 对象但是还没调用start()。TERMINATED操作系统内部的线程已经销毁了但 Thread 对象还在线程的入口方法执行完毕。RUNNABLE可工作的又可以分成正在工作中和即将开始工作。接下来的三个状态表示排队等着其他事情WAITING进入阻塞状态然后死等。TIMED_WAITING带有超时时间的阻塞等待。BLOCKED特指由于锁引起的阻塞。二、线程安全问题先来看一个线程不安全的案例运行之后两个线程分别对count变量都进行 50000 次的count预计结果应该是 100000 才对可是结果却是 71783而且再多运行几次会发现这个数值是不确定的、随机性的由此便引入线程安全问题。1. 线程安全概念给出一个线程安全的确切定义是比较复杂的但我们可以这样认为某个逻辑单个线程执行没有问题但是多个线程执行出现问题这便是线程安全问题。上述的逻辑如果单线程执行那毫无疑问最终的结果一定是 100000。2. 线程安全问题的原因根本原因操作系统对于线程的调度是随机的。两个线程针对同一个变量进行修改操作。修改操作不是原子性的。内存可见性。指令重排序。分析上述代码count这行代码对应了三个 CPU 的指令load把内存中的数据加载到寄存器中。add把寄存器中的数据 1。save把寄存器中的数据写回到内存中。但是由于调度顺序是不确定的因此会有无数种情况这里随机列举四种情况3. 如何解决线程安全问题我们主要从上述提到的“修改操作不是原子的”这个原因下手。没有保证原子性会给多线程带来什么问题如果一个线程正在对一个变量操作中途其他线程插入进来了若这个操作被打断了那么结果就可能是错误的。由此便引入了“锁”从而把修改操作变成原子性的。4. synchronized 关键字Java 中引入synchronized关键字其语法格式为synchronized(object) { // 一系列代码; }synchronized会起到互斥两件事情不可能同时发生效果若某个线程执行到某个对象的synchronized中时其他线程如果也执行到同一个对象synchronized就会阻塞等待。进入synchronized修饰的代码块相当于“加锁”。退出synchronized修饰的代码块相当于“解锁”。注意此处“加锁”不是禁止线程调度而是防止其他线程插队。synchronized()括号里的对象从语法角度上来讲可以是任意的对象只要是一个 Object 子类的实例就行但是从语义角度上来讲两个线程要填写相同的对象才会产生锁竞争才会有阻塞/防止插队这样的效果。下面来看这个示例流程lock 表示“加锁”unlock 表示“解锁”其实可以用生活中的上厕所来粗略理解假设一个人进了厕所锁上门一个线程进行“加锁”其他人就要在外面等待其他线程进入阻塞等待状态至少得等厕所里的人出来后“解锁”才能允许第二个人进去第二个线程加“锁”成功可以解除阻塞状态往下执行逻辑了。下面来看加“锁”后的代码运行后这样就达到我们预期结果了。如果把整个 for 循环都放到 synchronized 关键字里是否会有问题呢运行后依旧没问题其实把整个 for 循环放到 synchronized 里面相当于就只加了一次“锁”而只把count放到 synchronized 里面就相当于循环的加了 50000 次“锁”。我们说“锁”内部的逻辑少只是一次count那么“锁”的粒度就小。“锁”内部的逻辑多50000 次的 for 循环那么“锁”的粒度就大注意这并不是代码行数的问题而是与逻辑相关。5. synchronized 其他使用示例5.1. 修饰普通方法5.2. 修饰静态方法无论哪种写法synchronized()针对啥对象“加锁”不重要重要的是两个线程是否针对同一个对象“加锁”。6. 可重入锁先来看一下下面的代码案例thread1 线程针对 count1 进行第一次“加锁”调用 add() 方法时又针对 count1 进行第二次“加锁”按照之前的逻辑这次应该会阻塞此时就无法继续执行完 add() 方法也就无法执行到第一次“加锁”的“}”从而“解锁”了那逻辑就卡住了不就成“死锁”了吗实际代码一执行是可以正常运行的其实“死锁”的逻辑是存在的但是 synchronized 针对这种情况做了特殊处理这就是锁的“可重入机制”。JVM 会记录当前持有这把锁的线程同时维护一个锁计数器同一个线程再次获取已经持有的锁时不会阻塞等待仅仅将计数器加一每退出一层 synchronized 同步块计数器就减一只有计数器归零的时候锁才真正释放其他线程才可以获取。正是这套机制避免了同一个线程自己阻塞自己的问题。分析一下上述代码外层synchronized(count1)拿到锁 → 计数器 1进入 addsynchronized(this)重入 → 计数器 2退出 add 内同步块 → 计数器 1退出外层同步块 → 计数器 0锁真正释放7. 死锁7.1 概念锁的可重入只能避免同一个线程重复获取同一把锁造成的自我阻塞并不能消除死锁。当多个线程各自占有一把锁又同时去请求对方手里的锁时就会发生真正的死锁。那什么是死锁呢多个线程互相持有对方所需要的锁彼此无限等待对方释放锁所有线程都被卡住程序无法继续执行。7.2 死锁的四个必要条件必须同时满足互斥条件锁资源同一时刻只能被一个线程占用。不可剥夺条件锁只能由持有线程主动释放其他线程不能强行抢占。请求并保持线程持有一把锁在不释放已有锁的情况下再去申请其他锁。循环等待条件线程之间形成环状等待关系A 等 B、B 等 A。只要破坏其中任意一条必要条件就可以规避死锁。7.3 死锁的代码案例运行后程序不会终止后续打印语句永远不会执行程序陷入停滞发生死锁。案例流程分析thread1 获取 locker1休眠 1 秒不释放 locker1接着申请 locker2。thread2 获取 locker2休眠 1 秒不释放 locker2接着申请 locker1。thread1 在等待 thread2 释放 locker2thread2 在等待 thread1 释放 locker1。双方都不释放已占有的锁互相无限等待程序卡死产生死锁。这个案例同时满足死锁的四个必要条件互斥条件、不可剥夺条件、请求并保持、循环等待。注意区分“可重入锁”解决的是同一个线程重复申请同一把锁无法避免这种多线程之间的死锁。补充小提示代码里 sleep 不会释放锁sleep 只是让线程休眠锁依旧握在线程手上。7.4 如何规避死锁死锁的发生需要四个必要条件同时满足只要破坏其中任意一个条件就可以避免死锁。实际开发中重点关注打破“请求并保持”与打破“循环等待”。1. 打破“请求并保持”产生问题线程已经持有一部分锁不进行释放又去申请其他新的锁。解决办法一次性申请所有需要的锁全部锁获取成功之后再执行业务逻辑不要拿到一把锁之后中途再去申请另一把锁。如果申请不到新锁就主动释放当前已经持有的锁之后再重新尝试获取锁。2. 打破“循环等待”产生问题多个线程之间形成环形等待关系互相等待对方手中的锁。解决办法给所有锁规定统一的获取顺序所有线程严格按照相同的先后顺序申请锁切断循环等待环路。举例规定永远先获取 locker1再获取 locker2。thread1、thread2 都遵守该顺序就不会出现一个持有 locker1 等 locker2另一个持有 locker2 等 locker1 的情况。8. volatile 关键字8.1 内存可见性synchronized 关键字是解决线程安全问题的一种方案但是还有一种场景需要通过 volatile 来解决。这就要谈到内存可见性了。Java 线程工作时会把变量从主内存拷贝到自己的工作内存中进行操作。修改之后不会立刻刷新回主内存。当一个线程修改了共享变量的值另一个线程看不到最新修改后的值读到的还是旧数据这就是内存可见性问题。现象举例一个线程修改标记变量另一个线程一直在循环读取该变量永远感知不到变量已经被修改循环不会停止。8.2 volatile 作用1. 保证内存可见性被 volatile 修饰的共享变量一旦某个线程对它完成修改会立刻刷新到主内存其他线程读取时直接从主内存读取最新值不再使用自己缓存里的旧副本。2. 禁止指令重排序JVM、CPU 会对指令做优化重排序volatile 可以阻止部分不合理的指令重排保证代码执行顺序符合我们编写的逻辑。重要volatile 不能保证原子性volatile 只解决“可见性、禁止重排序”不能保证操作是原子的。例如 count读取-修改-写入多步操作即便加了 volatile多线程下依旧会出现线程安全问题这种场景仍然需要 synchronized。8.3 volatile 和 synchronized 的简单对比volatile轻量级只保证可见性、禁止指令重排序不保证原子性适合标记位、状态开关这类场景。synchronized重量级锁同时保证可见性、原子性、禁止指令重排序会阻塞线程。8.4 代码案例来看下面这个代码案例运行之后现象thread1 一直在执行while(flag 0)循环thread2 通过键盘输入把 flag 修改为非 0 值thread2 打印结束。但是 thread1 不会退出循环不会打印“thread1 结束”程序卡死。原因内存可见性问题flag 是共享变量。thread1 把 flag 拷贝到自己的工作内存一直读取缓存里旧值 0。thread2 修改 flag修改写回主内存。但是 thread1 不知道主内存的 flag 已经变化不去刷新自己的工作内存一直读到旧数据 0循环永远跑下去。解决办法给共享变量 flag 加上 volatile 修饰加上 volatile 之后thread2 修改 flag会立刻刷新到主内存thread1 不会再使用本地缓存副本每次都去主内存读取最新的 flag输入非 0 后while 条件不成立thread1 正常退出循环打印结束。9. wait() 和 notify()由于线程是抢占式执行线程之间执行的先后顺序是难以预知的。但是在实际开发中有时我们希望可以人为协调多个线程的执行先后顺序。完成线程之间的协调工作主要依靠三个方法wait()、notify()、notifyAll()。注意这三个方法是属于 Object 对象的方法不是 Thread 的方法并且必须写在 synchronized 同步代码块内部调用。9.1 wait() 方法调用 wait() 会做三件事让当前线程进入等待状态暂时停止往下执行释放当前持有的锁非常关键别的线程才可以拿到这把锁执行代码当线程被唤醒之后不会立刻继续执行而是要重新去竞争获取锁拿到锁之后才会从 wait 处继续往下运行。结束等待被唤醒的几种条件其他线程调用该对象的 notify() 方法唤醒等待在这个对象上的线程如果调用带超时时间的wait(long timeout)等待时间超时会自动唤醒线程被 interrupt() 中断会抛出 InterruptedException 异常结束等待。简单说明注意wait() 一定要在 synchronized 保护的代码块中调用在锁对象上调用 wait()。如果没有持有锁就直接调用 wait程序会直接抛出 IllegalMonitorStateException 异常。9.2 notify() 方法notify() 用来唤醒调用 wait() 进入等待的线程。作用唤醒一个正在该对象上等待的线程。注意如果有多个线程在该对象上等待会随机唤醒其中某一个线程。调用 notify() 之后不会立刻释放锁要等到当前 synchronized 代码块执行完毕退出同步块之后锁才会释放被唤醒的线程才可以去竞争获取锁继续执行。如果当前没有任何线程在等待调用 notify() 不会报错只是什么事情都不做。9.3 notifyAll() 方法作用唤醒所有在该对象上处于等待状态的线程。唤醒之后所有被唤醒的线程会进入锁竞争大家互相争抢这一把锁同一时刻也只能有一个线程拿到锁执行其余没抢到的继续阻塞。注意notify() vs notifyAll() 小结notify()随机唤醒一个等待线程。notifyAll()唤醒全部等待线程。9.4 代码案例运行后9.5 wait() 与 sleep() 的区别经典面试题1. 锁处理不同wait()在等待的时候会释放持有的锁其他线程可以获取锁执行sleep()休眠的时候不会释放锁如果已经拿到锁休眠期间锁仍然被自己占有别的线程拿不到锁。2. 所属类型不同wait()Object 的成员方法sleep()Thread 类的静态方法。3. 使用位置要求wait()必须放在 synchronized 同步代码块/同步方法中调用sleep()可以在任意地方使用不需要加锁。4. 唤醒方式wait()可以被 notify() / notifyAll() 主动唤醒也可以设置超时自动唤醒也可以被 interrupt 中断sleep()时间到自动结束休眠也可以被 interrupt() 中断抛出异常不能被 notify 唤醒。实际上sleep 只是单纯让线程歇一会wait 是用来做线程之间条件协调通信的。
延伸阅读

更多相关文章

2026/10/25 21:56:36

大模型技术评估实战:从传闻到验证,构建系统化测试框架

这类关于大模型版本猜测和对比的话题,最值得先看的不是传言本身,而是它背后反映出的技术动向和实际影响。对于开发者、技术选型者,甚至是普通用户来说,关心的核心问题其实很直接:如果真有新的模型版本出现,…

2026/10/26 19:19:46

单片机毕业设计-基于 STM32 与 WiFi 的坐姿健康监测智能座椅控制系统设计 基于 STM32 的环境光敏采集坐姿提醒智能设备开发(018404)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/25 22:23:27

微信小程序 + .NET 后端开发调试完整解决方案

🚀 微信小程序 + .NET 后端开发调试完整解决方案 从零搭建到真机调试,涵盖内网穿透、图片加载、HTTPS 等所有坑点 📖 写在前面 最近在开发一个小程序项目时,后端使用 ASP.NET Core 8.0,前端是微信小程序。从开发环境搭建到真机调试,遇到了各种让人抓狂的问题:电脑上一…

2026/10/26 19:20:37

Element UI表格合计行事件绑定实战与优化方案

1. 项目概述:el-table合计行事件绑定实战在Element UI的表格组件开发中,合计行(summary row)是一个高频使用的功能特性,但官方文档对合计行的事件处理并未详细说明。最近在电商后台订单统计模块开发时,我需…

2026/10/26 19:20:37

PLC工程师学AI:工业时序数据驱动的智能控制实战

1. 这不是未来,是正在发生的现场作业变革“懂 AI 的 PLC 工程师,正在拉开行业差距”——这句话最近在自动化圈子里传得特别快,不是因为某个厂商的广告词,而是我上个月在苏州一家汽车零部件厂调试产线时,亲眼看见的现实…

2026/10/26 19:20:37

中国气候区划工程落地:从静态地图到动态知识图谱

简介:本资源是依据国家标准GB/T 17297-1998数字化构建的中国气候区划GIS数据集,面向地理信息、气象、生态、农业及城市规划等领域的科研人员、高校师生与政策研究者,用于支撑气候空间分析、区域差异评估与可持续发展决策。压缩包共16个文件&a…

2026/10/26 19:20:37

2026年学术写作AI工具评测与实战指南

1. 2026年学术写作工具全景扫描三年前我刚开始博士研究时,面对市面上突然涌现的数十款AI写作工具完全无从下手。如今作为用过28款工具的"踩坑专业户",我想分享这份结合300小时实测的深度评测。不同于那些拿厂商赞助的软文,这里只有…

2026/10/26 19:20:37

工业设备智能运维:基于ML.NET的预测性维护实战

1. 项目概述:工业设备智能运维的AI解决方案这个项目本质上是在工业自动化领域构建了一套端到端的预测性维护系统。作为在工业上位机开发领域摸爬滚打多年的老手,我见过太多工厂因为设备突发故障导致停产的情况。传统的人工巡检和定期维护就像"盲人摸…

2026/10/26 19:15:34

YOLOv8检测框绘制:PIL/numpy输入与RGB/BGR坐标还原

做视觉项目时,最容易被低估的往往不是模型推理,而是“把结果画回去”这一步。标题里这个需求很具体:YOLOv8 接受一个 numpy 数组(cv2 风格)或 PIL 图像,在输入的 RGB 图像上绘制检测结果。听起来只是调个画…

2026/10/26 0:20:20

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/26 1:53:47

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/26 0:20:17

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/26 0:00:48

Nested Learning:解决AI灾难性遗忘的分层记忆架构

1. 为什么AI需要"记忆"能力?在传统深度学习模型中,我们常遇到一个根本性问题:模型在处理新任务时,往往会完全遗忘之前学到的知识。这种现象被业界称为"灾难性遗忘"(Catastrophic Forgetting)。想象一下&#…

2026/10/26 0:00:48

企业级大模型应用:OCR预处理、Dify编排与DeepSeek落地实战

1. 这不是“用大模型”,而是重构工作流的起点“大模型的应用和工具”——这八个字现在被贴在无数技术分享、招聘JD、内部立项书的标题栏里,但绝大多数人点进去后看到的,是零散的API调用示例、几个开源UI截图、或者一句“接入LLM就能降本增效”…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑