基础部分
-
Java中的ThreadLocal原理是什么?使用时需要注意什么问题?
- ThreadLocal提供了一种线程局部变量的概念,使得每个使用该变量的线程都有自己独立的变量副本。在多线程环境下,每个线程访问自己的内部的副本变量,从而避免了线程安全问题。
- 存储结构:ThreadLocal内部通过一个ThreadLocalMap来存储线程局部变量。每个Thread对象内部都有一个ThreadLocalMap的引用。
- 键值对:在ThreadLocalMap中,ThreadLocal对象作为键,线程局部变量作为值存储。这样,每个线程都可以通过自己内部的ThreadLocalMap,使用threadLocal对象作为键来访问或修改自己的变量副本。
- 访问与操作:当线程第一次通过ThreadLocal调用get()或set()方法时,会触发ThreadLocal作为当前线程初始化一个属于自己的线程局部变量,并存储在该线程的ThreadLocalMap中。后续的访问和修改都是针对这个线程内部的副本。
- 使用时注意的问题:
- 内存泄漏:最常见的问题就是内存泄漏。ThreadLocalMap的生命周期与线程相同,如果线程不死亡,则ThreadLocalMap及其所有对应的Entry的生命周期也不会结束。如果ThreadLocal对象被回收,其对应的线程局部变量却可能因为ThreadLocalMap的存在而无法回收,造成内存泄漏。
- 解决方案:每次使用完TheadLocal后,都应该调用ThreadLocal.remove()方法来清除线程局部变量,避免内存泄漏。
- 线程池中的使用:在使用线程池的情况下,由于线程会被复用,如果前一个任务使用了ThreadLocal且没有进行清理,那么下一个任务可能会访问到上一个任务的数据。
- 解决方案:在任务执行结束前,确保调用了ThreadLocal.remove()方法,清理资源。
- 内存泄漏:最常见的问题就是内存泄漏。ThreadLocalMap的生命周期与线程相同,如果线程不死亡,则ThreadLocalMap及其所有对应的Entry的生命周期也不会结束。如果ThreadLocal对象被回收,其对应的线程局部变量却可能因为ThreadLocalMap的存在而无法回收,造成内存泄漏。
- 总结:ThreadLocal是解决多线程编程中,线程隔离的一种手段,但使用时需要注意内存泄漏问题,特别是在使用线程池的场景下,确保适时清理ThreadLocal中的数据。
-
说说你对Java中CAS的理解,以及ABA问题如何解决?
- CAS(Compare-And-Swap)是一种用于实现多线程环境下的无锁原子操作的机制。它涉及三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,不做任何操作。无论哪种情况,他都会返回操作前的内存位置的值。
- CAS工作原理:
- 1、读取当前值:从内存中读取当前值。
- 2、比较当前值:检查当前值是否等于预期值,如果是,则进入第三步;如果不是,则操作失败。
- 3、更新为新值:如果当前值等于预期值,则更新为新值。
- CAS的特点:
- 1、无锁:CAS操作提供了一种无锁的方式来实现线程安全,避免了传统锁机制可能引起的线程阻塞和上下文切换的开销。
- 2、原子性:CAS保证了比较和替换这两个操作的原子性。
- ABA问题
- ABA问题是指在CAS操作过程中,某个变量原来是A值,后来被改成了B值,然后又被修改回A,这是CAS操作检查时发现值仍为A,就会误认为没有被其他线程修改过,从而完成CAS操作。但实际上,该变量的值已经被修改过两次。
- ABA问题的解决:
- 解决ABA问题的常用方法是使用版本号或时间戳。每次变量更新时,除了改变数据本身,还要更新一个版本号。这样,即使数据被改回原值,版本号也会不同,CAS操作就能检测到变量已经被修改过。
- Java中,AtomicStampedReference类就是采用这种方式来解决ABA问题的。它维护了对象引用及其版本号,每次执行CAS操作时,不仅需要检查引用是否相等,还要检查版本号是否一致。
- 总结:CAS是一种重要的并发原理,它通过硬件层面保证了操作的原子性,是实现无锁编程的基础。然而,CAS存在ABA问题,需要通过引入版本号等机制来解决。在Java中,AtomicStampedReference提供了一种解决ABA问题的实现方式。
-
HashMap在JDK1.7和JDK1.8中的区别
- HashMap是Java中广泛使用的基于散列的Map实现。从JDK1.7到1.8,HashMap经历了重要的内部实现变化,主要体现在以下几个方面:
- 1、数据结构
- JDK1.7:HashMap内部使用数组+链表的结构。每个数组元素是一个链表的头节点,当发生哈希冲突时,新的元素会被添加到链表的末尾。
- JDK1.8:引入了数组+链表+红黑树的结构。当链表长度超过阈值(默认为8),链表回转换为红黑树,以减少搜索时间。
- 2、扩容过程
- 1.7:在扩容时,新数组的元素位置要么是在原位置,要么是在原位置加上旧数组长度的位置。这个过程需要重新计算每个元素的哈希值。
- 1.8:优化了扩容过程,通过位运算和原索引值来决定元素在新数组中的位置,无需重新计算哈希值,提高了扩容率。
- 3、插入方式
- 1.7:采用了头插法:新插入的节点插入到链表头部。这种方式在多线程环境下会引起循环链表,导致死循环。
- 1.8:采用尾插法:新插入的节点插入到链表尾部。这种方式避免了1.7中的问题。
- 为什么要做这些改变
- 提高搜索效率:通过引入红黑树,当链表长度过长时,将链表转换为红黑树,可以显著降低搜索时间。红黑树的平均查找时间复杂度为O(logn),而链表为O(n)。
- 提高扩容率:JDK1.8中扩容优化减少了重新计算哈希值的需要,通过简单的位运算就可以确定元素在新数组中的位置。
- 增强并发性:JDK1.7中的头插法在兵法环境下可能会形成环形链表,导致死循环。JDK1.8采用尾插法解决了这个问题,虽然HashMap本身非线程安全,但这种改进至少避免了因为扩容导致的死循环问题。
- 1、数据结构
- HashMap是Java中广泛使用的基于散列的Map实现。从JDK1.7到1.8,HashMap经历了重要的内部实现变化,主要体现在以下几个方面:
-
说说你对Java中锁的理解,包括synchronized和Lock的区别,以及各种锁优化措施。
- Java中的锁机制是兵法编程中的核心概念,主要用于控制多线程对共享资源的访问。以下是Java锁的全面理解:
- synchronized和Lock的比较
- synchronized
- 隐式锁:由JVM实现,使用简单。
- 非公平锁:默认情况下保证等待线程的获取顺序。
- 自动释放:synchronized块执行完毕后自动释放锁。
- 不可中断:一个线程获得锁后,其他线程只能等待。
- Lock接口(入ReentrantLock)
- 显式锁:需要手动加锁和解锁。
- 可选公平性:可以创建公平锁。
- 灵活控制:支持可中断、超时等待、尝试获取锁等操作。
- 条件变量:通过Condition接口可以实现多条件等待。
- synchronized
- 锁优化措施
- JVM层面的优化(主要针对synchronized)
- 偏向锁
- 目的:减少无竞争情况下的同步开销。
- 原理第一个获取锁的线程会将对象头标记为偏向自己。
- 轻量级锁
- 目的:在竞争不激烈时避免使用重量级锁。
- 原理:线程在自己的栈帧中创建锁记录,尝试使用CAS将对象头指向锁记录。
- 自旋锁
- 目的:避免线程在短期等待时被挂起和唤醒。
- 原理:JIT编译时,对不可能存在竞争的同步块进行消除。
- 锁粗化
- 目的:减少反复加锁解锁开销。
- 原理:将多个连续的加锁、解锁操作合并为一个较大的同步块。
- 偏向锁
- 编程层面的优化
- 减小锁粒度:将大对象拆分,使多个精细粒度的锁,增加并行度。
- 读写分离:使用ReadWriteLock,允许多个读操作并发进行。
- 分段锁:入ConcurrentHashMap的实现,将数据分段,每段使用独立的锁。
- 避免锁嵌套:减少锁的持有时间,避免在持有锁时调用其他可能获取锁的方法。
- 使用并发容器:如ConcurrentHashMap,CopyOnWriteArrayList等,替代同步的集合类。
- 使用原子类:入AtomicInteger,避免使用同步块进行简单的原子操作。
- JVM层面的优化(主要针对synchronized)
- 总结:
- synchronized时Java内置的锁机制,使用简单,但灵活性较差。
- Lock接口提供了更灵活的锁操作,但需要手动管理。
- JVM对synchronized进行了多层次的优化,使其在大多数情况下性能接近显式锁。
- 合理使用各种锁优化措施可以显著提高并发程序性能。
- synchronized和Lock的比较
- Java中的锁机制是兵法编程中的核心概念,主要用于控制多线程对共享资源的访问。以下是Java锁的全面理解:
框架部分
-
Spring事务的传播机制有哪些?在实际项目中如何选择合适的传播机制?(Spring事务的传播机制定义了业务方法之间的事务如何传播。Spring支持7中事务传播行为。)
- Spring事务的传播机制
- REQUIRED(默认)
- 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。
- 使用场景:适用于大多数情况,特别是当业务操作需要在一个事务中完成时。
- SUPPORTS
- 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
- 使用场景:适用于不需要事务管理但又可以在事务环境下运行的操作。
- MANDATORY
- 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- 使用场景:适用于必须在事务环境下执行的操作。
- REQUIRES_NEW
- 总是启动一个新的事务,如果当前存在事务,则将事务挂起。
- 使用场景:适用于需要独立于当前事务执行的操作,例如日志记录等。
- NOT_SUPPORTED
- 总是以非事务方式执行,如果当前存在事务,则将事务挂起。
- 使用场景:适用于不应该在事务环境下运行的操作。
- NEVER
- 总是以非事务方式执行,如果当前存在事务,则抛出异常。
- 使用场景:适用于不允许在事务环境下运行的操作。
- NESTED
- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则表现同REQUIRED。
- 使用场景:适用于需要独立事务管理但又希望在外部事务回滚时能够回滚的操作。
- REQUIRED(默认)
- 如何选择合适的传播机制(选择合适的事务传播机制需要根据业务逻辑实际需求来决定)
- 业务逻辑的独立性:如果某个业务逻辑必须独立于其他事务执行(如日志记录),则应该严泽REQUIRES_NEW。
- 业务逻辑的依赖性:如果某个业务逻辑依赖于当前事务的上下文,则应该选择REQUIRED或MANDATORY。
- 性能考虑:频繁启动新事务会增加性能开销,如果不是必须,应尽量避免。
- 异常回滚行为:如果需要细粒度控制事务的回滚,可以考虑NESTED,它允许部分回滚。
- 实践经验
- 在实际项目中,大部分业务方法使用默认的REQUIRED传播机制即可满足需求。对于需要独立事务管理的操作,可以考虑使用REQUIRES_NEW。在设计事务边界时,应充分考虑业务逻辑的整体性和独立性,避免不必要的事务嵌套,以提高应用性能和数据一致性。
- Spring事务的传播机制
-
Spring循环依赖是如何解决的?三级缓存的各自作用是什么?
- Spring循环依赖是指两个或多个bean之间互相依赖,形成一个闭环。Spring通过三级缓存巧妙解决了大部分循环依赖问题。Spring解决循环依赖的基本原理:
- Spring主要是通过提前暴露半成品对象来解决循环依赖。在bean的生命周期中,Spring会再对象实例化后、属性填充之前,将这个办成品对象放入缓存,供其他bean引用。
- 三级缓存结构:
- 一级缓存:存放完全初始化好的bean
- 二级缓存:存放原始的bean对象(尚未填充属性)
- 三级缓存:存放bean工厂对象
- 三级缓存作用:
- 一级缓存
- 作用:存储完全初始化好的bean实例
- 特点:这里的bean已经完成了所有初始化步骤,可以直接使用
- 二级缓存
- 作用:存储原始的bean对象,用于解决循环依赖
- 特点:存储的是尚未完全初始化的bean实例
- 三级缓存
- 作用:存储ObjectFactory,主要用于处理AOP代码
- 特点:存储的是生产bean的工厂,可以在需要时生成代理对象
- 一级缓存
- 解决循环依赖的过程
- 创建beanA,实例化后放入三级缓存
- 填充beanA的属性,发现需要beanB
- 创建beanB,实例化后放入三级缓存
- 填充beanB的属性,发现需要beanA
- 从三级缓存中获取beanA的工厂,创建beanA的早期引用
- 将beanA的早期引用放入二级缓存,删除三级缓存中的条目
- 完成beanB的初始化,放入一级缓存
- 继续完成beanA的初始化,放入一级缓存
- 为什么需要三级缓存
- 一级缓存:存储完全初始化的bean,是最终使用的缓存
- 二级缓存:存储早期引用,用于解决循环依赖
- 主要用于处理AOP代理。如果bean需要被代理,可以通过ObjectFactory创建代理对象
- 三级缓存的设计允许Spring在bean初始化过程中灵活处理代理创建,同时解决循环依赖问题。
- 注意事项
- 构造器注入的循环依赖无法解决,因为实例化对象时就需要依赖
- 圆形作用域的bean无法解决循环依赖
- 使用@Async等导致的循环依赖可能无法解决
- Spring循环依赖是指两个或多个bean之间互相依赖,形成一个闭环。Spring通过三级缓存巧妙解决了大部分循环依赖问题。Spring解决循环依赖的基本原理:
- Mybatis的一级缓存和二级缓存的区别?在实际项目中如何合理使用缓存?
- 作用范围
- 一级缓存:SqlSession级别,同一个SqlSession中查询结果会被缓存
- 二级缓存:Mapper级别,可以跨SqlSession使用
- 生命周期
- 一级缓存:与SqlSession生命周期相同,SqlSession关闭后缓存失效
- 二级缓存:与应用生命周期,可以跨SqlSession存在
- 启用方式
- 一级缓存:默认开启,无法关闭
- 需要手动配置开启
- 缓存策略
- 一级缓存:采用PerpetualCache,HashMap存储
- 二级缓存:可配置多种缓存策略
- 数据一致性
- 一级缓存:在同一SqlSession中可以保证数据一致性
- 二级缓存:跨SqlSession可能产生脏读
- 在实际项目中合理使用缓存的建议
- 合理使用一级缓存
- 对于单次请求中多次查询同一数据的场景,可以利用一级缓存提高性能
- 注意在更新操作后及时清理缓存,避免数据不一致
- 谨慎使用二级缓存
- 在读多写少的场景下使用二级缓存
- 对于频繁更新的数据,避免使用二级缓存
- 配置合适的缓存策略和刷新间隔
- 使用自定义缓存
- 对于复杂的缓存需求,考虑使用Redis等外部缓存系统
- 实现自定义的Cache接口,集成到Mybatis中
- 缓存更新策略
- 采用更新数据库和缓存的双写一执行策略
- 在更新操作后,主动清理相关缓存
- 监控和优化
- 监控缓存命中率,根据实际情况调整缓存策略
- 对于热点数据,考虑使用本地缓存+分布式缓存的多级缓存架构
- 合理使用一级缓存
- 通过合理使用Mybatis的缓存机制,结合业务特点和性能需求,可以显著提高应用的查询性能和响应速度。同时,要注意缓存带来的数据一致性问题,采取适当的措施确保数据的准确性。
- 作用范围
-
SpringBoot自动装配的原理是什么?如何实现一个自定义的starter?
- SpringBoot自动装配原理
- @SpringBootApplication注解:SpringBoot自动装配从@SpringBootApplication注解开始。这个注解包含了@EnableAutoConfiguration注解。
- @EnableAutoConfiguration:这个注解导入了AutoConfigurationImportSelector类,这个类会扫描所有包含META-INF/spring.factories文件的jar包。
- spring.factories文件:在spring-boot-autoconfigure.jar中的META-INF/spring.factories文件中定义了大量的配置类。
- 条件注解:这些配置类使用了@Conditional等条件注解,在满足特定条件时才会被创建。
- 自动配置生效
- SpringBoot启动时会加载这些配置类,实现自动装配
- 实现自定义starter
- 创建starter项目:创建maven项目。
- 添加依赖spring-boot-starter、spring-boot-autoconfigure
- 创建配置类
- 创建Properties类
- 创建META-INF/spring.factories文件
- 打包发布
- 在其他SpringBoot项目中添加你的starter依赖即可使用。
- SpringBoot自动装配原理