文章目录
- 难点1:synchronize
- synchronized 的底层实现
- 锁的具体操作
- 举例说明
- 结论
- 难点2:动态代理和@Transactional失效问题
- `@Transactional` 工作原理
- 关键点
- 示例分析
- 正确的使用方式
- 结论
- 建议
难点所在代码块
@Overridepublic Result seckillVoucher(Long voucherId) {
// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return fail("秒杀尚未开始");}
// 3.判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return fail("秒杀已经结束");}
// 4.判断库存是否充足if (voucher.getStock()<1) {return fail("库存不足");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){//避免线程安全问题,确保每次5返回的都是"5",而不是不一样的5.toString()//如果直接return createVoucherOrder(voucherId);拿到的是当前VoucherOrderServiceImpl对象,而不是他的代理对象//注意seckillVoucher方法没有加@Transactional,会导致如果创建订单失败,则不会回滚,所以需要加@Transactional//事务要想生效,是Spring对VoucherOrderServiceImpl做了动态代理,拿到了他的代理对象,用createVoucherOrder(voucherId);做的事务处理//直接return createVoucherOrder(voucherId);等于直接return this.createVoucherOrder(voucherId);指的是非代理对象//也就是目标对象,也就是没有事务功能// 所以我们要拿到事务代理的对象才行IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();//拿到当前对象的代理对象return proxy.createVoucherOrder(voucherId);//这里我们直接用代理对象proxy.createVoucherOrder(voucherId);,而不是直接this.createVoucherOrder(voucherId);
// 这样proxy就是由Spring创建的,proxy就是由Spring管理的,就可以带上事务功能
// 这么写代码还要在Spring启动类加上@EnableAspectJAutoProxy(proxyTargetClass = true)}
// 7.返回订单id}
这里是一个项目难点,在于为什么要userId.toString().inter(),直接userId.toString()不行么?原因在于如果是不同的线程来了,都是一个userId,那么每次toString()都是全新的String对象,JVM不是使用了equals方法去判断是不是同一个共享资源,而是采用了对象头信息去判断(来自于ChatGPT-4o的回答)。
synchronized
关键字在 Java 中用于实现同步块或方法,以确保多线程环境下对共享资源的访问是线程安全的。它通过在进入同步块或方法时获取对象锁(也称为监视器锁)来实现这一点。
难点1:synchronize
synchronized 的底层实现
synchronized
的底层实现依赖于 JVM 的内部机制。具体来说,它使用对象头(Object Header)中的锁信息来判断锁的对象是不是同一个对象。下面是一些关键点:
-
对象头(Object Header):
- 每个 Java 对象在内存中都有一个对象头。对象头中包含了对象的元数据,包括锁的信息。
- 对象头的结构因 JVM 实现而异,但通常包含一个 Mark Word,用于存储锁标志位、哈希码、GC 信息等。
-
锁的状态:
- Java 中的锁有多种状态,如无锁(Unlocked)、偏向锁(Biased Lock)、轻量级锁(Lightweight Lock)、重量级锁(Heavyweight Lock)。
- 锁的状态转换和锁的获取释放都是通过对象头中的 Mark Word 来管理的。
-
锁的判断:
- 当一个线程进入同步块或方法时,JVM 会尝试获取对象头中的锁。如果对象已经被其他线程锁定,当前线程会被阻塞,直到锁被释放。
- JVM 通过比较对象头中的 Mark Word 来判断锁的对象是不是同一个对象。如果两个同步块的锁对象是同一个对象,那么它们的对象头的 Mark Word 应该相同。
锁的具体操作
当线程执行 synchronized (obj)
时:
-
锁的获取:
- 检查对象头中的 Mark Word 以确定当前锁的状态。
- 如果对象是无锁状态,JVM 会尝试使用 CAS(Compare And Swap)操作将对象头的 Mark Word 更新为当前线程的锁信息,表示当前线程获得了该对象的锁。
- 如果对象已经被锁定,JVM 会根据锁的状态(如偏向锁、轻量级锁、重量级锁)执行相应的锁获取逻辑,可能会导致线程阻塞或自旋。
-
锁的释放:
- 当线程退出同步块或方法时,JVM 会更新对象头中的 Mark Word 以释放锁。
- 如果有其他线程在等待该锁,JVM 会通知等待线程重新尝试获取锁。
举例说明
synchronized (userId.toString().intern()) {// critical section
}
在这段代码中:
userId.toString().intern()
返回一个字符串对象的引用。synchronized
块 会尝试获取该字符串对象的锁。- 对象头 中的 Mark Word 会记录当前线程对该对象的锁定信息。
- 当另一个线程执行相同的
synchronized
块时,JVM 会检查该字符串对象的 Mark Word,发现它已被锁定,从而进行相应的处理(如阻塞或自旋)。
结论
synchronized
通过对象头中的 Mark Word 来判断锁的对象是否相同,并通过锁状态的转换和管理来实现线程同步。这确保了多个线程在访问同一个共享资源时,不会出现线程安全问题。因此如果是"5".toString()的话,这里举的例子是userId=5,那么每次toString出来的String对象都是不一样的,我们的Mark Word也是不一样的,因此无法保证是同一个共享资源,因此需要添加intern()方法,保证直接引用的是常量池里面的内容!
难点2:动态代理和@Transactional失效问题
抽取上面代码块关键两行代码进行解读:
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();//拿到当前对象的代理对象
return proxy.createVoucherOrder(voucherId);//这里我们直接用代理对象proxy.createVoucherOrder(voucherId);,而不是直接
VoucherOrderServiceImpl的createVoucherOrder方法是@Transactional的,但是VoucherOrderServiceImpl的seckillVoucher方法是没有@Transactional的,如果没有@Transactional的方法调用了@Transactional的方法,会导致@Transactional方法失效,如果发生意外是无法回滚的,
如果直接return createVoucherOrder(voucherId);拿到的是当前VoucherOrderServiceImpl对象,而不是他的代理对象
注意seckillVoucher方法没有加@Transactional,会导致如果创建订单失败,则不会回滚,所以需要加@Transactional
事务要想生效,是Spring对VoucherOrderServiceImpl做了动态代理,拿到了他的代理对象,用createVoucherOrder(voucherId);做的事务处理
直接return createVoucherOrder(voucherId);等于直接return this.createVoucherOrder(voucherId);指的是非代理对象
也就是目标对象,也就是没有事务功能
所以我们要拿到事务代理的对象才行
在 Spring 框架中,@Transactional
注解用于声明式事务管理。了解 @Transactional
的工作原理和调用关系对于确保事务管理有效至关重要。
@Transactional
工作原理
Spring 的事务管理是通过 AOP(面向方面编程)实现的。@Transactional
注解的实现依赖于 Spring 的事务代理机制。代理对象会在方法调用前后进行事务的开启、提交或回滚操作。
关键点
- 代理对象:
@Transactional
依赖于 Spring 创建的代理对象来管理事务。当一个事务方法被调用时,实际上调用的是代理对象的方法,代理对象负责处理事务逻辑。 - 方法内部调用:如果一个非事务方法调用另一个标记了
@Transactional
的方法(在同一个类中),由于是直接调用,没有经过代理对象,事务不会生效。这是因为代理对象只能拦截外部调用,而无法拦截类内部的自调用。
示例分析
假设有以下类:
@Service
public class MyService {@Transactionalpublic void transactionalMethod() {// transactional code}public void nonTransactionalMethod() {transactionalMethod();}
}
在这个例子中:
- 如果外部类(或外部对象)调用
nonTransactionalMethod()
,事务管理不会生效,因为nonTransactionalMethod()
直接调用了transactionalMethod()
,绕过了代理对象。 - 如果外部类(或外部对象)直接调用
transactionalMethod()
,事务管理会生效,因为调用通过了代理对象。
正确的使用方式
为了确保事务管理有效,可以采取以下策略:
-
外部调用:
确保事务方法通过代理对象调用。可以将事务方法放在另一个类中,从外部调用它们。 -
自调用解决方案:
使用@Autowired
注入当前类的代理对象,确保内部调用也通过代理对象完成。@Service public class MyService {@Autowiredprivate MyService self;@Transactionalpublic void transactionalMethod() {// transactional code}public void nonTransactionalMethod() {self.transactionalMethod(); // 使用代理对象调用事务方法} }
结论
-
非事务方法调用事务方法:
- 事务管理不会生效,因为调用没有经过代理对象。
- 这种情况下
@Transactional
注解会失效。
-
事务方法调用事务方法:
- 事务管理会生效,但前提是调用通过代理对象进行。
- 如果事务方法内部调用另一个事务方法,必须确保调用通过代理对象,否则事务管理仍然可能失效。
建议
为了确保 @Transactional
注解有效,尽量通过外部类或注入的代理对象来调用事务方法,避免在同一个类内部直接调用带有 @Transactional
注解的方法。