黑马点评项目难点-动态代理,sychronized,@Transactional失效的情况

news/2024/7/8 1:51:42 标签: java, AOP, Spring

文章目录

  • 难点1:synchronize
    • synchronized 的底层实现
    • 锁的具体操作
    • 举例说明
      • 结论
  • 难点2:动态代理和@Transactional失效问题
      • `@Transactional` 工作原理
      • 关键点
      • 示例分析
      • 正确的使用方式
      • 结论
      • 建议

难点所在代码块

java">    @Override

    public 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)中的锁信息来判断锁的对象是不是同一个对象。下面是一些关键点:

  1. 对象头(Object Header)

    • 每个 Java 对象在内存中都有一个对象头。对象头中包含了对象的元数据,包括锁的信息。
    • 对象头的结构因 JVM 实现而异,但通常包含一个 Mark Word,用于存储锁标志位、哈希码、GC 信息等。
  2. 锁的状态

    • Java 中的锁有多种状态,如无锁(Unlocked)、偏向锁(Biased Lock)、轻量级锁(Lightweight Lock)、重量级锁(Heavyweight Lock)。
    • 锁的状态转换和锁的获取释放都是通过对象头中的 Mark Word 来管理的。
  3. 锁的判断

    • 当一个线程进入同步块或方法时,JVM 会尝试获取对象头中的锁。如果对象已经被其他线程锁定,当前线程会被阻塞,直到锁被释放。
    • JVM 通过比较对象头中的 Mark Word 来判断锁的对象是不是同一个对象。如果两个同步块的锁对象是同一个对象,那么它们的对象头的 Mark Word 应该相同。

锁的具体操作

当线程执行 synchronized (obj) 时:

  1. 锁的获取

    • 检查对象头中的 Mark Word 以确定当前锁的状态。
    • 如果对象是无锁状态,JVM 会尝试使用 CAS(Compare And Swap)操作将对象头的 Mark Word 更新为当前线程的锁信息,表示当前线程获得了该对象的锁。
    • 如果对象已经被锁定,JVM 会根据锁的状态(如偏向锁、轻量级锁、重量级锁)执行相应的锁获取逻辑,可能会导致线程阻塞或自旋。
  2. 锁的释放

    • 当线程退出同步块或方法时,JVM 会更新对象头中的 Mark Word 以释放锁。
    • 如果有其他线程在等待该锁,JVM 会通知等待线程重新尝试获取锁。

举例说明

java">synchronized (userId.toString().intern()) {
    // critical section
}

在这段代码中:

  1. userId.toString().intern() 返回一个字符串对象的引用。
  2. synchronized 会尝试获取该字符串对象的锁。
  3. 对象头 中的 Mark Word 会记录当前线程对该对象的锁定信息。
  4. 当另一个线程执行相同的 synchronized 块时,JVM 会检查该字符串对象的 Mark Word,发现它已被锁定,从而进行相应的处理(如阻塞或自旋)。

结论

synchronized 通过对象头中的 Mark Word 来判断锁的对象是否相同,并通过锁状态的转换和管理来实现线程同步。这确保了多个线程在访问同一个共享资源时,不会出现线程安全问题。因此如果是"5".toString()的话,这里举的例子是userId=5,那么每次toString出来的String对象都是不一样的,我们的Mark Word也是不一样的,因此无法保证是同一个共享资源,因此需要添加intern()方法,保证直接引用的是常量池里面的内容!

难点2:动态代理和@Transactional失效问题

抽取上面代码块关键两行代码进行解读:

java">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 的事务代理机制。代理对象会在方法调用前后进行事务的开启、提交或回滚操作。

关键点

  1. 代理对象@Transactional 依赖于 Spring 创建的代理对象来管理事务。当一个事务方法被调用时,实际上调用的是代理对象的方法,代理对象负责处理事务逻辑。
  2. 方法内部调用:如果一个非事务方法调用另一个标记了 @Transactional 的方法(在同一个类中),由于是直接调用,没有经过代理对象,事务不会生效。这是因为代理对象只能拦截外部调用,而无法拦截类内部的自调用。

示例分析

假设有以下类:

java">@Service
public class MyService {
    
    @Transactional
    public void transactionalMethod() {
        // transactional code
    }

    public void nonTransactionalMethod() {
        transactionalMethod();
    }
}

