Java并发synchronized线程安全详解
深入理解Java并发编程中的synchronized关键字,是构建线程安全应用的关键。本文聚焦synchronized在方法和代码块中的应用,剖析wait()和notify()系列方法的使用规范,强调其必须在同步块内调用的原因。通过循环缓冲区的并发实现案例,揭示了分离锁导致的线程安全问题,强调统一锁机制的重要性。同时,阐述了并发编程中,wait()条件判断使用while循环而非if的必要性,旨在帮助开发者避免并发陷阱,编写健壮的并发程序。掌握synchronized的正确用法,是Java开发者进阶并发编程的必经之路。

本文深入探讨Java中synchronized关键字在方法和代码块层面的应用,重点解析wait()和notify()系列方法的使用规范及其必须在同步块内调用的原因。通过分析循环缓冲区的并发实现案例,文章揭示了分离锁可能导致的严重线程安全问题,强调了统一锁机制的重要性,并阐述了在并发编程中,wait()条件判断使用while循环而非if的必要性,旨在指导读者构建健壮的并发程序。
在Java并发编程中,synchronized关键字是实现线程同步和互斥的核心机制之一。它不仅能够保证代码块或方法的原子性,还能确保内存可见性,即一个线程对共享变量的修改对其他线程是可见的。本文将通过一个经典的生产者-消费者模型(循环缓冲区)的并发实现案例,详细剖析synchronized方法与synchronized代码块的区别、wait()/notify()机制的使用细节以及并发编程中常见的陷阱与最佳实践。
synchronized方法与synchronized代码块:基础与选择
synchronized关键字可以用于修饰方法或代码块,以实现对共享资源的互斥访问。
synchronized方法: 当synchronized修饰一个实例方法时,锁对象是当前实例对象(this);当修饰一个静态方法时,锁对象是当前类的Class对象。这种方式简单直观,但锁的粒度较大,可能会限制并发性。
class ExampleBuffer { // ... 共享资源 synchronized void addElement(byte b) { // 访问和修改共享资源 } synchronized byte removeElement() { // 访问和修改共享资源 return 0; // 示例返回值 } }synchronized代码块: synchronized代码块允许我们指定任意对象作为锁。这提供了更细粒度的控制,我们可以根据需要选择不同的锁对象,或者只同步代码的关键部分,从而在某些场景下提高并发度。
class ExampleBuffer { private final Object lock = new Object(); // 显式声明锁对象 // ... 共享资源 void addElement(byte b) { synchronized (lock) { // 访问和修改共享资源 } } void removeElement() { synchronized (lock) { // 访问和修改共享资源 } } }
在实际开发中,应根据共享资源的访问模式和对并发性能的要求,合理选择synchronized方法或synchronized代码块,并确定合适的锁对象。
wait()、notify()和notifyAll()的同步要求
Object类提供的wait()、notify()和notifyAll()方法是Java中实现线程间协作(如生产者-消费者模式)的关键。然而,使用这些方法有一个严格的规定:任何调用wait()、notify()或notifyAll()的方法,都必须在持有该对象监视器(即该对象的锁)的synchronized代码块或方法内部。 否则,会抛出IllegalMonitorStateException运行时异常。
考虑案例中的第二个实现片段:
// 在add方法中
synchronized (removeLock){ // 为什么这里又加了一个同步块?
removeLock.notifyAll();
}
// 在remove方法中
synchronized (addLock){ // 为什么这里又加了一个同步块?
addLock.notifyAll();
}这里添加第二个synchronized代码块的原因,正是为了满足notifyAll()方法的调用要求。当需要调用removeLock.notifyAll()时,线程必须先获得removeLock对象的锁;同理,调用addLock.notifyAll()时,线程必须获得addLock对象的锁。synchronized关键字不仅提供了互斥访问,还保证了内存同步,使得wait()和notify()能够正确地协调线程状态,确保线程在等待或被唤醒时,其状态转换是原子且可见的。
并发编程中的陷阱:分离锁与共享资源
尽管第二个实现为了满足notifyAll()的调用要求而添加了额外的同步块,但它引入了一个更严重的并发问题:对同一个共享资源使用了不同的锁。
在第二个实现中,add方法在addLock上同步以修改buffer和availableObjects,而remove方法在removeLock上同步以读取buffer和修改availableObjects。
// add方法片段
void add (byte b) throws InterruptedException{
synchronized (addLock){ // 保护对buffer的写入
// ... 修改 buffer ...
// ... 修改 availableObjects ...
}
// ...
}
// remove方法片段
byte remove () throws InterruptedException{
byte element;
synchronized (removeLock){ // 保护对buffer的读取
// ... 读取 buffer ...
// ... 修改 availableObjects ...
}
// ...
return element;
}这种分离锁的策略是极其危险的。当一个线程在addLock的保护下修改buffer时,另一个线程可能在removeLock的保护下读取buffer。由于它们使用的是不同的锁,synchronized机制无法保证对buffer的互斥访问和内存可见性。这会导致:
- 数据不一致: 读取线程可能读取到写入线程修改前的旧数据。
- 内存可见性问题: 写入线程对buffer的修改可能对读取线程不可见,即使写入操作已经完成。
- 读取到null或其他错误值: 在极端情况下,读取线程可能读取到部分写入的数据,导致错误。
核心原则:所有访问(读或写)共享可变状态的操作,都必须通过同一个锁来保护。
对于循环缓冲区这样的共享数据结构,无论是添加元素(生产者)还是移除元素(消费者),它们都在操作同一个buffer数组、head、tail指针以及availableObjects计数器。因此,正确的做法是使用一个统一的锁对象来保护所有对这些共享状态的访问。
修正方案示例:
以下是一个使用统一锁对象实现线程安全循环缓冲区的示例:
import java.util.concurrent.atomic.AtomicInteger; // 在这里可以简化为普通int
class ThreadSafeCircularBuffer {
private final byte[] buffer;
private int head;
private int tail;
private int availableObjects; // 在统一锁保护下,可以是普通int
private final int size;
private final Object bufferLock = new Object(); // 统一的锁对象
public ThreadSafeCircularBuffer(int size) {
if (size <= 0) {
throw new IllegalArgumentException("Buffer size must be positive.");
}
this.size = size;
this.buffer = new byte[size];
this.head = 0;
this.tail = 0;
this.availableObjects = 0;
}
public void add(byte b) throws InterruptedException {
synchronized (bufferLock) { // 使用统一的锁
while (availableObjects == size) { // 使用while循环判断条件
bufferLock.wait(); // 缓冲区满,等待消费者消费
}
buffer[tail] = b;
tail = (tail + 1) % size;
availableObjects++;
bufferLock.notifyAll(); // 唤醒等待的消费者(或生产者,如果他们有其他条件)
}
}
public byte remove() throws InterruptedException {
byte element;
synchronized (bufferLock) { // 使用统一的锁
while (availableObjects == 0) { // 使用while循环判断条件
bufferLock.wait(); // 缓冲区空,等待生产者生产
}
element = buffer[head];
head = (head + 1) % size;
availableObjects--;
bufferLock.notifyAll(); // 唤醒等待的生产者(或消费者,如果他们有其他条件)
}
return element;
}
}在这个修正后的实现中,add和remove方法都通过bufferLock来同步,确保了对buffer、head、tail和availableObjects的访问是线程安全的,并且内存可见性得到了保证。
wait()条件判断:if vs. while
原始的第一个实现中使用了if(condition) wait();,而第二个实现则改为了while(condition) wait();。这是一个非常重要的改进,也是并发编程中的一个常见陷阱。
为什么必须使用while循环来判断wait()的条件?
- 虚假唤醒(Spurious Wakeups): JVM规范允许线程在没有收到notify()或notifyAll()调用时被“虚假唤醒”。如果使用if,被虚假唤醒的线程会直接执行后续代码,而此时它等待的条件可能并未满足,导致逻辑错误。
- 多个生产者/消费者: 在多生产者-多消费者场景中,一个notifyAll()可能会唤醒所有等待的线程。当这些线程逐一获得锁并检查条件时,可能发现条件已不满足。例如,多个消费者被唤醒,第一个消费者获取到元素后,第二个消费者再检查时可能发现缓冲区又空了。
- 条件变更: 即使没有虚假唤醒,一个线程被唤醒后,它所等待的条件可能在它获得锁并重新检查之前,被另一个(更快的)线程再次改变。
今天关于《Java并发synchronized线程安全详解》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!
微信如何查看共同好友?详细步骤解析
- 上一篇
- 微信如何查看共同好友?详细步骤解析
- 下一篇
- Java避免类重复的实用技巧
-
- 文章 · java教程 | 1天前 | Java · ServiceLoader module-info.java Java模块
- Java 模块服务加载的可选实现与回退路径
- 363浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java VarHandle 内存序选择与可见性验证
- 226浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java JFR 事件流定位短时延迟尖峰
- 213浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java Record 序列化兼容字段演进方案
- 483浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java ServiceLoader 隔离模块化插件实现
- 414浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java Pattern Matching switch 处理密封层级
- 189浏览 收藏
-
- 文章 · java教程 | 1天前 | Stream · Java教程 · java Stream Collector Collectors.teeing 统计汇总
- Java Collectors.teeing 汇总两条统计管道
- 385浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java Foreign Function Memory API 管理本地内存生命周期
- 385浏览 收藏
-
- 文章 · java教程 | 2天前 |
- Java StampedLock 乐观读校验失败的回退方式
- 165浏览 收藏
-
- 文章 · java教程 | 2天前 | 异常处理 · 并发编程 · Java教程 · StructuredTaskScope Java Structured Concurrency Joiner allUntil 子任务异常 聚合异常
- Java Structured Concurrency 汇总子任务异常
- 260浏览 收藏
-
- 文章 · java教程 | 2天前 |
- Java ForkJoinPool asyncMode 调整任务队列顺序
- 121浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 435次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 508次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 524次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 466次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 296次使用
-
- Java try-with-resources 多个资源关闭顺序是什么
- 2026-09-10 501浏览
-
- 矩阵主副对角线快速定位技巧
- 2026-05-31 501浏览
-
- Java多态优化流程代码与行为分发改进
- 2026-05-26 501浏览
-
- JVM 类元数据双亲委派链表深度解析
- 2026-05-21 501浏览
-
- 反射异常处理:InvocationTargetException解析与应用
- 2026-05-16 501浏览

