[java 锁]

news/2025/10/26 13:07:30/文章来源:https://www.cnblogs.com/shiyuzhahan/p/19166731

确实,实际业务中库存通常存在数据库,但多线程操作时仍可能有并发问题(比如数据库事务未控制好导致超卖)。下面结合真实业务场景(含数据库操作),用更贴近实际的代码示例说明 synchronizedvolatile 的用法,同时加入数据库层面的处理逻辑。

一、同步代码块:解决“数据库库存并发扣减”问题(结合数据库锁)

场景:电商秒杀系统的库存扣减

秒杀场景中,1000个用户同时抢购100件商品,库存存在MySQL中(表 product_stock,字段 idstock)。即使数据库有事务,若多个线程同时读取库存、扣减、提交,仍可能出现“超卖”(最终库存为负)。
核心问题:多线程并发读取到相同的库存值,导致扣减后库存为负。

未加同步的问题代码

@Service
public class SeckillService {@Autowiredprivate JdbcTemplate jdbcTemplate;// 秒杀扣减库存(未加同步)public boolean seckill(Long productId) {// 1. 查询当前库存Integer stock = jdbcTemplate.queryForObject("SELECT stock FROM product_stock WHERE id = ?",Integer.class,productId);if (stock <= 0) {return false; // 库存不足}// 2. 扣减库存(stock-1)int rows = jdbcTemplate.update("UPDATE product_stock SET stock = stock - 1 WHERE id = ?",productId);return rows > 0;}
}

问题:1000个线程同时查询到库存=100,然后都执行扣减,最终库存变为 -900(超卖)。

解决方案(同步代码块 + 数据库行锁)

  1. 用同步代码块保证同一商品的扣减逻辑“单线程执行”(避免并发读取相同库存);
  2. 数据库更新时加行锁(FOR UPDATE),确保查询和扣减的原子性。
@Service
public class SeckillService {@Autowiredprivate JdbcTemplate jdbcTemplate;// 用商品ID作为锁的key(避免不同商品抢同一把锁,提高并发)private final ConcurrentHashMap<Long, Object> lockMap = new ConcurrentHashMap<>();public boolean seckill(Long productId) {// 为每个商品创建独立的锁对象(不存在则初始化)Object lock = lockMap.computeIfAbsent(productId, k -> new Object());// 同步代码块:同一商品的扣减逻辑互斥synchronized (lock) {// 1. 查询库存时加行锁(FOR UPDATE),防止其他事务修改Integer stock = jdbcTemplate.queryForObject("SELECT stock FROM product_stock WHERE id = ? FOR UPDATE", // 行锁Integer.class,productId);if (stock <= 0) {return false;}// 2. 扣减库存(此时其他线程被阻塞,不会读到旧库存)int rows = jdbcTemplate.update("UPDATE product_stock SET stock = stock - 1 WHERE id = ?",productId);return rows > 0;}}
}

为什么这样设计

  • ConcurrentHashMap 为每个商品创建独立锁,避免不同商品的秒杀互相阻塞(比如秒杀商品A和B可以同时进行);
  • 同步代码块保证同一商品的“查询库存+扣减”逻辑单线程执行,结合数据库行锁,彻底防止超卖;
  • 比直接用 synchronized 修饰方法更高效(缩小了锁范围)。

二、同步实例方法:解决“单用户并发操作个人账户”问题

场景:用户账户的“余额变动”(如充值、消费同时发生)

用户账户余额存在数据库(表 user_account,字段 user_idbalance)。同一用户可能同时进行充值(如支付宝到账)和消费(如扫码支付),需保证余额计算正确。
核心问题:多线程并发修改同一用户的余额,导致金额错乱(比如充值100和消费50,最终余额可能只加了50)。

未加同步的问题代码

