柏虎资源网

专注编程学习,Python、Java、C++ 教程、案例及资源

多线程总出 bug?90% 人没搞懂这 4 种同步机制!附秒杀实战代码

在 Java 开发中,“多线程” 是提升程序效率的利器 —— 比如秒杀系统用多线程处理并发下单、后台任务用多线程批量处理数据。但很多开发者一写多线程就出问题:库存明明够却显示 “已抢完”、数据计算结果时而正确时而错乱。其实这些 bug 大多是 “线程不安全” 导致的,而解决问题的关键,就是用好 Java 的 4 种多线程同步机制。今天用大白话讲清它们的区别,再结合秒杀案例教你怎么用,以后写多线程再也不慌!

一、先搞懂:为啥需要 “同步机制”?

简单说,多线程就像 “多个工人同时抢一个工具”—— 如果不加以控制,就会出现 “工具被抢乱、活儿干错” 的情况。

举个通俗例子:假设秒杀系统有 100 件商品库存,用 1000 个线程模拟用户抢单。如果不做同步,多个线程会同时读取 “库存 = 100”,然后都执行 “库存 - 1”,最后可能出现 “100 个线程抢单,库存却只减了 10” 的 bug(这就是 “线程安全问题”)。

而 “同步机制” 就像 “工具管理员”,控制工人按顺序使用工具,保证每个操作都完整执行 —— 让 100 个抢单线程依次减库存,最终库存准确变为 0。

二、4 种同步机制:一张表分清,用法全掌握

很多人记不住同步机制的区别,其实用一张表对比核心特点和适用场景,立马就能分清:

同步机制

核心作用

优势

适用场景

synchronized 关键字

保证同一时刻只有一个线程执行代码

用法简单(不用手动释放锁)

简单同步场景(如单线程操作共享变量)

Lock 接口

手动控制锁的获取和释放

灵活(可尝试获取锁、超时释放)

复杂同步场景(如秒杀、分布式锁)

volatile 关键字

保证变量 “可见性”(线程读最新值)

轻量级(不阻塞线程)

变量只被单个线程写、多线程读的场景

wait/notify 机制

让线程 “等待” 或 “唤醒”,实现协作

控制线程执行顺序

线程间需通信的场景(如生产者 - 消费者)

简单解释关键概念:

可见性:一个线程修改了变量值,其他线程能立刻读到最新值(没有 volatile 的话,线程可能读缓存里的旧值);

原子性:操作要么全执行,要么全不执行(比如 “库存 - 1” 是原子操作,不会出现 “减到一半被其他线程打断” 的情况)。

三、实战案例:秒杀系统用同步机制防超卖(附完整代码)

光说理论没用,我们用 “秒杀库存扣减” 这个高频场景做案例,看看如何用Lock接口(最适合秒杀的同步机制)解决超卖问题,再对比 “无同步” 的 bug 效果。

需求场景:

模拟 1000 个用户同时抢购 100 件商品,要求:

防止超卖(库存不能减到负数);

记录每个用户的抢购结果(成功 / 失败);

统计最终成功抢购的人数(必须等于 100)。

代码实现思路:

用ReentrantLock(Lock 接口的实现类)控制库存扣减的同步,保证每个线程依次执行;

用CountDownLatch让 1000 个线程同时开始抢单(模拟真实并发场景)。

完整代码(带详细注释):

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

// 秒杀服务类

class SeckillService {

private int stock = 100; // 商品库存:100件

private int successCount = 0; // 成功抢购人数

// Lock接口实现类:创建可重入锁(同一线程可多次获取锁)

private Lock lock = new ReentrantLock();

// 抢购方法:返回true表示抢购成功,false表示失败

public boolean seckill(String userId) {

// 1. 手动获取锁(加锁):保证只有一个线程进入后续逻辑

lock.lock();

try {

// 2. 判断库存是否充足(必须在锁内判断,否则多线程会同时读旧库存)

if (stock > 0) {

// 3. 库存充足:扣减库存,增加成功人数

stock--;

successCount++;

System.out.println("用户" + userId + "抢购成功!剩余库存:" + stock);

return true;

} else {

// 4. 库存不足:抢购失败

System.out.println("用户" + userId + "抢购失败!商品已抢完");

return false;

}

} finally {

// 5. 手动释放锁(必须在finally里,防止代码抛异常导致锁不释放)

lock.unlock();

}

}

// 获取最终统计结果

public void getResult() {

System.out.println("-------------------");

System.out.println("秒杀结束!");

System.out.println("成功抢购人数:" + successCount);

System.out.println("剩余库存:" + stock);

}

}

// 测试类:模拟1000个并发用户

public class SeckillDemo {

public static void main(String[] args) throws InterruptedException {

SeckillService seckillService = new SeckillService();

int userCount = 1000; // 模拟1000个用户

// CountDownLatch:让1000个线程同时开始执行(倒计时器,减到0才继续)

CountDownLatch countDownLatch = new CountDownLatch(userCount);

// 循环创建1000个线程(每个线程代表一个用户)

for (int i = 1; i <= userCount; i++) {

String userId = "U" + i; // 生成用户ID(U1、U2...U1000)

new Thread(() -> {

try {

// 等待所有线程创建完成(倒计时器减1)

countDownLatch.countDown();

countDownLatch.await(); // 阻塞,直到所有线程都调用countDown()(倒计时器为0)

// 执行抢购

seckillService.seckill(userId);

} catch (InterruptedException e) {

e.printStackTrace();

}

}).start();

}

// 等待所有抢购线程执行完成

Thread.sleep(3000);

// 打印最终结果

seckillService.getResult();

}

}

代码运行结果(关键部分):

用户U56抢购成功!剩余库存:1

用户U78抢购成功!剩余库存:0

用户U99抢购失败!商品已抢完

...

-------------------

秒杀结束!

成功抢购人数:100

剩余库存:0

对比 “无同步” 的 bug 效果:

如果删掉lock.lock()和lock.unlock(),运行后会出现:

成功抢购人数超过 100(比如 105);

剩余库存变成负数(比如 - 5);

这就是 “超卖 bug”,而Lock接口通过同步控制,完美解决了这个问题。

其他机制的用法补充:

如果用synchronized替代Lock,只需把seckill方法改成synchronized修饰:

public synchronized boolean seckill(String userId) {

// 逻辑和之前一样,不用手动加锁解锁

}

如果变量stock只需 “单线程写、多线程读”,用volatile修饰即可(但秒杀场景需要原子性,volatile 不够,必须用 Lock 或 synchronized):

private volatile int stock = 100;

四、新手必记:3 个 “避坑” 小技巧

synchronized 和 Lock 别混用:比如用 synchronized 加锁,却用 Lock 解锁,会导致锁错乱;

Lock 必须手动释放:一定要在finally里写unlock(),否则代码抛异常后锁不释放,其他线程会一直阻塞;

volatile 不保证原子性:别以为用了 volatile 就万事大吉 —— 比如 “stock++” 是 “读 - 改 - 写” 三步操作,volatile 管不了,必须用锁保证原子性。

互动话题

你之前写多线程时,遇到过最头疼的 bug 是什么?是超卖、数据错乱,还是线程死锁?最后用哪种同步机制解决的?评论区说说你的经历,也聊聊你觉得哪种同步机制最难用~

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言