在这个例子中:

  • 如果外部类(或外部对象)调用 nonTransactionalMethod(),事务管理不会生效,因为 nonTransactionalMethod() 直接调用了 transactionalMethod(),绕过了代理对象。
  • 如果外部类(或外部对象)直接调用 transactionalMethod(),事务管理会生效,因为调用通过了代理对象。

正确的使用方式

为了确保事务管理有效,可以采取以下策略:

  1. 外部调用
    确保事务方法通过代理对象调用。可以将事务方法放在另一个类中,从外部调用它们。

  2. 自调用解决方案
    使用 @Autowired 注入当前类的代理对象,确保内部调用也通过代理对象完成。

    java">@Service
    public class MyService {
    
        @Autowired
        private MyService self;
    
        @Transactional
        public void transactionalMethod() {
            // transactional code
        }
    
        public void nonTransactionalMethod() {
            self.transactionalMethod(); // 使用代理对象调用事务方法
        }
    }
    

结论

  1. 非事务方法调用事务方法

    • 事务管理不会生效,因为调用没有经过代理对象。
    • 这种情况下 @Transactional 注解会失效。
  2. 事务方法调用事务方法

    • 事务管理会生效,但前提是调用通过代理对象进行。
    • 如果事务方法内部调用另一个事务方法,必须确保调用通过代理对象,否则事务管理仍然可能失效。

建议

为了确保 @Transactional 注解有效,尽量通过外部类或注入的代理对象来调用事务方法,避免在同一个类内部直接调用带有 @Transactional 注解的方法。


http://www.niftyadmin.cn/n/5535829.html

相关文章

在大型项目中,怎样有效地组织和管理 SCSS 文件结构以提高开发效率?

在大型项目中&#xff0c;组织和管理 SCSS 文件结构是非常重要的&#xff0c;可以提高开发效率和代码的可维护性。下面是一些有效的方法&#xff1a; 使用模块化和层次化的文件结构&#xff1a;将 SCSS 文件按照模块进行组织&#xff0c;每个模块包含相关的样式规则。可以使用文…

【多媒体】Java实现MP4和MP3音视频播放器【JavaFX】【音视频播放】

在Java中播放音视频可以使用多种方案&#xff0c;最常见的是通过Swing组件JFrame和JLabel来嵌入JMF(Java Media Framework)或Xuggler。不过&#xff0c;JMF已经不再被推荐使用&#xff0c;而Xuggler是基于DirectX的&#xff0c;不适用于跨平台。而且上述方案都需要使用第三方库…

上位机GUI 第三弹

&#x1f60a; &#x1f60a; &#x1f60a; 从协议层面讲&#xff0c;地质单元相当重要&#xff0c;调试模式,我只能义命令发送的索引码作为,每个设备的区分方式,调试的情况&#xff0c;不在设备上设置任何东西&#xff0c;开机访问地址和端口就能用 因为懒&#xff0c;直接将…

0058__NTFS重解析点(Reparse Points)

NTFS重解析点&#xff08;Reparse Points&#xff09;-CSDN博客

刷代码随想录有感(125):动态规划——最长公共子序列

题干&#xff1a; 代码&#xff1a; class Solution { public:int longestCommonSubsequence(string text1, string text2) {vector<vector<int>>dp(text1.size() 1, vector<int>(text2.size() 1, 0));for(int i 1; i < text1.size(); i){for(int j …

redis服务介绍

redis 基础概念安装使用基础操作命令数据类型操作命令管理和维护命令 基础概念 Remote Dictionary Server&#xff08;Redis&#xff09;远程字典服务器是完全开源免费的&#xff0c;用C语言编写的&#xff0c;遵守BSD开源协议&#xff0c;是一个高性能的&#xff08;key/valu…

推荐一个私有化部署的物联网平台

引言 随着物联网技术的飞速发展&#xff0c;越来越多的企业开始寻求能够提供稳定、安全、可定制的物联网解决方案。私有化部署的物联网平台因其能够满足企业对数据安全和个性化需求的优势&#xff0c;逐渐成为市场的新宠。本文将详细介绍ThingsKit物联网平台&#xff0c;一个专…

React+TS前台项目实战(二十四)-- 全局常用绘制组件Qrcode封装

文章目录 前言Qrcode组件1. 功能分析2. 代码详细注释3. 使用方式4. 效果展示(pc端 / 移动端) 总结 前言 今天要封装的Qrcode 组件&#xff0c;是通过传入的信息&#xff0c;绘制在二维码上&#xff0c;可用于很多场景&#xff0c;如区块链项目中的区块显示交易地址时就可以用到…