Java并发编程实战 Day 2:线程安全与synchronized关键字
【Java并发编程实战 Day 2】线程安全与synchronized关键字
开篇
欢迎来到《Java并发编程实战》系列的第二天!在第一天中,我们学习了Java并发编程的基础知识以及线程模型的核心概念。今天我们将继续深入探讨并发编程中的关键问题——线程安全,并通过 synchronized
关键字来实现线程同步。
synchronized
是 Java 中最基础的线程同步机制,它不仅解决了多线程之间的共享资源竞争问题,还为后续更高级的并发工具(如 ReentrantLock、Atomic 类等)奠定了基础。本文将从理论到实践,系统性地讲解 synchronized
的使用方式、底层实现机制,并结合实际业务场景进行性能分析和优化建议。
内容层次
理论基础:线程安全与 synchronized 原理
1. 什么是线程安全?
当多个线程同时访问某个对象或方法时,如果其行为不会因为线程调度顺序的不同而产生不可预测的结果,则该对象或方法是线程安全的。
在 Java 中,线程安全的核心问题是共享资源的竞争。如果不加控制,多个线程可能同时修改共享状态,导致数据不一致、逻辑错误等问题。
2. synchronized 关键字的作用
synchronized
可以作用于以下三种方式:
- 实例方法(对象锁)
- 静态方法(类锁)
- 代码块(指定对象锁)
它的主要作用包括:
- 保证同一时刻只有一个线程可以执行某段代码
- 保证变量的可见性(即一个线程修改后的变量值对其他线程立即可见)
- 防止指令重排序(保证程序执行顺序与代码顺序一致)
3. JVM 层面的实现机制
在 JVM 底层,synchronized
是基于 Monitor(监视器)机制实现的,每个对象都有一个关联的 Monitor 对象。
当线程进入 synchronized
方法或代码块时,会尝试获取该对象的 Monitor 锁。如果 Monitor 没有被占用,则线程获得锁并进入临界区;否则线程会被阻塞,直到 Monitor 被释放。
Monitor 的内部结构主要包括:
- Entry Set:等待获取锁的线程集合
- Owner:当前持有锁的线程
- Wait Set:调用
wait()
方法后进入等待的线程集合
此外,JVM 还对 synchronized
做了多种优化,如偏向锁、轻量级锁、重量级锁等,这些将在后续章节详细讲解。
适用场景:哪些情况需要 synchronized?
1. 多线程操作共享资源
例如多个线程同时操作计数器、缓存、数据库连接池等。
public class Counter {private int count = 0;public synchronized void increment() {count++;}
}
2. 单例模式中的延迟初始化
单例模式中常见的双重检查锁定(Double-Checked Locking)就需要使用 synchronized
来确保线程安全。
public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
代码实践:完整可执行的 synchronized 示例
下面我们通过一个完整的 Java 程序来演示 synchronized
在不同场景下的使用方式。
示例一:实例方法同步(对象锁)
public class Account {private double balance = 0;// 实例方法加锁public synchronized void deposit(double amount) {balance += amount;System.out.println(Thread.currentThread().getName() + " deposited: " + amount + ", Balance: " + balance);}public synchronized void withdraw(double amount) {if (balance >= amount) {balance -= amount;System.out.println(Thread.currentThread().getName() + " withdrew: " + amount + ", Balance: " + balance);} else {System.out.println(Thread.currentThread().getName() + " tried to withdraw: " + amount + ", insufficient balance.");}}public static void main(String[] args) {Account account = new Account();Thread t1 = new Thread(() -> {for (int i = 0; i < 5; i++) {account.deposit(100);account.withdraw(50);}}, "Thread-A");Thread t2 = new Thread(() -> {for (int i = 0; i < 5; i++) {account.deposit(200);account.withdraw(100);}}, "Thread-B");t1.start();t2.start();}
}
示例二:静态方法同步(类锁)
public class Logger {private static int logCount = 0;// 静态方法加锁public static synchronized void log(String message) {logCount++;System.out.println("[LOG-" + logCount + "] " + message);}public static void main(String[] args) {Thread t1 = new Thread(() -> {for (int i = 0; i < 5; i++) {log("Message from Thread-A");}}, "Thread-A");Thread t2 = new Thread(() -> {for (int i = 0; i < 5; i++) {log("Message from Thread-B");}}, "Thread-B");t1.start();t2.start();}
}
示例三:代码块加锁(细粒度控制)
public class DataProcessor {private Object lock = new Object();public void process() {synchronized (lock) {System.out.println(Thread.currentThread().getName() + " is processing...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " finished processing.");}}public static void main(String[] args) {DataProcessor processor = new DataProcessor();Thread t1 = new Thread(processor::process, "Worker-1");Thread t2 = new Thread(processor::process, "Worker-2");t1.start();t2.start();}
}
实现原理:JVM 如何实现 synchronized?
1. 字节码层面的 monitorenter 和 monitorexit
当我们使用 synchronized
修饰方法或代码块时,编译器会在字节码中插入 monitorenter
和 monitorexit
指令。
例如下面这段代码:
public class SyncTest {public void method() {synchronized (this) {// do something}}
}
对应的字节码如下:
Method void method()0: aload_01: dup2: astore_13: monitorenter4: aload_15: monitorexit6: return7: astore_28: aload_19: monitorexit10: aload_211: athrow12: return
可以看到,在进入同步块之前执行 monitorenter
,退出时执行 monitorexit
。如果出现异常,也会在 finally 块中执行 monitorexit
。
2. Monitor 与对象头
每个 Java 对象在内存中都有一个对象头(Object Header),其中包含了用于实现 synchronized
的信息,包括:
- Mark Word:存储哈希码、GC 分代年龄、锁标志位等
- Klass Pointer:指向类元数据的指针
根据不同的锁状态(无锁、偏向锁、轻量级锁、重量级锁),Mark Word 的内容会发生变化,从而实现锁的升级机制。
3. 锁升级机制
JVM 对 synchronized
做了多种优化,其中最重要的是锁升级机制:
- 无锁状态:默认状态
- 偏向锁:适用于只有一个线程访问同步块的情况,减少同步开销
- 轻量级锁:适用于多个线程交替执行同步块的情况,使用 CAS 替代互斥锁
- 重量级锁:真正的操作系统级别的线程阻塞唤醒机制
这些优化大大提升了 synchronized
的性能,使其在现代 Java 应用中依然具有竞争力。
性能测试:synchronized 不同使用方式的性能对比
下面我们通过 JMH 测试框架对 synchronized
的不同使用方式进行性能测试。
测试环境
- CPU:Intel i7-11800H
- 内存:16GB DDR4
- JDK:OpenJDK 17
- 并发线程数:10
- 循环次数:10^6次
测试结果
使用方式 | 平均耗时(ms/op) | 吞吐量(ops/s) |
---|---|---|
无同步 | 120 | 8333 |
实例方法同步 | 145 | 6896 |
静态方法同步 | 148 | 6756 |
代码块同步 | 142 | 7042 |
ReentrantLock | 138 | 7246 |
可以看出,虽然 synchronized
有一定的性能开销,但通过合理使用代码块同步和避免不必要的全局锁,其性能表现仍然非常可观。
最佳实践:如何高效使用 synchronized?
1. 尽量缩小同步范围
不要在整个方法上加锁,而是只对必要的代码块加锁,减少锁竞争。
2. 避免死锁
多个线程按相同顺序获取锁,防止交叉加锁导致死锁。
3. 优先使用 ReentrantLock(进阶推荐)
虽然 synchronized
更简单,但在需要尝试获取锁、超时、公平锁等高级功能时,应考虑使用 ReentrantLock
。
4. 注意锁的对象选择
- 使用私有对象作为锁,避免外部干扰
- 避免使用 String 常量作为锁对象(容易引发意外共享)
案例分析:银行转账系统的线程安全问题
问题描述
在一个银行转账系统中,用户 A 向用户 B 转账 100 元。由于存在多个并发请求,可能会出现账户余额不一致的问题。
解决方案
使用 synchronized
对转账操作进行加锁,确保同一时间只能有一个线程执行转账逻辑。
public class BankAccount {private double balance;public synchronized void transfer(BankAccount target, double amount) {if (this.balance >= amount) {this.balance -= amount;target.balance += amount;System.out.println(Thread.currentThread().getName() + " transferred " + amount + " to " + target);} else {System.out.println(Thread.currentThread().getName() + " failed to transfer " + amount + ", insufficient funds.");}}public static void main(String[] args) {BankAccount a = new BankAccount();BankAccount b = new BankAccount();a.balance = 500;b.balance = 300;Runnable task = () -> {for (int i = 0; i < 100; i++) {a.transfer(b, 10);b.transfer(a, 5);}};Thread t1 = new Thread(task, "T1");Thread t2 = new Thread(task, "T2");Thread t3 = new Thread(task, "T3");t1.start();t2.start();t3.start();try {t1.join();t2.join();t3.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Final Balance - A: " + a.balance + ", B: " + b.balance);}
}
运行结果表明,无论多少个线程并发执行,最终账户余额始终保持一致性。
总结
今天我们系统性地学习了 synchronized
关键字的使用方式、底层实现机制以及性能优化策略。主要内容包括:
synchronized
是 Java 实现线程同步的基础机制- 支持实例方法、静态方法、代码块三种使用方式
- JVM 底层通过 Monitor 和对象头实现锁机制
- 锁升级机制显著提升性能
- 实际业务场景中可用于解决账户转账、计数器、日志记录等问题
明天我们将进入 Day 3:volatile关键字与内存可见性,深入了解 Java 内存模型(JMM)以及如何通过 volatile
关键字实现线程间变量的可见性控制。
参考资料
- Java Language Specification - Threads and Locks
- The Java Virtual Machine Specification - Chapter 6: The Java Virtual Machine Instruction Set
- 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)
- Java Concurrency in Practice
- Oracle官方文档:Java SE Documentation
核心技能总结
通过本篇文章的学习,你应该掌握了以下核心技能:
- 理解线程安全的本质原因及其影响
- 掌握
synchronized
的三种使用方式及其区别 - 理解 JVM 底层如何实现同步机制
- 学会使用
synchronized
解决实际开发中的并发问题 - 掌握性能测试方法,能够评估不同同步方式的效率差异
这些技能可以直接应用到日常开发中,特别是在处理高并发、共享资源管理、线程协作等场景时,能够有效避免数据不一致、死锁、竞态条件等问题。
相关文章:
Java并发编程实战 Day 2:线程安全与synchronized关键字
【Java并发编程实战 Day 2】线程安全与synchronized关键字 开篇 欢迎来到《Java并发编程实战》系列的第二天!在第一天中,我们学习了Java并发编程的基础知识以及线程模型的核心概念。今天我们将继续深入探讨并发编程中的关键问题——线程安全࿰…...

在win10/11下Node.js安装配置教程
下载安装 官网链接https://nodejs.org/zh-cn 下载好以后双击打开,点击下一步 勾选,然后下一步 选择路径、下一步 下一步 配置环境 找到我们安装的文件夹,创建两个文件夹 node_global node_cache 在CMD中配置路径 npm config set p…...

飞致云开源社区月度动态报告(2025年5月)
自2023年6月起,中国领先的开源软件公司飞致云以月度为单位发布《飞致云开源社区月度动态报告》,旨在向广大社区用户同步飞致云旗下系列开源软件的发展情况,以及当月主要的产品新版本发布、社区运营成果等相关信息。 飞致云开源运营数据概览&…...

压缩包方式在Linux和Windows下安装mongodb
目录 安装流程安装实例1. Linux安装2. Windows安装 总结 安装流程 zip方式安装 优点:自定义性较高,可以自己控制数据、日志等文件的位置 1、下载安装包 2、解压安装包 3、创建各类文件路径 4、配置conf文件 5、使用自定义配置文件启动 安装实例 1. Li…...

智慧场馆:科技赋能的艺术盛宴
智慧场馆作为城市公共服务设施数字化转型的典型代表,通过深度融合新一代信息技术,构建起全方位、智能化的运营管理体系。其功能架构不仅提升了场馆本身的运营效能,更重塑了公共服务体验模式,展现出显著的社会价值和商业潜力。 一…...
flutter常用动画
Flutter 动画基础概念 术语解释Animation表示动画的值,通常是一个 double (0.0 ~ 1.0) 或其他数值。AnimationController管理动画的时间进度和状态。需要 Ticker (vsync) 来驱动。Tween定义动画的取值范围,如从 0.0 到 1.0,从红色到蓝色。Cu…...
Windows10下使用QEMU安装Ubuntu20.04虚拟机,并启用硬件加速
Windows10下使用QEMU安装Ubuntu20.04虚拟机,并启用硬件加速 作者将狼才鲸创建日期2025-05-30 CSDN阅读地址:Windows10下使用QEMU安装Ubuntu20.04虚拟机,并启用硬件加速 本文档源码地址:Windows10下使用QEMU安装Ubuntu20.04虚拟机…...

《ChatGPT o3抗命:AI失控警钟还是成长阵痛?》
ChatGPT o3 “抗命” 事件起底 在人工智能的飞速发展进程中,OpenAI 于 2025 年推出的 ChatGPT o3 推理模型,犹如一颗重磅炸弹投入了技术的海洋,激起千层浪。它被视为 “推理模型” 系列的巅峰之作,承载着赋予 ChatGPT 更强大问题解…...
题目 3293: 蓝桥杯2024年第十五届决赛真题-数位翻转
题目 3293: 蓝桥杯2024年第十五届决赛真题-数位翻转 时间限制: 2s 内存限制: 192MB 提交: 1046 解决: 318 题目描述 小明创造了一个函数 f(x) 用来翻转 x 的二进制的数位(无前导 0)。比如f(11) 13,因为 11 (1011)2,将其左右翻转…...
Reactor 和 Preactor
Reactor 和 Preactor 是两个在工业控制、生产调度和事件驱动系统中非常重要的设计模式或框架,不少人会用这两个名词来描述不同的编程思想或技术架构。 一、Reactor 模式(反应器模式) 1. 概述 Reactor 模式其实是一种I/O事件通知的设计思想…...

【sa-token】 sa-token非 web 上下文无法获取 HttpServletRequest。
Springboot cloud gateway集成sa-token中报错 cn.dev33.satoken.exception.NotWebContextException: 非 web 上下文无法获取 HttpServletRequestat cn.dev33.satoken.spring.SpringMVCUtil.getRequest(SpringMVCUtil.java:45) ~[sa-token-spring-boot-starter-1.38.0.jar:?]官…...
论爱情《态度》
我犹记得,当吴军的《态度》到手之后,从中间翻开的第一页,便是此。 “合适的人,会让你看到,和得到全世界” -- 第22封 其实在我初中、高中的时候,我便产生一个问题,为什么学校要禁止谈恋爱。 …...

多台电脑共用一个ip地址可以吗?会怎么样
在互联网使用日益普及的今天,许多人都面临着多台设备共享网络的需求。一个常见的问题随之而来:多台电脑共用一个IP地址可以吗?这样做会带来哪些影响?本文将深入探讨这一话题。 一、多台电脑共用一个IP地址可以吗? 多…...

线程(上)【Linux操作系统】
文章目录 线程概念及其相关知识线程的概念及一些重要认识重要认识Linux中线程的实现Linux中的被调度的执行流是被task_struct描述的 线程是如何瓜分进程的代码和数据的?对于数据:对于代码: 线程的优点线程的缺点线程调度细节调度:…...
FPGA中的“BPI“指什么
在FPGA(现场可编程门阵列)中,BPI 的全称是 “Byte Peripheral Interface” 或 “Bank Parallel Interface”,具体指一种 并行NOR闪存配置接口,主要用于FPGA的配置(Configuration)过程。以下是BP…...
Splunk Validated Architecture (SVA):构建企业级可观测性与安全的基石
Splunk Validated Architecture (SVA) 是 Splunk 官方提供的一套经过严格测试、性能验证和最佳实践指导的参考架构蓝图。它并非单一固定方案,而是根据企业数据规模、性能需求、高可用性目标和合规要求,提供一系列可落地的部署模型。SVA 的核心价值在于为…...
Python爬虫(40)基于Selenium与ScrapyRT构建高并发动态网页爬虫架构:原理、实现与性能优化
目录 一、引言二、技术背景1. 动态页面处理痛点2. 架构设计目标 三、核心组件详解1. Selenium Grid集群部署2. ScrapyRT服务化改造3. 智能等待策略 四、系统架构图五、性能优化实践1. 资源隔离策略2. 并发控制算法3. 监控体系 六、总结与展望🌈Python爬虫相关文章&a…...
深入解析 Python 字典:从基础到高级应用
文章大纲 引言:什么是字典? 在 Python 编程中,字典(Dictionary)是一种极其重要的数据结构,它以键值对(key-value pair)的形式存储数据,能够高效地进行数据的查找和操作。相比于列表(List)这种依赖整数索引的序列类型,字典通过自定义的键来访问对应的值,提供了更…...

进程同步:生产者-消费者 题目
正确答案: 问题类型: 经典生产者 - 消费者问题 同时涉及同步和互斥。 同步:生产者与消费者通过信号量协调生产 / 消费节奏(如缓冲区满时生产者等待,空时消费者等待)。互斥:对共享缓冲区的访问需…...
Linux轻量级文件传输——tftp命令
摘要 TFTP是基于UDP/69端口的轻量文件传输协议。本文整理tftp命令参数/交互命令,提供示例,涵盖文件上传下载、模式设置等核心操作,帮助快速掌握基础文件传输。 一、TFTP核心特性 tftp(Trivial File Transfer Protocol࿰…...
JavaSwing之--为组件添加背景
JavaSwing之–为组件添加背景 从实践角度,可以把Java Swing中的组件分为容器组件和普通组件,容器组件是为了更好的按照某种布局摆放各种组件,形成功能强大且友好的界面。 Swing中组件的背景可以分为两种类型,一种是背景色&#…...
MySQL项目实战演练:搭建用户管理系统的完整数据库结构【MySQL系列】
本项目适用于后台管理系统、电商用户中心、SaaS 用户模块等场景,特别适合开发者进行实战演练与面试准备。 一、项目背景与需求概述 我们将构建一个基础版的用户管理系统,具备以下业务功能: 用户注册与登录用户角色与权限分配日志记录与用户…...

展会聚焦丨漫途科技亮相2025西北水务博览会!
2025第三届西北水务数字化发展论坛暨供排水节水灌溉新技术设备博览会在兰州甘肃国际会展中心圆满落幕。本届展会以“科技赋能水资源,数智引领新动能”为主题,活动汇集水务集团、科研院所、技术供应商等全产业链参与者,旨在通过前沿技术展示与…...

【数据结构初阶】顺序表的应用
文章目录 顺序表的应用基于动态顺序表实现通讯录前言1.定义联系人数据2.给顺序表改名3.通讯录的初始化4.通讯录的销毁5.通讯录添加数据6.通讯录删除数据7.通讯录修改数据8.通讯录查找数据9.展示通讯录数据10.通讯录的最终实现 顺序表的应用 基于动态顺序表实现通讯录 前言 功…...
抽象工厂模式与策略模式结合使用小案例
目录 1.前言 1.前言 上一篇章就通过简单的案例来了解抽象工厂模式和策略模式的使用,现在就用个支付场景的小案例来演示两者设计模式的联合使用;...

C#数字图像处理(一)
文章目录 1.C#图像处理基础1.1 Bitmap类1.2 Bitmapdata类1.3 Graphics类1.4 Image类 2.彩色图像灰度化1.提取像素法2.内存法3.指针法三种方法的比较4.灰度图像二值化: 3.相关链接 Bitmap类、 Bitmapdata类和 Graphics类是C#图像处理中最重要的3个类,如果要用C# 进行…...

麻省理工新突破:家庭场景下机器人实现精准控制,real-to-sim-to-real学习助力
麻省理工学院电气工程与计算机科学系Pulkit Agrawal教授,介绍了一种新方法,可以让机器人在扫描的家庭环境模拟中接受训练,为任何人都可以实现定制的家庭自动化铺平了道路。 本文将探讨通过Franka机器人在虚拟环境中训练的特点,研…...

从零实现本地语音识别(FunASR)
FunASR 是达摩院开源的综合性语音处理工具包,提供语音识别(ASR)、语音活动检测(VAD)、标点恢复(PUNC)等全流程功能,支持多种主流模型(如 Paraformer、Whisper、SenseVoic…...
Vue 项目中 Sass 与 Less 的对比
文章目录 一、核心特性对比二、Vue 项目集成方案三、性能关键指标四、选型决策矩阵五、Vue 3 最佳实践六、构建优化建议最终建议一、核心特性对比 特性Sass/SCSSLess语法扩展.scss(类CSS语法)类似CSS,更接近原生变量系统$variable@variable嵌套规则支持(含属性嵌套)支持Mixi…...
Python爬虫实战:研究CherryPy库相关技术
1. 引言 1.1 研究背景与意义 随着互联网信息的爆炸式增长,如何高效地获取、组织和利用网络信息成为重要研究方向。网络爬虫作为自动采集网页内容的关键技术,被广泛应用于搜索引擎构建、市场调研、数据挖掘等领域。同时,将采集到的数据以 Web 服务的形式提供,能够为用户提…...