@Service
public class AccountService {@Autowiredprivate JdbcTemplate jdbcTemplate;// 充值(未同步)public void recharge(Long userId, double amount) {// 1. 查询当前余额double balance = jdbcTemplate.queryForObject("SELECT balance FROM user_account WHERE user_id = ?",Double.class,userId);// 2. 计算新余额并更新jdbcTemplate.update("UPDATE user_account SET balance = ? WHERE user_id = ?",balance + amount,userId);}// 消费(未同步)public void consume(Long userId, double amount) {double balance = jdbcTemplate.queryForObject("SELECT balance FROM user_account WHERE user_id = ?",Double.class,userId);if (balance < amount) {throw new RuntimeException("余额不足");}jdbcTemplate.update("UPDATE user_account SET balance = ? WHERE user_id = ?",balance - amount,userId);}
}

问题:用户初始余额100,同时充值100和消费50,可能最终余额=100(充值后未被消费线程读取)。

解决方案(同步实例方法 + 事务)
创建 AccountOperator 实例(每个用户一个),用同步实例方法保证同一用户的所有余额操作互斥,结合事务确保原子性。

// 账户操作类(每个用户一个实例)
public class AccountOperator {private Long userId;private JdbcTemplate jdbcTemplate;public AccountOperator(Long userId, JdbcTemplate jdbcTemplate) {this.userId = userId;this.jdbcTemplate = jdbcTemplate;}// 同步实例方法:锁是当前 AccountOperator 实例(即单个用户)@Transactional // 事务保证查询+更新的原子性public synchronized void recharge(double amount) {double balance = jdbcTemplate.queryForObject("SELECT balance FROM user_account WHERE user_id = ? FOR UPDATE", // 行锁Double.class,userId);jdbcTemplate.update("UPDATE user_account SET balance = ? WHERE user_id = ?",balance + amount,userId);}@Transactionalpublic synchronized void consume(double amount) {double balance = jdbcTemplate.queryForObject("SELECT balance FROM user_account WHERE user_id = ? FOR UPDATE",Double.class,userId);if (balance < amount) {throw new RuntimeException("余额不足");}jdbcTemplate.update("UPDATE user_account SET balance = ? WHERE user_id = ?",balance - amount,userId);}
}// 服务类:管理用户的 AccountOperator 实例
@Service
public class AccountService {@Autowiredprivate JdbcTemplate jdbcTemplate;// 缓存用户的操作实例(每个用户一个)private final ConcurrentHashMap<Long, AccountOperator> operatorMap = new ConcurrentHashMap<>();public void recharge(Long userId, double amount) {// 获取用户对应的操作实例(单例)AccountOperator operator = operatorMap.computeIfAbsent(userId, k -> new AccountOperator(k, jdbcTemplate));operator.recharge(amount);}public void consume(Long userId, double amount) {AccountOperator operator = operatorMap.computeIfAbsent(userId, k -> new AccountOperator(k, jdbcTemplate));operator.consume(amount);}
}

为什么这样设计

  • 每个用户对应一个 AccountOperator 实例,同步实例方法保证同一用户的充值/消费操作互斥(不同用户不影响);
  • 结合 @Transactional 和数据库行锁,确保“查询+更新”的原子性,避免余额错乱;
  • 比直接在 AccountService 加锁更高效(锁范围缩小到单个用户)。

三、同步静态方法:解决“全局计数器并发统计”问题

场景:统计系统“今日订单总数”(跨实例共享)

分布式系统中,多个服务实例同时处理订单,需统计今日订单总数(存Redis或数据库)。多实例并发累加时,需保证计数准确。
核心问题:多个线程(甚至多个服务实例)同时读取旧值、累加、写入,导致计数丢失(比如100个订单只统计到90个)。

未加同步的问题代码

@Service
public class OrderStatisticsService {@Autowiredprivate StringRedisTemplate redisTemplate;private static final String TODAY_ORDER_KEY = "today:order:count";// 统计订单数(未同步)public static void incrementOrderCount() {// 1. 读取当前计数String countStr = redisTemplate.opsForValue().get(TODAY_ORDER_KEY);int count = countStr == null ? 0 : Integer.parseInt(countStr);// 2. 累加后写入redisTemplate.opsForValue().set(TODAY_ORDER_KEY, String.valueOf(count + 1));}
}

