目录
- 1.ArrayList和LinkedList的区别
- 2.两个各自装啥数据合适
- 3.final和finally的区别
- 4.catch里面有个return,finally执行不执行
- 5.线程的创建方式
- 6.ThreadLocal
- 7.序列化
- 8.抽象类和接口的区别
- 9.数据库的四大特性
- 10.事务的一致性是啥
- 11.事务的隔离级别
- 12.可重复读是个啥
- 13.数据库索引有哪些
- 14.聚簇索引和非聚簇索引的区别
- 15.主键索引是不是聚簇索引
- 16.索引失效的情况
- 17.Spring依赖倒置
- 18.Spring循环依赖
- 19.AOP
- 20.AOP的应用场景
- 21.动态代理
- 22.Redis缓存雪崩
- 23.雪崩的处理办法
- 24.乐观锁和悲观锁
- 25.乐观锁的实现方式
1.ArrayList和LinkedList的区别
ArrayList
- 基于数组,需要连续内存
- 随机访问快(指根据下标访问)
- 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
- 可以利用 cpu 缓存,局部性原理
LinkedList
- 基于双向链表,无需连续内存
- 随机访问慢(要沿着链表遍历)
- 头尾插入删除性能高
- 占用内存多
2.两个各自装啥数据合适
如果您需要频繁地访问它和修改元素,那么ArrayList可能是更好的选择。如果您需要频繁地插入或删除元素,那么LinkedList们都可以用来存储和操作数据。可能是更好的选择。
3.final和finally的区别
final
final 用于修饰变量、方法和类。
final 变量:被修饰的变量不可变,不可变分为引用不可变和对象不可变,final 指的是引用不可变,final 修饰的变量必须初始化,通常称被修饰的变量为常量。
final 方法:被修饰的方法不允许任何子类重写,子类可以使用该方法。
final 类:被修饰的类不能被继承,所有方法不能被重写。
finally
finally 作为异常处理的一部分,它只能在 try/catch 语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,System.exit (0) 可以阻断 finally 执行。
4.catch里面有个return,finally执行不执行
执行
会先执行finally的内容,然后在执行catch中的return
如果finally中有return,那么catch中的return不会执行
5.线程的创建方式
- Thread
- Runable
- Callable
- 线程池
6.ThreadLocal
ThreadLocal的实现原理是每一个Thread维护一个ThreadLocalMap映射表,映射表的key是ThreadLocal实例,并且使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。下面用一张图展示这些对象之间的引用关系,实心箭头表示强引用,空心箭头表示弱引用。
https://donglin.blog.csdn.net/article/details/126905449
使用ThreadLocal有什么问题吗?如何解决?
内存泄漏问题
当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。
https://donglin.blog.csdn.net/article/details/126905449
7.序列化
序列化是指将 Java 对象转换为字节序列的过程
反序列化则是将字节序列转换为 Java 对象的过程。
Java序列化有以下几个优点,这也是为什么我们要使用Java序列化的原因:
-
数据持久化:Java序列化可以将对象的状态保存到字节流中,以便在需要时重新创建对象。这使得我们可以将对象保存到磁盘上,以便在程序重新启动时恢复对象的状态。
-
网络传输:Java序列化可以将对象序列化为字节流,以便在网络上传输。这使得我们可以在不同的计算机之间传输对象,而不需要手动将对象的状态转换为字节流。
-
分布式计算:Java序列化可以将对象序列化为字节流,以便在分布式计算中传输对象。这使得我们可以在不同的计算机之间传输对象,以便进行分布式计算。
-
缓存:Java序列化可以将对象序列化为字节流,以便在缓存中存储对象。这使得我们可以将对象保存到缓存中,以便在需要时快速访问对象。
总的来说,Java序列化是一种将Java对象转换为字节序列的过程,它可以将对象的状态保存到字节流中,以便在需要时重新创建对象。Java序列化可以用于数据持久化、网络传输、分布式计算和缓存等场景,使得我们可以更方便地处理对象的状态。
8.抽象类和接口的区别
- 抽象类可以有方法实现,而接口的方法中只能是抽象方法(Java 8 之后接口方法可以有默认实现);
- 抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型;
- 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法(Java 8之后接口可以有静态方法);
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
9.数据库的四大特性
ACID
原子性 一致性 隔离性 持久性
10.事务的一致性是啥
根据定义,一致性是指事务执行前后,数据从一个 合法性状态 变换到另外一个 合法性状态 。这种状态是 语义上 的而不是语法上的,跟具体的业务有关。
11.事务的隔离级别
12.可重复读是个啥
可重复读,是 MySQL 的默认事务隔离级别,它能确保同一事务多次查询的结果一致。但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读 (Phantom Read)。
13.数据库索引有哪些
普通索引: 用表中的普通列构建的索引,没有任何限制;
唯一索引: 唯一索引列的值必须唯一,但允许有空值。如果是联合索引,则列值的联合必须唯一;
主键索引: 是一种特殊的唯一索引,根据主键建立索引,不允许重复,不允许空值;
全文索引: 通过过建立倒排索引,快速匹配文档的方式。MySQL 5.7.6 之前仅支持英文,MySQL 5.7.6 之后支持中文;
组合索引: 又叫联合索引。用多个列组合构建的索引,这多个列中的值不允许有空值。可以在创建表的时候指定,也可以修改表结构。
14.聚簇索引和非聚簇索引的区别
聚簇索引:叶子节点中存储了完整的用户记录,就是聚簇索引。
非聚簇索引:叶子节点中只存储了当前索引字段和主键ID,这样的存储结构就是非聚簇索引。
15.主键索引是不是聚簇索引
主键索引和聚簇索引是两个不同的概念。
- 主键索引是一种特殊的索引,它是用于唯一标识表中每一行数据的索引。主键索引可以是聚簇索引,也可以是非聚簇索引。如果主键索引是聚簇索引,那么数据行的物理顺序就是主键索引的顺序;如果主键索引是非聚簇索引,那么数据行的物理顺序就是随机的。
- 聚簇索引是一种特殊的索引,它是按照表中数据行的物理顺序来组织数据的索引。聚簇索引只能有一个,它是表的主键索引或者是唯一索引。如果表没有主键或唯一索引,那么InnoDB存储引擎会自动创建一个隐藏的聚簇索引。
因此,主键索引可以是聚簇索引,也可以非聚簇索引;而聚簇索引只能是主键索引或唯一索引。
拓展
有人会疑虑似乎聚簇索引一定会是主键,那如果数据表不建立主键的话是否就没有聚簇索引了?
在 InnoDB 中,聚集索引不一定是主键,但是主键一定是聚集索引:原因是如果没有定义主键,聚集索引可能是第一个不允许为 null 的唯一索引,如果也没有这样的唯一索引,InnoDB 会选择内置 6 字节长的 ROWID 作为隐含的聚集索引。
InnoDB 的数据是按照主键顺序存放的,而聚集索引就是按照每张表的主键构造一颗 B+ 树,它的叶子节点存放的是整行数据。
每张 InnoDB 表都有一个聚集索引,但是不一定有主键。
16.索引失效的情况
1.最佳左前缀法则
结论:过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。
2.计算、函数导致索引失效
3.范围条件右边的列索引失效
4.不等于(!= 或者<>)索引失效
5.is not null无法使用索引,is null可使用索引
6.like以通配符%开头索引失效
17.Spring依赖倒置
Spring框架中的依赖倒置原则是指,高层模块不应该依赖于底层模块,而是应该依赖于抽象接口。同时,抽象接口也不应该依赖于具体实现,而是应该由具体实现来依赖于抽象接口。
在Spring框架中,依赖倒置原则的实现主要是通过控制反转(IoC)和依赖注入(DI)来实现的。控制反转是指将对象的创建和依赖关系的管理交给Spring容器来完成,而不是由程序员手动创建和管理对象。依赖注入是指将对象所依赖的其他对象通过构造函数、属性或方法参数的方式注入到对象中,从而实现对象之间的解耦。
通过依赖倒置原则和控制反转、依赖注入的实现,Spring框架可以实现高层模块和底层模块之间的解耦,从而提高代码的可维护性、可扩展性和可测试性。同时,Spring框架还提供了AOP(面向切面编程)等功能,可以进一步提高代码的复用性和可维护性。
18.Spring循环依赖
循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环。
比如A依赖于B,B依赖于A循环依赖在spring中是允许存在,spring框架依据三级缓存已经解决了大部分的循环依赖
一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
二级缓存:缓存早期的bean对象(生命周期还没走完)
三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的
19.AOP
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在将横切关注点(cross-cutting concerns)从业务逻辑中分离出来。横切关注点是在多个模块或功能中重复出现的代码,如日志记录、事务管理、权限控制等。通过使用AOP,开发者可以将这些横切关注点与核心业务逻辑分离,提高代码的可维护性和可重用性。
在Spring框架中,AOP的实现主要依赖于动态代理技术。以下是Spring AOP的主要原理和组成部分:
-
切面(Aspect):切面是封装横切关注点的模块。一个切面可以包含多个通知(Advice)和切入点(Pointcut)的定义。切面可以使用Java类和注解(如@Aspect)来实现。
-
通知(Advice):通知是切面中的具体行为,表示在某个连接点(Joinpoint)执行的操作。通知可以有以下几种类型:
- 前置通知(Before):在连接点执行之前执行的通知。
- 后置通知(After):在连接点执行之后(无论成功还是异常)执行的通知。
- 返回通知(AfterReturning):在连接点正常返回后执行的通知。
- 异常通知(AfterThrowing):在连接点抛出异常后执行的通知。
- 环绕通知(Around):在连接点执行前后都执行的通知,可以控制连接点的执行。
切入点(Pointcut):切入点定义了通知应该在哪些连接点执行。切入点使用表达式(如AspectJ表达式)来描述匹配的连接点。开发者可以根据类名、方法名、参数类型等条件来定义切入点。
-
连接点(Joinpoint):连接点表示在程序执行过程中的某个点,如方法调用、异常抛出等。通知会在与切入点匹配的连接点处执行。
-
代理(Proxy):在Spring AOP中,通常使用动态代理技术(如JDK动态代理或CGLIB代理)来实现AOP。代理对象是目标对象的一个封装,它可以在目标方法执行前后插入通知的逻辑。当调用代理对象的方法时,实际上会先执行通知,然后执行目标方法。
以下是Spring AOP的基本过程:
-
定义切面:开发者需要定义一个切面,包括通知和切入点的定义。切面可以使用Java类和注解来实现。
-
配置AOP:在Spring配置文件或使用注解的方式配置AOP,包括切面、代理策略等。配置告诉Spring框架如何创建代理对象以及如何将通知应用到目标对象。
-
创建代理对象:在应用程序启动时,Spring AOP会根据配置创建代理对象。代理对象是目标对象的一个封装,它可以在目标方法执行前后插入通知的逻辑。根据配置,Spring AOP可以使用JDK动态代理或CGLIB代理来创建代理对象。
-
执行通知:当调用代理对象的方法时,代理对象会先根据切入点匹配相应的通知。根据通知类型(前置、后置、返回、异常、环绕),代理对象会在目标方法执行前后插入通知的逻辑。
-
调用目标方法:在通知执行完毕后,代理对象会调用目标方法。如果是环绕通知,通知中可以控制目标方法的执行时机。
通过这个过程,Spring AOP实现了将横切关注点从业务逻辑中分离出来,提高了代码的可维护性和可重用性。同时,由于AOP使用了动态代理技术,开发者无需修改目标对象的代码,就可以轻松地应用和修改横切关注点。
需要注意的是,尽管Spring AOP提供了强大的功能,但它并不适用于所有场景。在某些情况下,使用AOP可能导致性能下降或代码复杂性增加。因此,在使用AOP时,应充分考虑其优缺点,以确保代码的可维护性和可读性。
20.AOP的应用场景
记录操作日志,缓存,spring实现的事务
核心是:使用aop中的环绕通知+切点表达式(找到要记录日志的方法),通过环绕通知的参数获取请求方法的参数(类、方法、注解、请求方式等),获取到这些参数以后,保存到数据库
21.动态代理
JDK动态代理:
JDK动态代理是Java官方提供的代理实现方式,主要依赖于java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。JDK动态代理要求目标对象必须实现一个或多个接口。
实现步骤:
- 创建一个实现InvocationHandler接口的类,重写invoke方法。这个方法将在代理对象调用目标方法时被执行,包含代理逻辑。
- 使用Proxy类的newProxyInstance方法创建代理对象。这个方法接收三个参数:目标对象的类加载器、目标对象实现的接口列表和InvocationHandler实现类的实例。
- 调用代理对象的方法。当代理对象的方法被调用时,InvocationHandler的invoke方法会被执行,插入代理逻辑。
优点: JDK动态代理是官方提供的代理实现,无需引入额外的依赖
缺点: JDK动态代理要求目标对象必须实现一个或多个接口。如果目标对象没有实现接口,无法使用JDK动态代理。此外,JDK动态代理只能为接口创建代理对象,不能针对类进行代理。
CGLIB代理:
CGLIB(Code Generation Library)是一个第三方库,可以在运行时动态生成代理对象。CGLIB代理基于继承机制,要求目标对象不能是final类。
实现步骤:
- 创建一个实现MethodInterceptor接口的类,重写intercept方法。这个方法将在代理对象调用目标方法时被执行,包含代理逻辑。
- 使用CGLIB的Enhancer类创建代理对象。需要设置目标对象的类、回调方法(MethodInterceptor实现类的实例)等属性。
- 调用代理对象的方法。当代理对象的方法被调用时,MethodInterceptor的intercept方法会被执行,插入代理逻辑。
优点: CGLIB代理可以针对没有实现接口的类进行代理,功能更加强大。
缺点: CGLIB代理需要引入额外的依赖(cglib.jar),而且性能相对较低。另外,CGLIB代理不能针对final类进行代理,因为它使用继承机制实现代理。
总结:
动态代理在Java中主要有两种实现方式:JDK动态代理和CGLIB代理。JDK动态代理是官方提供的代理实现,性能较好,但要求目标对象实现接口。CGLIB代理是第三方库实现的代理,可以针对没有实现接口的类进行代理,功能更强大,但性能较低。在选择动态代理实现时,需要根据具体场景和需求进行权衡。
22.Redis缓存雪崩
Redis缓存雪崩是指在某个时间段内,缓存中的大量数据同时失效或者被清空,导致大量请求直接打到数据库上,从而导致数据库瞬时压力过大,甚至宕机的情况。
23.雪崩的处理办法
- 设置缓存过期时间时,可以采用随机时间,避免缓存同时失效。
- 使用Redis集群,将缓存数据分散到多个节点上,避免单点故障。
- 在缓存层增加限流措施,避免瞬时大量请求打到数据库上。
- 在应用层增加熔断机制,当缓存失效或者异常时,可以快速切换到备用方案,避免对数据库造成过大压力。
- 定期对缓存进行预热,避免缓存失效后,大量请求同时打到数据库上。(就是系统上线后,将相关的缓存数据直接加载到缓存系统。 这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!)
24.乐观锁和悲观锁
-
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。 -
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
乐观锁 适合 读操作多 的场景,相对来说写的操作比较少。它的优点在于 程序实现 , 不存在死锁
问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
悲观锁 适合 写操作多 的场景,因为写的操作具有 排它性 。采用悲观锁的方式,可以在数据库层
面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突。
25.乐观锁的实现方式
版本号机制
一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时, version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
CAS
CAS(Compare and Swap)比较并交换,是一种乐观锁的实现,是用非阻塞算法来代替锁定,其中 java.util.concurrent 包下的 AtomicInteger 就是借助 CAS 来实现的。
- 原理
CAS (CompareAndSwap)
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。
当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来
- CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。 - 解决方案
Java 提供了一个 AtomicStampedReference 原子引用变量