Java 语言的特点
面向对象、平台无关性、支持多线程、编译与解释共存
面向对象:封装、继承、多态
平台无关性:一次编译,到处运行
支持多线程:C++语言没有内置多线程,因此必须调用操作系统的多线程功能来进行多线程设计。而 Java 提供了多线程支持
JDK JRE JVM
JDK:Java 开发工具包
JRE:Java 运行环境
JVM:Java 虚拟机
跨平台性及其原理
所谓跨平台性是指 Java 语言编写的程序 一次编译后可以在多个系统平台上运行
实现原理:Java 程序是通过 Java 虚拟机在平台上运行,只要该系统安装 Java 虚拟机就可以运行 Java 程序
字节码及字节码的好处
字节码就是 Java 程序经过编译之后的.class 文件,字节码能被虚拟机识别从而实现跨平台性
只需要把 Java 程序编译成 Java 虚拟机能识别的 Java 字节码,不同平台安装对应的 Java 虚拟机,就可以实现 Java 语言的平台无关性
Java 程序编译与解释并存
编译型是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码,解释型是指解释器对源程序逐行解释成特定平台的机器码并立即执行
Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 语言要经过先编译、后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码文件,这种字节码必须再经过 JVM,解释成操作系统能识别的机器码,再由操作系统执行。因此可以认为 Java 语言编译与解释并存
值传递和引用传递的区别
值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了
引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的是引用的地址,也就是变量所对应的内存空间的地址。也就是说传递前和传递后都指向同一个引用(同一个内存空间)
Java 数据类型
基本数据类型:布尔型、数值型(整型 浮点型 字节型)、字符型
引用数据类型:类、接口、数组
面向对象和面向过程的区别
面向过程:面向过程是分析出解决问题的步骤,然后用函数把步骤一个一个实现,使用的时候再一个一个调用就可以
面向对象:把构成问题的事务分解成各个对象,而创建对象的目的不是为了完成一个个步骤,而是为了描述某个事件在解决整个问题的过程中发生的行为。目的是为了写出通用代码,加强代码的重用,屏蔽差异性
Java获取对象的方法
- new 创建新对象
- 通过反射机制
- 采用 clone 机制
- 通过序列化机制
反射
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制
反射的优缺点
优点:
能够运行时动态获取类的实例,提高灵活性;可与动态编译结合
缺点:
使用反射性能较低,需要解析字节码,将内存中的对象进行解析。其解决方案是:通过 setAccessible(true),关闭JDK的安全检查来提升反射速度;多次创建一个类的实例时,有缓存会快很多;ReflflectASM工具类,通过字节码生成的方式加快反射速度
反射中获取Class对象
知道具体类的情况下可以使用.class
Class alunbarClass = TargetObject.class;
通过 Class.forName()传入类的全路径获取
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
通过对象实例instance.getClass()获取
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
通过类加载器xxxClassLoader.loadClass()传入类路径获取
ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");
接口和抽象类的区别
接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系
接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值。而抽象类的成员变量默认 default 可在子类中被重新定义 也可被重新赋值
接口方法默认修饰符是 public,抽象类方法可以有 public、private、default
集合类的优劣
ArrayList:ArrayList 可以看作一个动态数组,可以在运行时动态扩容。优点是访问速度快,可以通过索引直接查到元素。缺点是插入和删除元素可能需要移动元素,效率低
LinkedList:LinkedList 是一个双向链表,适合频繁的插入和删除操作。优点是插入和删除元素的时候只需要改变节点前后的指针,缺点是访问元素时需要遍历链表
HashMap:HashMap 是一个基于哈希表的键值对集合。优点是插入删除和查找元素的速度都很快,缺点是不保留键值对的插入顺序
LinkedHashMap:LinkedHashMap 在 HashMap 的基础上增加了一个双向链表来保持键值对的插入顺序
队列和栈的区别
队列是一种先进先出的数据结构。在队列中,第一个加入队列的元素会是第一个移除的。队列常用于处理按顺序来的任务
栈是一种后进先出的数据结构。在这种结构中,最后一个加入栈的元素会是第一个被移除的,这种特性使得栈非常适合于那些需要访问最新添加的元素的场合
ArrayList 的扩容机制
ArrayList 是基于数组的集合。数组的容量是在定义的时候确定的,如果数组满了再插入就会数组溢出。所以在插入时候会先检查是否需要扩容,如果当前容量+1 超过数组长度会进行扩容。ArrayList 的扩容是创建一个 1.5 倍的新数组,然后把原数组的值拷贝过去
HashMap 的 hash 函数
HashMap 的哈希函数是先拿到 key 的 hashcode,是一个 32 位的 int 类型的数值,然后让 hashcode 的高 16 位和低 16 位进行异或操作。这么设计是为了降低哈希碰撞的概率
HashMap 的容量是 2 的倍数
为了快速定位元素的下标
初始化 HashMap 传一个 17 容量的处理
HashMap 会将这个值转换为大于或等于 17 的最小的 2 的幂,因为 HashMap 的设计是基于哈希表的,而哈希表的大小最好是 2 的幂。这样可以优化哈希值的计算并减少哈希冲突,所以如果传入 17 作为初始容量 HashMap 实际上会被初始化大小为 32 的哈希表
HashMap 链表转红黑树的阈值为 8
选择 8 作为阈值,可以保证在大多数情况下保持链表的高效性,同时又能够有效的将较长的链表转换为红黑树避免链表过长导致的性能下降
扩容因子为 0.75
这个值可以保证空间利用率较高的同时,减少哈希冲突的概率,提高查找插入删除等操作的性能
TreeMap 和 HashMap 的区别
HashMap 是基于数组+链表+红黑树实现的。put 元素的时候会先计算 key 的哈希值,然后通过哈希值计算出数组的索引,然后将元素插入到数组中。如果发生哈希冲突,会使用链表来解决。如果链表长度大于 8(且数组的长度大于 64)会转换为红黑树
get 元素的时候同样会先计算 key 的哈希值,然后通过哈希值计算出数组的索引,如果遇到链表或红黑树,会通过 key 的 equals 方法来判断是否是要找的元素
TreeMap 是基于红黑树实现的 put 元素的时候会先判断根节点是否为空,如果为空直接插入到根节点,如果不为空为通过 key 的比较器来判断元素应该插入到左子树还是右子树
get 元素的时候会通过 key 的比较器来判断元素的位置,然后递归查找
sleep 和 wait 的区别
sleep()和 wait()是 Java 中用于暂停当前线程的两个方法 sleep 是让当前线程休眠,不涉及对象类,也不需要获取对象的锁,属于 Thread 类的方法。wait 是让获取对象锁的线程实现等待,属于 Object 类的方法
所属类不同:
sleep()方法属于 Thread 类
wait()方法属于 Object 类
锁行为不同:
当线程执行 sleep 方法时他不会释放任何锁,也就是说在一个线程在持有某个对象的锁时调用了 sleep 他在睡眠期间仍然会持有这个锁
当线程执行 wait 方法时他会释放他持有的那个对象的锁,其他线程可以有机会获取该对象的锁
使用条件不同:
sleep()方法可以在任何地方被调用
wait()方法必须在同步代码块或同步方法中被调用,因为调用 wait()方法的前提是当前线程必须持有对象的锁,否则会抛出异常
唤醒方法不同:
sleep()方法在指定的时间过后线程会自动唤醒继续执行
wait()方法需要依靠 notify() notifyAll()方法或者 wait()方法中指定的等待时间到期来唤醒线程
进程和线程
进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发
线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发
线程的六种状态
初始、运行、阻塞、等待、超时等待、终止
初始状态:线程被创建,但还没有调用 start()方法
运行状态:Java 线程将操作系统中的就绪和运行两种状态笼统的称作“运行”
阻塞状态:表示线程阻塞于锁
等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
超时等待状态:该状态不同于 WAITIND,它是可以在指定的时间自行返回的
终止状态:表示当前线程已经执行完毕
线程上下文切换
为了让用户感觉多个线程是同时执行的,CPU 资源的分配采用了时间片轮换也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换
线程池的优点
频繁地创建和销毁线程会消耗系统资源,线程池能够复用已创建的线程
提高响应速度,当任务到达时,任务可以不需要等待线程创建就立即执行
线程池支持定时执行、周期性执行、单线程执行和并发数控制等功能
线程池的工作流程
当应用程序提交一个任务时,线程池会根据当前线程的状态和参数决定如何处理这个任务
- 如果线程池中的核心线程都在忙,并且线程池未达到最大线程数,新提交的任务会被放入队列中进行等待
- 如果任务队列已满,且当前线程数量小于最大线程数,线程池会创建新的线程来处理任务
空闲的线程会从任务队列中取出任务来执行,当任务执行完毕后,线程并不会立即销毁,而是继续保持在池中等待下一个任务
当线程空闲时间超出指定时间,且当前线程数量大于核心线程数时,线程会被回收
线程池的主要参数
核心线程(corePoolSize):定义了线程池中的核心线程数量。即使这些线程处于空闲状态,它们也不会被回收。这是线程池保持在等待状态下的线程数
最大线程数(maximumPoolSize):线程池允许的最大线程数量。当工作队列满了之后,线程池会创建新线程来处理任务,直到线程数达到这个最大值
非核心线程存活时间(keepAliveTime):非核心线程的空闲存活时间。如果线程池中的线程数量超过了 corePoolSize,那么这些多余的线程在空闲时间超过 keepAliveTime 时会被终止
非核心线程存活时间单位(unit):keepAliveTime 参数的时间单位
等待队列(workQueue):用于存放待处理任务的阻塞队列。当所有核心线程都忙时,新任务会被放在这个队列里等待执行
创建线程使用工厂(threadFactory):一个创建新线程的工厂。它用于创建线程池中的线程。可以通过自定义 ThreadFactory 来给线程池中的线程设置有意义的名字,或设置优先级等
饱和拒绝策略(handler):拒绝策略 RejectedExecutionHandler,定义了当线程池和工作队列都满了之后对新提交的任务的处理策略。常见的拒绝策略包括抛出异常、直接丢弃、丢弃队列中最老的任务、由提交任务的线程来直接执行任务等
线程池的拒绝策略
AbortPolicy:这是默认的拒绝策略。该策略会抛出一个 RejectedExecutionException 异常
CallerRunsPolicy:该策略不会抛出异常,而是会让提交任务的线程(即调用 execute 方法的线程)自己来执行这个任务
DiscardOldestPolicy:策略会丢弃队列中最老的一个任务(即队列中等待最久的任务),然后尝试重新提交被拒绝的任务
DiscardPolicy:策略会默默地丢弃被拒绝的任务,不做任何处理也不抛出异常
线程池的工作队列
数组实现有界队列(ArrayBlockingQueue):是一个用数组实现的有界阻塞队列,按 FIFO 排序量
链表结构队列(LinkedBlockingQueue):是基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE,吞吐量通常要高于 ArrayBlockingQuene;newFixedThreadPool 线程池使用了这个队列
延迟执行队列(DelayQueue):是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool 线程池使用了这个队列
有优先级无界阻塞队列(PriorityBlockingQueue):是具有优先级的无界阻塞队列
无元素存储阻塞队列(SynchronousQueue):是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQuene,newCachedThreadPool 线程池使用了这个队列
线程池提交 execute 和 submit 的区别
execute()方法用于提交不需要返回值的任务
submit()方法用于提交需要返回值的任务。线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get()方法来获取返回值
线程池的关闭
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止
shutdown() 将线程池状态置为 shutdown,并不会立即停止:
- 停止接收外部 submit 的任务
- 内部正在跑的任务和队列里等待的任务,会执行完
- 等到第二步完成后,才真正停止
shutdownNow() 将线程池状态置为 stop。一般会立即停止,事实上不一定:
- 和 shutdown()一样,先停止接收外部提交的任务
- 忽略队列里等待的任务
- 尝试将正在跑的任务 interrupt 中断
- 返回未执行的任务列表
shutdown 和 shutdownnow 简单区别
shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大
shutdown()只是关闭了提交通道,用 submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池
Spring的优点
IoC 和 DI 的支持:Spring 的核心就是一个大的工厂容器,可以维护所有对象的创建和依赖关系,Spring 工厂用于生成 Bean,并且管理 Bean 的生命周期,实现高内聚低耦合的设计理念
AOP 编程的支持:Spring 提供了面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等切面功能
声明式事务的支持:支持通过配置就来完成对事务的管理,而不需要通过硬编码的方式,以前重复的一些事务提交、回滚的 JDBC 代码,都可以不用自己写了
快捷测试的支持:Spring 对 Junit 提供支持,可以通过注解快捷地测试 Spring 程序
快速集成功能:方便集成各种优秀框架,Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis、Quartz 等)的直接支持
复杂 API 模板封装:Spring 对 JavaEE 开发中非常难用的一些 API(JDBC、JavaMail、远程调用等)都提供了模板化的封装,这些封装 API 的提供使得应用难度大大降低
Spring的主要模块
Spring Core:Spring 核心,它是框架最基础的部分,提供 IoC 和依赖注入 DI 特性
Spring Context:Spring 上下文容器,它是 BeanFactory 功能加强的一个子接口
Spring Web:它提供 Web 应用开发的支持
Spring MVC:它针对 Web 应用中 MVC 思想的实现
Spring DAO:提供对 JDBC 抽象层,简化了 JDBC 编码,同时,编码更具有健壮性
Spring ORM:它支持用于流行的 ORM 框架的整合,比如:Spring + Hibernate、Spring + iBatis、Spring + JDO 的整合等
Spring AOP:即面向切面编程,它提供了与 AOP 联盟兼容的编程实现
SpringBean 定义和依赖定义的方式
直接编码方式:我们一般接触不到直接编码的方式,但其实其它的方式最终都要通过直接编码来实现
配置文件方式:通过 xml、propreties 类型的配置文件,配置相应的依赖关系,Spring 读取配置文件,完成依赖关系的注入
注解方式:注解方式应该是我们用的最多的一种方式了,在相应的地方使用注解修饰,Spring 会扫描注解,完成依赖关系的注入
SpringBean 的生命周期
实例化、属性赋值、初始化、使用中、销毁
实例化:Spring 容器根据 Bean 的定义创建 Bean 的实例,相当于执行构造方法 也就是 new 一个对象
属性赋值:相当于执行 setter 方法为字段赋值
初始化:初始化阶段允许执行自定义的逻辑,比如设置某些必要的属性值,开启资源执行预加载操作等等,以确保 Bean 在使用之前是完全配置好的
销毁:相当于执行=null 释放资源
Spring 容器启动阶段
容器启动阶段主要做的工作是加载和解析配置文件,保存到对应的 Bean 定义中
容器启动开始,首先会通过某种途径加载 Congiguration MetaData,在大部分情况下容器需要依赖某些工具类对加载的 Congiguration MetaData 进行解析和分析,并将分析后的信息组为相应的 BeanDefinition,最后把这些保存了 Bean 定义必要信息的 BeanDefinition 注册到相应的 BeanDefinitionRegistry,这样容器就启动完成
Spring 解决循环依赖
一级缓存:单例池,用于保存实例化,属性注入 初始化完成的 Bean 实例
二级缓存:早期曝光对象,用于保存实例化完成的 Bean 实例
三级缓存:早期曝光对象工厂,用于保存 Bean 床架工厂
当 AB 两个类发生循环依赖时
创建 A 实例,实例化时把 A 的对象工厂放入三级缓存中,表示 A 开始实例化了
A 注入属性时,发现依赖 B,此时 B 还没有创建出来,所以去实例化 B
同样 B 注入属性时发现依赖 A,它就从缓存里找 A 对象,依次从一级到三级缓存查询 A,发现可以从三级缓存中通过对象工厂拿到 A 把 A 放入二级缓存,同时删除三级缓存的 A,此时 B 已经实例化完成并且初始化完成 把 B 放入一级缓存
接着 A 继续属性赋值,顺利从一级缓存拿到实例化并且初始化完成的 B 对象 A 对象创建完成,删除二级缓存的 A 同时把 A 放入一级缓存
JDK 动态代理和 CGLIB 动态代理
Spring 的 AOP 是通过动态代理来实现的 ,动态代理主要有两种方式:JDK 动态代理和 CGLIB 代理
JDK 动态代理是基于接口的方式,它使用 Java 原生的 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口来创建和管理代理对象
基于 interface:JDK 动态代理要求目标对象必须实现一个或多个接口,代理对象不是直接继承自目标对象,而是实现了与目标对象相同的接口
基于 invocationhandler:在调用代理对象的任何方法时,调用都会转发到一个invoicehandler 实例的 invoke 方法,可以在这个 invoke 方法中定义拦截器,比如方法调用前后执行的操作
基于 proxy:proxy 利用 invoicehandler 动态创建一个符合目标类实现的接口实例,生成目标类的代理对象
CGLIB 是一个第三方代码生成库,他通过继承方式实现代理,不需要接口,被广泛应用于 Spring AOP 中用于提供方法拦截操作
基于继承:CGLIB 通过在运行时生成目标对象的子类来创建代理对象,并在子类中覆盖非 final 的方法
基于 ASM:ASM 是一个 Java 字节码操作和分析框架,CGLIB 可以通过 ASM 读取目标类的字节码,然后修改字节码生成新的类。它在运行时动态生成一个被代理类的子类,并在子类中覆盖父类的方法,通过方法拦截技术插入增强代码
SpringAOP 和 AspectJ AOP 区别
SpringAOP 属于运行时增强,主要特点有:
基于动态代理实现。默认如果使用接口的,用 JDK 提供的动态代理实现,如果是方法则使用 CGLIB 实现
SpringAOP 需要依赖 IOC 容器来管理,并且只能作用于 Spring 容器,使用纯 Java 代码实现
在性能上,由于 SpringAOP 是基于动态代理实现的,在容器启动时需要生成代理实例。在方法调用上也会增加栈的深度,使得 SpringAOP 性能不如 AspectJ
AspectJ 是一个易用的功能强大的 AOP 框架,属于编译时增强,可以单独使用,也可以整合到其他框架中,是 AOP 编程的完全解决方案,需要用到单独编译器 ajc
AspectJ 属于静态织入 通过修改代码来实现,在实际运行之前就完成了织入,所以它生成的类是没有额外运行时开销的 一般有如下几个织入时机:
编译期织入:如类A使用AspectJ添加了一个属性,类B引用了它,这个场景就需要编译期的时候进行织入,否则没法编译类B
编译后织入:也就是已经生成了.class文件,或已经打成jar包了,这种情况我们需要增强处理的话,就要用到编译后织入
类加载后织入:指的是在加载类的时候进行织入,要实现这个时期的织入有几种常见的方法
Spring事务的种类
在Spring中 事务管理可以分为两大类:声明式事务管理和编程式事务管理
编程式事务:编程式事务可以使用TransactionTemplate和PlatformTransactionManager来实现,需要显式执行事务,允许我们在代码中直接控制事务的边界,通过编程方式明确指定事务的开始、提交和回滚
声明式事务:声明式事务是建立在AOP之上的,其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在目标方法执行完之后根据执行情况进行提交或回滚事务
两者的区别
编程式事务:需要在代码中显式调用事务管理的API来控制事务的边界,比较灵活,但是代码侵入性较强,不够优雅
声明式事务:这种方式使用Spring的AOP来声明事务,将事务管理代码从业务代码中分离出来。 优点是代码简洁、易于维护,但缺点是不够灵活,只能在预定义的方法上使用事务
@Transactional 注解失效场景
非spring管理的bean:
如果一个类没有被spring容器管理,那么@Transactional注解不会被识别,因为事务管理是通过spring AOP实现的,需要spring代理来控制事务的边界
方法非public:
默认情况下,spring AOP代理只会拦截public方法。如果在非public方法上使@Transactional注解,该注解将不会生效
内部方法调用:
当一个对象的方法内部调用同一个对象的另一个@Transactional方法时,由于使用的是this引用,不会经过Spring代理,因此不会应用事务
异常类型不正确:
@Transactional默认只对运行时异常(RuntimeException和Error的子类)进行回滚。如果方法抛出的是检查型异常(Exception的直接子类,但不是RuntimeException),事务不会回滚,除非在注解中明确指定
事务传播行为不当:
如果@Transactional注解使用了不合适的传播行为,可能导致事务不按预期执行
数据库或JPA提供者不支持事务:
某些数据库(如某些嵌入式数据库)或JPA提供者可能不支持事务或某些类型的事务
事务管理器配置错误:
如果Spring配置中事务管理器没有正确配置,或者@Transactional注解没有指定正确的事务管理器,事务将不会被正确处理
同类中方法互相调用:
在同一个类中,一个@Transactional方法直接调用另一个@Transactional方法时,由于是通过this引用调用,不会通过代理,因此不会应用声明的事务
事务同步问题:
如果在事务方法中进行了某些异步操作,比如启动一个新的线程来执行部分逻辑,那么这部分逻辑将不会在原有的事务中执行
事务超时:
如果一个事务运行的时间超过了配置的超时时间,那么事务可能会被回滚
数据库隔离级别不当:
如果事务的隔离级别设置不当,可能会导致事务失效。例如,某些隔离级别下,可能无法看到其他事务所作的修改
未管理的事务状态:
如果在事务方法中直接管理事务状态,比如手动提交或回滚,这可能会与Spring的事务管理冲突,导致不可预料的结果
多个数据源:
如果应用配置了多个数据源但没有正确指定@Transactional注解应该使用哪个事务管理器,可能会导致事务不被正确管理
注解在非业务方法上:
在非业务逻辑的方法上使用@Transactional,比如toString()、equals()或hashCode()方法,这些方法通常不会被Spring代理所拦截
配置类不被扫描:
如果Spring Boot应用的@Configuration类没有被扫描到,比如没有放在主应用类或其子包下,那么相关的事务管理配置可能不会生效
事务管理器未正确绑定到JPA:
如果使用JPA,并且@Transactional指定的事务管理器没有正确绑定到JPA EntityManager,事务可能不会正常工作
使用错误的注解:
如果不小心使用了错误的注解,比如javax.transaction.Transactional而不是org.springframework.transaction.annotation.Transactional,事务将不会按预期工作
多个事务管理器冲突:
如果配置了多个事务管理器但没有在@Transactional注解中指定使用哪一个,Spring将不确定使用哪个事务管理器,可能导致事务不生效
代理方式不正确:
如果配置了基于接口的代理(使用JDK动态代理)而实际上应该使用基于类的代理(使用CGLIB),或者反之,可能会导致事务注解不生效
@Transactional调用内部类中方法事务是否失效
当在一个类的@Transactional方法中调用该类内部类的方法时,事务是否会失效取决于内部类是如何被使用和管理的。这里有两种情况需要考虑:
非静态内部类:
如果内部类是非静态的,它将与外部类的实例相关联。这种情况下,如果外部类被Spring代理了,那么内部类的方法调用通常不会经过Spring的代理,因为它们是直接通过外部类实例的内部路径访问的。这意味着,如果内部类的方法没有显式地声明@Transactional,它们将不会在独立的事务中运行,而是参与到外部类方法的事务中
静态内部类:
如果内部类是静态的,它不依赖于外部类的实例。在这种情况下,如果静态内部类被Spring以Bean的形式管理并且正确配置了事务代理,那么@Transactional注解应该是有效的。然而,如果静态内部类没有被Spring管理,或者其方法没有被标注为@Transactional,那么这些方法将不会在事务的上下文中执行
在任何情况下,如果希望内部类的方法参与到事务中,需要确保这些方法被Spring代理,并且正确使用了@Transactional注解。如果内部类是作为独立的Bean被Spring管理的,那么可以通过依赖注入的方式注入内部类的Bean,并在其方法上使用@Transactional注解。这样,当这些方法被调用时,它们将会被Spring的事务代理所管理
Spring中应用的设计模式
工厂模式 : Spring 容器本质是一个大工厂,使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象
代理模式 : Spring AOP 功能功能就是通过代理模式来实现的,分为动态代理和静态代理
单例模式 : Spring 中的 Bean 默认都是单例的,这样有利于容器对 Bean 的管理
模板模式 : Spring 中 JdbcTemplate、RestTemplate 等以 Template 结尾的对数据库、网络等等进行操作的模板类,就使用到了模板模式
观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用
适配器模式 :Spring AOP 的增强或通知 (Advice) 使用到了适配器模式、Spring MVC 中也是用到了适配器模式适配 Controller
策略模式:Spring 中有一个 Resource 接口,它的不同实现类,会根据不同的策略去访问资源
SpringBoot的优点
通过 Intellij IDEA 或者官方的 Spring Initializr 就可以快速创建新项目,只需要选择需要的依赖就可以五分钟内搭建一个项目骨架
Spring Boot 内嵌了 Tomcat、Jetty、Undertow 等容器,不需要在服务器上部署 WAR 包了,直接运行 jar 包就可以启动项目,超级方便
Spring Boot 无需再像以前一样在 web.xml、applicationContext.xml 等配置文件里配置大量的内容,大部分初始工作 Spring Boot 都帮我们做好了。例如,如果项目中添加了 spring-boot-starter-web,Spring Boot 会自动配置 Tomcat 和 Spring MVC
Spring Boot 允许我们通过 yaml 来管理应用的配置,比传统的 properties 文件更加简洁
Spring Boot 提供了一系列的 Starter,可以快速集成常用的框架,例如 Spring Data JPA、Spring Security、MyBatis 等
Spring Boot 提供了一系列的 Actuator,可以帮助我们监控和管理应用,比如健康检查、审计、统计等
配合 Spring Cloud 可以快速构建微服务架构
CAP原则
CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性)这3个基本需求,最多只能同时满足其中的2个
分布式锁的三种实现方案
常见的分布式锁实现方案有三种:MySQL分布式锁、ZooKepper分布式锁、Redis分布式锁
MySQL分布式锁:
用数据库实现分布式锁比较简单,就是创建一张锁表,数据库对字段作唯一性约束
加锁的时候,在锁表中增加一条记录即可;释放锁的时候删除记录就行
如果有并发请求同时提交到数据库,数据库会保证只有一个请求能够得到锁
这种属于数据库 IO 操作,效率不高,而且频繁操作会增大数据库的开销,因此这种方式在高并发、高性能的场景中用的不多
ZooKeeper实现分布式锁:
ZooKeeper也是常见分布式锁实现方法
ZooKeeper的数据节点和文件目录类似,例如有一个lock节点,在此节点下建立子节点是可以保证先后顺序的,即便是两个进程同时申请新建节点,也会按照先后顺序建立两个节点
所以我们可以用此特性实现分布式锁。以某个资源为目录,然后这个目录下面的节点就是我们需要获取锁的客户端,每个服务在目录下创建节点,如果它的节点,序号在目录下最小,那么就获取到锁,否则等待。释放锁,就是删除服务创建的节点
ZK实际上是一个比较重的分布式组件,实际上应用没那么多了,所以用ZK实现分布式锁,其实相对也比较少
Redis实现分布式锁:
Redis实现分布式锁,是当前应用最广泛的分布式锁实现方式
Redis执行命令是单线程的,Redis实现分布式锁就是利用这个特性
实现分布式锁最简单的一个命令:setNx(set if not exist),如果不存在则更新
加锁了之后如果机器宕机,那我这个锁就无法释放,所以需要加入过期时间,而且过期时间需要和setNx同一个原子操作,在Redis2.8之前需要用lua脚本,但是redis2.8之后redis支持nx和ex操作是同一原子操作
一般生产中都是使用Redission客户端,非常良好地封装了分布式锁的api,而且支持RedLock
分布式事务
分布式事务是相对本地事务而言的,对于本地事务,利用数据库本身的事务机制,就可以保证事务的ACID特性。而在分布式环境下,会涉及到多个数据库。分布式事务其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性
分布式事务处理的关键是:
- 需要记录事务在任何节点所做的所有动作;
- 事务进行的所有操作要么全部提交,要么全部回滚
线程池中线程异常后销毁或复用
当执行方式是execute时,可以看到堆栈异常的输出。线程池会把这个线程移除掉,并创建一个新的线程放到线程池中
当执行方式是submit时,堆栈异常没有输出。但是调用future.get()方法时,可以捕获到异常,不会把这个线程移除掉,也不会创建新的线程放入到线程池中
以上两种执行方式都不会影响到线程池中其他线程的正常执行