问题:多实例同时读取到 count=100,都写入101,导致实际只加了1(正确应为102)。

解决方案(同步静态方法 + Redis原子操作)
用同步静态方法保证单个服务实例内的计数逻辑互斥,同时结合Redis的原子自增命令(跨实例安全)。

@Service
public class OrderStatisticsService {@Autowiredprivate StringRedisTemplate redisTemplate;private static final String TODAY_ORDER_KEY = "today:order:count";// 同步静态方法:锁是 OrderStatisticsService.class(单个服务实例内互斥)public static synchronized void incrementOrderCount() {// 用Redis的原子自增命令(INCR),避免跨实例并发问题redisTemplate.opsForValue().increment(TODAY_ORDER_KEY);}
}

为什么这样设计

  • 同步静态方法保证单个服务实例内的计数操作互斥(避免同一实例内多线程冲突);
  • Redis的 INCR 命令是原子操作,即使多个服务实例并发调用,也能保证计数准确;
  • 静态方法的锁是类对象,确保同一服务实例内所有线程共享这把锁。

一个服务实例就是一个独立的 JVM 进程,进程之间内存隔离,各自的类对象(锁)是完全独立的。
静态方法的锁(类对象)只在当前 JVM 进程内生效,只能控制该进程内的线程排队执行,无法影响其他进程的线程。

四、volatile:解决“分布式任务调度的状态通知”问题

场景:定时任务的“动态启停”

分布式系统中,一个定时任务线程(每10秒执行一次)运行在服务实例中,运维人员可通过接口动态停止该任务。需保证任务线程能立即感知到“停止信号”。
核心问题:任务线程可能缓存“运行状态”变量,导致主线程修改后无法立即生效(任务继续执行)。

未加volatile的问题代码

@Component
public class ScheduledTask {private boolean isRunning = true; // 任务运行状态// 定时任务(每10秒执行)@Scheduled(fixedRate = 10000)public void execute() {if (!isRunning) {return; // 停止执行}System.out.println("执行定时任务...");}// 外部接口:停止任务@PostMapping("/stopTask")public void stopTask() {isRunning = false; // 主线程修改状态}
}

问题isRunning 未加 volatile,任务线程可能一直读取到缓存中的 true,即使 stopTask 被调用,任务仍继续执行。

解决方案(加volatile)
volatile 修饰 isRunning,确保任务线程每次读取都从主内存获取最新值。

@Component
public class ScheduledTask {private volatile boolean isRunning = true; // 加volatile保证可见性@Scheduled(fixedRate = 10000)public void execute() {if (!isRunning) {return;}System.out.println("执行定时任务...");}@PostMapping("/stopTask")public void stopTask() {isRunning = false; // 任务线程能立即感知}
}

为什么这样设计

  • volatile 确保 isRunning 的修改对所有线程可见,避免任务线程“看不到”停止信号;
  • 这里无需 synchronized,因为 isRunning 只有“读”和“写”两种操作,没有复合操作(如 isRunning = !isRunning),volatile 足够保证安全。

总结:贴近真实业务的核心原则

  1. 同步代码块:用于“按资源维度隔离锁”(如按商品ID、用户ID),避免全局锁导致的并发效率低下,常结合数据库行锁或Redis锁;
  2. 同步实例方法:用于“单个对象的多方法互斥”(如同一用户的充值/消费),锁范围是实例级;
  3. 同步静态方法:用于“类级全局资源”(如跨实例的计数器),确保单个服务实例内的操作互斥,结合分布式锁(如Redis)可解决跨实例问题;
  4. volatile:用于“简单的状态标记”(如启停信号),仅保证可见性,不适合复杂的复合操作(如 i++)。

