优惠卷秒杀
约 5384 字大约 18 分钟
2025-06-30
3.1 全局唯一 ID
每个店铺都可以发布优惠券,用户抢购时会生成订单并保存到 tb_voucher_order
表中。如果订单表使用数据库自增 ID,会存在以下问题:
- ID 的规律性太明显,容易被用户或竞争对手猜测出敏感信息,例如商城一天的订单量。
- 受单表数据量的限制,MySQL 单表容量不宜超过 500W。数据量过大时需要进行拆库拆表,但拆分后逻辑上是同一张表,因此 ID 不能重复,需要保证唯一性。
为此,考虑全局 ID 生成器,其是一种在分布式系统下生成全局唯一 ID 的工具,满足以下特性:
- 唯一性:必须保证生成的 ID 是全局唯一的。
- 高可用:任何时候都能正确的生成 ID。
- 高性能:需要满足高并发场景下的 ID 生成需求。
- 递增性:最好是趋势递增的,符合 MySQL 的 InnoDB 引擎特性。
- 安全性:ID 中最好不包含敏感信息。
Redis 提供了一个便捷的自增功能,可用于生成不重复的 ID。这种方法具备简单高效的优点,并且 Redis 的自增操作是原子性的,即使在高并发环境下也能保证 ID 的唯一性。每个 ID 的构成如下:
- 符号位:1 bit,永远为 0。
- 时间戳:31 bit,以秒为单位,可以使用 69 年。
- 序列号:32 bit,秒内的计数器,支持每秒产生 232 个不同 ID。
3.2 Redis 实现全局唯一 ID
使用 Redis 实现全局唯一 ID 的 Java 代码如下:
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
@Resource
private RedisIdWorker redisIdWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
void testRedisIdWorker() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
} latch.countDown();
};
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
es.submit(task);
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("time = " + (end - begin));
}
CountDownLatch
CountDownLatch
是一种同步工具类,用于协调多个线程之间的等待与唤醒。可以理解为一个倒计时计数器,当计数器值减为 0 时,所有等待的线程都会被释放。
countDown()
方法:将计数器值减 1。await()
方法:阻塞当前线程,直到计数器值变为 0。
在上述代码中,CountDownLatch
用于等待所有生成 ID 的任务完成。如果没有 CountDownLatch
,主线程 testRedisIdWorker
可能会在异步任务完成之前结束。
3.3 添加优惠券
每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购。
tb_voucher
:优惠券的基本信息,如优惠金额、使用规则等。tb_seckill_voucher
:优惠券的库存、开始抢购时间、结束抢购时间。特价优惠券才需要填写这些信息。
平价券优惠力度较小,可以任意领取。特价券优惠力度大,需要限制数量。从表结构上也能看出,特价券除了具有优惠券的基本信息外,还具有库存、抢购时间、结束时间等字段。
项目中已经实现添加优惠券、特价秒杀券的功能。现只需借助 Postman 发送 POST
http://localhost:8081/voucher/seckill
并携带一下内容,即可添加特价秒杀券。
{
"shopId": 1,
"title": "100 元代金券",
"subTitle": "周一至周五均可使用",
"rules": "全场通用\\n无需预约\\n可无限叠加\\n不兑现、不找零\\n仅限堂食",
"payValue": 8000,
"actualValue": 10000,
"type": 1,
"stock": 100,
"beginTime": "2025-06-30T10:09:17",
"endTime": "2025-07-02T17:30:00"
}
3.4 实现秒杀下单
秒杀下单需要考虑以下两点:
- 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单。
- 库存是否充足,不足则无法下单。