这些场景都结合了数据库/Redis等实际存储,更贴近生产环境的并发问题处理方式。

synchronized 是可重入

你的总结很准!这几种用法没有严格“递进关系”,核心是锁的粒度和作用范围不同(从细到粗:资源维度锁 → 实例锁 → 类锁),下面用统一的“作用范围+核心表述”整理,保持和静态方法一致的易懂风格:

一、四种用法的统一清晰表述(对应你的总结)

1. 同步代码块(资源维度锁)

  • 核心表述:锁是自定义对象(如按商品ID、用户ID创建的独立锁),仅对“同一个JVM进程内,竞争同一把自定义锁”的线程有效。
  • 通俗说:只让操作同一个资源(比如同一商品、同一用户)的线程排队,操作不同资源的线程互不干扰,锁粒度最细、效率最高。

2. 同步实例方法(实例锁)

  • 核心表述:锁是当前对象实例(this),仅对“同一个JVM进程内,调用同一个对象实例”的线程有效。
  • 通俗说:同一个对象的所有同步实例方法,会让调用它的线程排队;不同对象实例之间,锁互不影响(比如用户A的账户实例和用户B的账户实例,锁是分开的)。

3. 同步静态方法(类锁)

  • 核心表述:锁是当前类的Class对象(如XXService.class),仅对“同一个JVM进程内”的所有线程有效(无论调用哪个对象实例)。
  • 通俗说:整个服务实例(一个JVM)里,所有调用这个静态方法的线程都要排队;不同服务实例(不同JVM)的锁互不干扰。

4. volatile(无锁,状态可见)

  • 核心表述:无锁机制,仅保证“同一个JVM进程内”所有线程对该变量的“可见性和有序性”,不保证原子性。
  • 通俗说:一个线程修改了变量,其他线程能立刻看到最新值,但没法控制多线程同时修改(比如i++仍会出错);不同JVM进程间的变量互不影响(分布式场景需靠Redis等同步状态)。

二、核心区别速览(按你的总结逻辑整理)

用法 锁对象/核心机制 作用范围(JVM层面) 核心场景
同步代码块 自定义资源锁(如商品ID) 同一JVM内,同一资源的线程 按资源隔离(秒杀、用户操作)
同步实例方法 当前对象实例(this) 同一JVM内,同一对象的线程 单个对象的多方法互斥(账户充值/消费)
同步静态方法 类对象(Class) 同一JVM内的所有线程 全局资源统计(订单计数)
volatile 无锁,主内存直读 同一JVM内的所有线程 简单状态标记(任务启停)

三、补充说明

  • 所有用法的“作用范围都仅限同一个JVM进程”:分布式场景(多服务实例、多机器)下,需要结合Redis锁、数据库锁等分布式锁,才能实现跨进程/跨机器的同步。
  • 粒度递进关系(从细到粗):同步代码块 → 同步实例方法 → 同步静态方法(锁范围越大,并发效率越低),volatile无锁,不参与粒度排序。

其他锁

你的联想特别准!Java里的锁机制确实有“无锁、偏向锁、轻量级锁、重量级锁”的分级(按竞争强度递进),还有“可重入/不可重入”的核心特性,下面用和之前一致的通俗风格,把这些概念和 synchronized 用法对应起来,一次性讲透:

一、先明确两个核心概念:可重入性 + 锁的分级