@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 1. 查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2. 判断秒杀是否开始
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(voucher.getBeginTime())) {
return Result.fail(MessageConstants.ACTIVITY_NOT_STARTED);
}
// 3. 判断秒杀是否结束
if (now.isAfter(voucher.getEndTime())) {
return Result.fail(MessageConstants.ACTIVITY_COMPLETED);
}
// 4. 判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail(MessageConstants.STOCK_OUT);
}
// 5. 扣减库存
boolean isSuccess = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
if (!isSuccess) {
return Result.fail(MessageConstants.STOCK_OUT);
}
// 6. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1 优惠券 id
voucherOrder.setVoucherId(voucherId);
// 6.2 订单 id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 6.3 用户 id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
save(voucherOrder);
return Result.ok(orderId);
}
代码解释:
seckillVoucher
方法用于处理秒杀下单的逻辑。
- 查询优惠券:根据
voucherId
从tb_seckill_voucher
表中查询秒杀券信息。 - 判断秒杀是否开始或结束:如果当前时间在秒杀开始时间之前或结束时间之后,则返回失败。
- 判断库存是否充足:如果库存小于 1,则返回失败。
- 扣减库存:使用
update()
方法,执行 SQL 语句stock = stock - 1
,扣减库存。 - 创建订单:
- 创建
VoucherOrder
对象。 - 使用
redisIdWorker.nextId("order")
生成订单 ID。 - 设置用户 ID、代金券 ID。
- 保存订单信息到
tb_voucher_order
表。
- 创建
3.5 库存超卖问题分析
在上述代码中,存在超卖问题。考虑多线程场景:假设线程 1 查询库存,判断库存大于 1,正准备扣减库存,但尚未扣减。此时线程 2 也查询库存,发现库存也大于 1。两个线程都会扣减库存,导致超卖。

超卖问题是典型的多线程安全问题,可以通过加锁解决。加锁通常有两种解决方案:悲观锁和乐观锁。
- 悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。例如
Synchronized
、Lock
都属于悲观锁. - 乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。
- 如果没有修改则认为是安全的,自己才更新数据。
- 如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。
乐观锁有两种常见方案:
- 版本号法。
- CAS 法(Compare And Swap) 。
版本号法的流程如下:
假设当前有两个并发线程(线程 1 和线程 2)都试图对库存进行减扣操作。
线程 1 的操作步骤:
查询库存和版本号
线程 1 从数据库查询数据,得到stock = 1
和version = 1
。判断库存是否大于 0
线程 1 判断stock > 0
是否成立,因为当前stock = 1
,条件为真,可以进行减扣操作。尝试执行减扣操作和版本号更新 线程 1 执行 SQL 语句:
UPDATE table SET stock = stock - 1, version = version + 1 WHERE id = 10 AND version = 1;
这里重要的是
WHERE
子句version = 1
,它保证了只有当数据库中的version
仍然是 1 时,才会执行更新操作。
由于没有任何竞争条件,线程 1 成功更新数据库,数据变为:stock = 0
version = 2
线程 2 的操作步骤:
查询库存和版本号
- 线程 2 同样查询数据,也得到
stock = 1
和version = 1
。
- 线程 2 同样查询数据,也得到
判断库存是否大于 0
线程 2 判断stock > 0
同样成立,因此也认为可以执行减扣操作。执行减扣操作和版本号更新
线程 2 执行同样的 SQL 语句:UPDATE table SET stock = stock - 1, version = version + 1 WHERE id = 10 AND version = 1;
但由于线程 1 已经更新了数据,此时数据库中的
version
已经是 2 而不是 1,因此线程 2 的更新条件不匹配,这条 SQL 语句将不会更新任何数据。

对于商品库存场景,实际上存在与 version
字段功能相同的另一字段,即 stock
。由此,CAS 法的流程如下:
假设当前有两个并发线程(线程 1 和线程 2)都试图对库存进行减扣操作。
线程 1 的操作步骤:
查询库存和版本号
线程 1 从数据库查询数据,得到stock = 1
。判断库存是否大于 0
线程 1 判断stock > 0
是否成立,因为当前stock = 1
,条件为真,可以进行减扣操作。尝试执行减扣操作和版本号更新 线程 1 执行 SQL 语句:
UPDATE table SET stock = stock - 1, version = version + 1 WHERE id = 10 and stock = 1;
这里重要的是
WHERE
子句stock = 1
,它保证了只有当数据库中的stock
仍然是 1 时,才会执行更新操作。
由于没有任何竞争条件,线程 1 成功更新数据库,数据变为:stock = 0
线程 2 的操作步骤:
查询库存和版本号
- 线程 2 同样查询数据,也得到
stock = 1
。
- 线程 2 同样查询数据,也得到
判断库存是否大于 0
线程 2 判断stock > 0
同样成立,因此也认为可以执行减扣操作。执行减扣操作和版本号更新
线程 2 执行同样的 SQL 语句:UPDATE table SET stock = stock - 1, version = version + 1 WHERE id = 10 AND stock = 1;
但由于线程 1 已经更新了数据,此时数据库中的
stock
已经是 0 而不是 1,因此线程 2 的更新条件不匹配,这条 SQL 语句将不会更新任何数据。

3.6 乐观锁解决超卖问题
根据 CAS 法的思想,可将代码修改如下:
boolean isSuccess = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update();
这段代码尝试通过乐观锁的方式,基于 voucher_id
和当前 stock
值来更新库存。其逻辑是,只有当数据库中 voucher_id
对应的 stock
值与程序中读取的 voucher.getStock()
相等时,才会执行 stock - 1
的操作。
然而,在实际测试中,这种方法在高并发情况下失败率很高。原因在于:
- 并发竞争:假设有 100 个线程同时尝试购买秒杀券,并且都读取到
stock
值为 100。 - CAS 竞争失败:这 100 个线程同时执行
update
操作。由于 CAS 的原子性,只有一个线程能够成功地将stock
更新为 99。 - 剩余线程失败:剩下的 99 个线程在执行
update
时,由于数据库中的stock
已经不是 100 了,eq("stock", voucher.getStock())
条件不满足,导致更新失败。
为了解决之前乐观锁方案中成功率过低的问题,可对乐观锁做如下优化:将判断条件放宽至 stock
大于 0。
修改后的代码如下:
boolean isSuccess = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).gt("stock",0).update();
多个线程可能同时看到库存大于 0 并尝试扣减,但是,由于我们使用了 stock > 0
作为 update
的条件,当一个线程成功执行 stock = stock - 1
后,stock
的值会发生变化。
- 如果
stock
变为 0:后续尝试执行update
的线程会因为不满足stock > 0
的条件而更新失败,从而避免了超卖。 - 如果
stock
仍然大于 0:后续尝试执行update
的线程仍然有可能成功,直到stock
变为 0。
3.6 优惠券秒杀 - 一人一单
当前优惠券业务存在一个问题:单个用户可以无限制地抢购并使用优惠券,这与优惠券作为引流手段的初衷不符。为了解决这个问题,我们需要增加一层逻辑,限制每个用户只能针对特定优惠券下一单。
在下单前,系统需要进行以下检查:
- 时间充足性判断:首先,判断当前时间是否在优惠券的使用有效期内。只有在有效期内,才允许继续进行后续判断。
- 库存充足性判断:其次,判断当前优惠券的剩余库存是否足够。只有当库存充足时,才允许继续进行后续判断。
- 用户下单记录查询:然后,根据优惠券 ID 和用户 ID 查询该用户是否已经下过该优惠券的订单。
- 如果该用户已经下过该优惠券的订单,则拒绝下单。
- 如果该用户尚未下过该优惠券的订单,则允许下单。
3.6.1 增加一人一单逻辑
下面是增加了 “一人一单” 逻辑的初步代码实现:
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 1. 查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2. 判断秒杀是否开始
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(voucher.getBeginTime())) {
return Result.fail(MessageConstants.ACTIVITY_NOT_STARTED);
}
// 3. 判断秒杀是否结束
if (now.isAfter(voucher.getEndTime())) {
return Result.fail(MessageConstants.ACTIVITY_COMPLETED);
}
// 4. 判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail(MessageConstants.STOCK_OUT);
}
// 5. 一人一单
// 5.1 获取用户 id
Long userId = UserHolder.getUser().getId();
// 5.2 判断是否已有购买记录
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail(MessageConstants.SINGLE_ORDER_LIMIT_REACHED);
}
// 6. 扣减库存
boolean isSuccess = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).gt("stock",0).update();
if (!isSuccess) {
return Result.fail(MessageConstants.STOCK_OUT);
}
// 7. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1 优惠券 id
voucherOrder.setVoucherId(voucherId);
// 7.2 订单 id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 7.3 用户 id
voucherOrder.setUserId(userId);
save(voucherOrder);
return Result.ok(orderId);
}
上述代码虽然增加了 “一人一单” 的逻辑,但在高并发场景下仍然存在问题。由于多线程并发访问数据库,可能出现多个线程同时查询到用户没有下单记录,导致重复创建订单。
3.6.2 使用悲观锁解决并发问题
为了解决并发问题,我们需要加锁。由于插入数据不适合使用乐观锁,因此我们选择使用悲观锁。
我们可以封装一个 createVoucherOrder
方法,并在方法上添加 synchronized
锁来保证线程安全。
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
// 1. 一人一单
// 1.1 获取用户 id
Long userId = UserHolder.getUser().getId();
// 1.2 判断是否已有购买记录
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail(MessageConstants.SINGLE_ORDER_LIMIT_REACHED);
}
// 2. 扣减库存
boolean isSuccess = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).gt("stock",0).update();
if (!isSuccess) {
return Result.fail(MessageConstants.STOCK_OUT);
}
// 3. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 3.1 优惠券 id
voucherOrder.setVoucherId(voucherId);
// 3.2 订单 id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 3.3 用户 id
voucherOrder.setUserId(userId);
save(voucherOrder);
return Result.ok(orderId);
}
这种方式存在锁粒度过粗的问题,因为 synchronized
锁的对象是 this
,会导致所有用户的请求都需要竞争同一把锁,影响并发性能。
3.6.3 细化锁粒度
为了提高并发性能,我们需要细化锁的粒度,只对同一用户加锁。
@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {
public Result createVoucherOrder(Long voucherId) {
// 1. 一人一单
// 1.1 获取用户 id
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
// 1.2 判断是否已有购买记录
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail(MessageConstants.SINGLE_ORDER_LIMIT_REACHED);
}
// 2. 扣减库存
boolean isSuccess = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).gt("stock",0).update();
if (!isSuccess) {
return Result.fail(MessageConstants.STOCK_OUT);
}
// 3. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 3.1 优惠券 id
voucherOrder.setVoucherId(voucherId);
// 3.2 订单 id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 3.3 用户 id
voucherOrder.setUserId(userId);
save(voucherOrder);
return Result.ok(orderId);
}
}
代码解释:
userId.toString().intern()
:intern()
方法从常量池中获取字符串对象,确保所有相同userId
的线程都持有同一把锁。如果直接使用userId.toString()
,每次都会创建新的字符串对象,导致锁失效。
3.6.4 事务失效问题
上述代码仍然存在问题,因为 createVoucherOrder
方法被 Spring 的事务管理。如果在方法内部加锁,可能会导致事务提交前锁就被释放,从而出现并发安全问题。
解决方案:将加锁操作放在 seckillVoucher
方法中,确保事务的完整性。
@Override
public Result seckillVoucher(Long voucherId) {
// 1. 查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2. 判断秒杀是否开始
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(voucher.getBeginTime())) {
return Result.fail(MessageConstants.ACTIVITY_NOT_STARTED);
}
// 3. 判断秒杀是否结束
if (now.isAfter(voucher.getEndTime())) {
return Result.fail(MessageConstants.ACTIVITY_COMPLETED);
}
// 4. 判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail(MessageConstants.STOCK_OUT);
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
return createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 1. 一人一单
// 1.1 获取用户 id
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
// 1.2 判断是否已有购买记录
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail(MessageConstants.SINGLE_ORDER_LIMIT_REACHED);
} // 2. 扣减库存
boolean isSuccess = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).gt("stock",0).update();
if (!isSuccess) {
return Result.fail(MessageConstants.STOCK_OUT);
}
// 3. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 3.1 优惠券 id
voucherOrder.setVoucherId(voucherId);
// 3.2 订单 id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 3.3 用户 id
voucherOrder.setUserId(userId);
save(voucherOrder);
return Result.ok(orderId);
}
}
3.6.5 事务生效问题
虽然将加锁操作放在 seckillVoucher
方法中可以解决事务提前释放的问题,但又引入了新的问题:seckillVoucher
方法通过 this.
调用 createVoucherOrder
方法,这会导致事务失效。
Spring 的事务是通过代理来实现的。只有通过代理对象调用方法,事务才能生效。而 this.
调用绕过了代理,导致事务失效。
解决方案:从 Spring 容器中获取代理对象,然后通过代理对象调用 createVoucherOrder
方法,确保事务生效。
@Override
public Result seckillVoucher(Long voucherId) {
// 1. 查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2. 判断秒杀是否开始
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(voucher.getBeginTime())) {
return Result.fail(MessageConstants.ACTIVITY_NOT_STARTED);
}
// 3. 判断秒杀是否结束
if (now.isAfter(voucher.getEndTime())) {
return Result.fail(MessageConstants.ACTIVITY_COMPLETED);
}
// 4. 判断库存是否充足
if (voucher.getStock() < 1) {
return Result.fail(MessageConstants.STOCK_OUT);
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
return createVoucherOrder(voucherId);
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
代码解释:
- 当前对象的代理对象即为
IVoucherOrderService
,因此可以用IVoucherOrderService
对象接收,并强转即可。 - 为使得代码对象能成功运行,应在接口中添加对应方法。在本例中,即在
IVoucherOrderService
创建createVoucherOrder
方法。
获取代理对象
为成功获取代理对象,还有两个要求:
- 添加依赖。
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
- 启动类添加注解:暴露代理对象。
@EnableAspectJAutoProxy(exposeProxy = true) @MapperScan("com.hmdp.mapper") @SpringBootApplication public class HmDianPingApplication { public static void main(String[] args) { SpringApplication.run(HmDianPingApplication.class, args); } }
3.7 集群环境下的并发问题
通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下失效。
按照如下步骤,可模拟集群环境:
将服务启动两份,端口分别为 8081 和 8082。
- 在 IDEA 的 Service 中选中当前项目的 Application,按住
Ctrl
D
后即可复制。修改新项目的端口如下:
- 在 IDEA 的 Service 中选中当前项目的 Application,按住
修改 nginx 的 conf 目录下的 nginx.conf 文件,配置反向代理和负载均衡。
http { include mime.types; default_type application/json; sendfile on; keepalive_timeout 65; server { listen 8080; server_name localhost; # 指定前端项目所在的位置 location / { root html/hmdp; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } location /api { default_type application/json; #internal; keepalive_timeout 30s; keepalive_requests 1000; #支持keep-alive proxy_http_version 1.1; rewrite /api(/.*) $1 break; proxy_pass_request_headers on; #more_clear_input_headers Accept-Encoding; proxy_next_upstream error timeout; # proxy_pass http://127.0.0.1:8081; proxy_pass http://backend; // [!code highlight] } } upstream backend { server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1; server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1; // [!code highlight] }
集群环境下 synchronized
锁的失效原因分析

在集群环境中,由于每个 Tomcat 实例运行在独立的 JVM 中,即使多个 Tomcat 实例中的线程执行相同的代码,它们的锁对象也是不同的。这导致了 synchronized
锁在单 JVM 环境下的互斥性无法跨 JVM 实例生效,从而导致锁失效。
假设存在服务器 A 和服务器 B,它们都部署了 Tomcat 应用。
- 服务器 A 的 Tomcat 内部有两个线程(线程 1 和线程 2),它们共享同一份代码,因此它们的
synchronized
锁对象是同一个,可以实现互斥。 - 服务器 B 的 Tomcat 内部也有两个线程(线程 3 和线程 4),它们的代码与服务器 A 相同,但由于它们运行在不同的 JVM 中,锁对象与服务器 A 的线程 1 和线程 2 不同。因此,线程 3 和线程 4 之间可以实现互斥,但无法与线程 1 和线程 2 实现互斥。