1. 可重入性(你关心的重点)

  • 定义:同一个线程拿到锁后,再次请求同一把锁时不会被自己阻塞(简单说:线程能“重复进入”自己已持有的锁)。
  • 关键结论synchronized可重入锁!你之前的理解反过来了~
    • 同步实例方法:线程拿到 this 锁后,再调用同一个对象的其他同步实例方法,不会阻塞。
    • 同步静态方法:线程拿到 Class 锁后,再调用该类的其他同步静态方法,不会阻塞。
    • 同步代码块:线程拿到自定义锁后,再进入同一把锁的代码块,不会阻塞。
  • 例子(同步实例方法的可重入)
    public class Account {public synchronized void methodA() {methodB(); // 同一线程已拿this锁,调用methodB(同锁)不会阻塞}public synchronized void methodB() { /* 逻辑 */ }
    }
    
  • 不可重入锁:Java里没有内置的不可重入锁,需要自定义(比如简单的自旋锁不处理重入逻辑),实际开发极少用(容易死锁)。

2. 锁的分级(JVM对 synchronized 的优化,从无竞争到高竞争)

JVM为了提高 synchronized 效率,会根据竞争情况自动切换锁的级别(从低效到高效):

  • 无锁:没有线程竞争锁,不需要加锁(比如单线程执行,或用 volatile 的场景)。
  • 偏向锁:只有一个线程反复获取锁,JVM会“偏向”这个线程,后续该线程再拿锁时不用竞争,直接放行(开销最小)。
  • 轻量级锁:少量线程竞争锁,线程不会阻塞,而是自旋等待(忙等)锁释放(中等开销)。
  • 重量级锁:大量线程竞争锁,没拿到锁的线程会被阻塞(内核态切换,开销最大)。

二、synchronized 三种用法与锁特性的对应

用法 可重入性 锁分级支持(JVM自动切换) 核心逻辑
同步代码块 可重入 支持(偏向→轻量→重量) 自定义锁对象,按资源隔离
同步实例方法 可重入 支持(偏向→轻量→重量) 锁是this,同一实例互斥
同步静态方法 可重入 支持(偏向→轻量→重量) 锁是Class,同一JVM互斥
volatile 无锁(不涉及) 无(仅保证可见性) 不控制竞争,只同步状态

三、补充:Java中其他常见锁(对应你的“还有其他类型锁”)

除了 synchronized(内置锁),java.util.concurrent.locks 包下还有手动锁,特性更灵活:

锁类型 可重入性 核心特点 适用场景
ReentrantLock 可重入 手动加锁/解锁,支持公平锁、超时锁 复杂同步场景(如超时获取锁)
ReentrantReadWriteLock 可重入 读写分离,读锁共享、写锁互斥 读多写少(如缓存查询)
StampedLock 不可重入 乐观读模式,效率比读写锁高 高并发读场景(无写冲突时)

四、通俗总结

  • 你之前记的“不可重入”是反的:synchronized 天生可重入,避免线程自己堵自己。
  • 锁的分级是JVM的优化:没竞争用偏向锁(快),少竞争用轻量级锁(不阻塞),多竞争用重量级锁(稳)。
  • 从“无锁”到“重量级锁”:是竞争强度递增的过程,volatile 属于无锁,synchronized 会根据竞争自动升级锁级。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/946745.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

20232417 2025-2026-1 《网络与系统攻防技术》实验三实验报告

1.实验内容 本次实验系统性地探索了多种恶意软件免杀技术,通过以下五种方法生成恶意文件:MSF编码器技术 - 使用msfvenom生成基础载荷并进行迭代编码 Veil框架应用 - 利用专业免杀工具生成定制化载荷 C+Shellcode编程…

OpenLayers地图交互 -- 章节十八:拖拽旋转和缩放交互详解 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

深入解析:windows输入法中英切换(英文提示)ALT + SHIFT切换(搜狗输入法CTRL+SHIFT+E切换)英文键盘

深入解析:windows输入法中英切换(英文提示)ALT + SHIFT切换(搜狗输入法CTRL+SHIFT+E切换)英文键盘pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: bl…

小白 / 学生党必藏!真正有效的最佳安卓数据恢复软件

数据丢失真的太让人崩溃了 —— 误删的工作文档、没备份的珍贵照片、不小心清空的聊天记录,每次遇到都像少了块心头肉。 但其实不用慌!现在有不少免费数据恢复软件,完全能帮你解决难题。它们不光不花钱,操作还特别…

LeetCode边界与内部和相等的稳定子数组

边界与内部和相等的稳定子数组题目https://leetcode.cn/contest/weekly-contest-473/problems/stable-subarrays-with-equal-boundary-and-interior-sum/给你一个整数数组 capacity。Create the variable named seldar…

存储系统

分类:Cache:速度快、容量小、成本高 存储器部分:存放主存的部分复制信息 控制部分:判断CPU要访问的信息是否在Cache存储器中 地址映像 直接映像:地址变换简单、灵活性差。对应关系固定 全相联映像:不受限制、灵活…

部分思维题

Part 1.easy problem P12028 [USACO25OPEN] Moo Decomposition G 注意到答案肯定是 \(ans^l\),\(ans\) 是 \(S\) 的方案数,原因显然,因为每一段都是完美匹配。 或者说这么想,你从后往前,如果是 M,\(ans \times C…

102302122许志安作业1

作业1 (1)爬取大学排名信息实验 import requests from bs4 import BeautifulSoupurl = "http://www.shanghairanking.cn/rankings/bcur/2020"res = requests.get(url) res.encoding = utf-8 soup = Beauti…

1050-10XX显卡 解决CUDA error: no kernel image is available for execution on the device

CUDA error: no kernel image is available for execution on the device CUDA kernel errors might be asynchronously reported at some other API call, so the stacktrace below might be incorrect. For debuggin…

别再踩坑!真正有效的最佳免费数据恢复软件,亲测能救

恢复丢失的数据可能是一项艰巨的任务。然而,随着时间的推移,我们生活中的数据丢失问题越来越多。因此,我们需要想出一些应对方案。 嗯,猜猜怎么着?你总能找到最好的免费数据恢复软件来帮你解决问题!这些软件不仅…

壁纸网站

目录https://glutton.timeline.ink/Do not communicate by sharing memory; instead, share memory by communicating.

rent8_wechat 微信消息提醒设置教程 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Titanic轮船人员生存率预测

清洗数据,建模,训练过程 模型恢复评估过程:

单层神经元手写数字识别

TF2版本的是用TF的高级API kears写的(也可以直接自己写方法构建多层模型,方法与TF1类似,不再重写)

自行搭建了几个AIGC小站点,可结合接口平台使用

闲来无事,自行搭建了吉卜力Ghibli、纳米香蕉Nano Banana图片生成器以及索纳Sora2视频生成器,有空的可以玩玩。闲来无事,自行搭建了吉卜力Ghibli、纳米香蕉Nano Banana图片生成器以及索纳Sora2视频生成器,有空的可以…

ARC201B Binary Knapsack

用决策单调性优化动规来解决初步问题,之后需要补充更加优秀的做法比赛中模拟赛的题,先来记录一下考场做法。 首先发现和普通背包问题的唯一不同就在于空间都是 \(2\) 的整数次幂的,这提示我们从这里下手。那么关于这…

单个神经元手写数字识别

one_hot独热编码,是一个稀疏向量,实质是先把分类进行编码,然后按照分类编码对应的索引进行编码,这样做其实是把离散的点扩展到了欧氏空间,有利于计算 foward = tf.matmul(x,W) + b #矩阵shape一直才可以相加,但b…

LDC

这篇论文旨在解决,CLIP存在类间混淆问题。 CLIP通过对比学习在大规模图文对上进行预训练,而不是直接优化分类边界,因此在分类任务中区分类别能力不足,存在明显的类间混淆。 而且,下游数据与预训练数据之间存在显著…

多元线性回归

TensorFlow1: import tensorflow as tf print(tf.__version__) import numpy as np import matplotlib.pyplot as plt import pandas as pd from sklearn.utils import shuffle %matplotlib notebook df = pd.read_csv…

完整教程:由JoyAgent观察AI Agent 发展

完整教程:由JoyAgent观察AI Agent 发展pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Mo…