在 JDK 7 和 JDK 8 中,HashMap
在处理哈希冲突和内部结构上有一些区别:
JDK 7 中的 HashMap:
JDK 8 中的 HashMap:
元素数量下降长会变回链表吗?
在 JDK 8 中的 HashMap
中,当元素数量减少时,可能会将红黑树重新转换回链表,这是为了避免维持一个过大的红黑树所带来的额外开销。
具体来说,当红黑树中节点数量降低到一定阈值以下(在 JDK 8 中是 6 个节点),就会将红黑树重新转换为链表。这种机制可以避免频繁地在元素数量波动时反复进行树化和退化,以保持数据结构在适当的大小和性能之间的平衡。
这样的转换机制是为了在元素数量变化时保持 HashMap
内部结构的合理性,并尽可能减少不必要的性能损耗。
JDK 7 中的 HashMap 多线程问题:
ConcurrentModificationException
异常或导致数据不一致。JDK 8 中的 HashMap 多线程问题:
HashMap
用 Node 数组替换了 Segment 数组。这种改变在某种程度上提高了并发性能,但在极端情况下(比如大量线程同时写入),仍可能出现线程安全问题。HashMap
在 JDK 8 中仍然不是线程安全的。在并发写入时,仍需要额外的同步措施来保证线程安全性。共同问题:
HashMap
本身不提供足够的并发性保障,需要借助 ConcurrentHashMap
或者其他并发容器来保证线程安全。建议:
ConcurrentHashMap
,它提供了更好的并发性能和线程安全保障。HashMap
在多线程下可能不会产生太大问题,但依然需要注意潜在的线程安全性问题。ConcurrentHashMap
是 Java 中用于多线程环境下的并发安全的哈希表实现。其底层原理在不同的 JDK 版本中有所不同。
JDK 7 中的 ConcurrentHashMap:
ConcurrentHashMap
在 JDK 7 中采用了分段锁的机制,将整个哈希表分成多个段(Segment),每个段都类似于一个小的 HashMap
,每个段拥有自己的锁,不同段之间的操作互不影响,提高了并发度。JDK 8+ 中的 ConcurrentHashMap:
ConcurrentHashMap
改用了 CAS(Compare and Swap)操作以及 synchronized 来实现更加精细的并发控制。引入了 Node 数组,使用 CAS 操作进行元素的插入和修改,同时在必要时使用 synchronized 进行并发控制。在 JDK 7 中,ConcurrentHashMap
的分段锁是通过 Segment
实现的。每个 Segment
是一个可重入的独立锁,类似于一个小型的 HashMap
,其中包含一个数组,这个数组的每个元素是一个链表或者红黑树,用来存储键值对。
具体来说,分段锁的实现包括以下几个关键点:
ConcurrentHashMap
内部维护了一个 Segment
数组,数组的大小默认是 16。Segment
包含一个哈希表,是一个独立的哈希表结构,内部通过数组 + 链表(或红黑树)的方式存储键值对。Segment
对应一个锁,当进行写操作(插入、修改等)时,只需要锁住对应的 Segment
,其他 Segment
不受影响,可以并发进行读操作。Segment
,而其他 Segment
不会受到影响,可以继续并发访问,减少了锁的竞争,提高了并发度和性能。这种分段锁的实现机制有效地降低了多线程并发操作时的锁竞争,提高了并发性能。每个 Segment
的锁粒度比较细,使得只有部分数据受到锁的保护,从而允许多个线程同时访问不同的 Segment
,提高了整体的并发性能。
Java 的泛型是一种参数化类型的概念,在编写通用的代码,可以在不同类型上进行操作,提高了代码的重用性、安全性和可读性。泛型的出现主要是为了解决以下问题:
1. 类型安全:
在 Java 5 之前,集合(如 ArrayList
、HashMap
等)可以存储任意对象,但是在取出对象时需要进行类型转换,如果类型转换错误,会导致运行时的异常。泛型通过提供参数化类型的方式,在编译时强制进行类型检查,从而提高了类型安全性,避免了运行时的类型错误。
2. 代码重用:
通过泛型,可以编写通用的代码逻辑,使得代码可以用于不同类型的数据,避免了重复编写类似的代码。
3. 可读性和维护性:
泛型代码更加清晰易懂,因为在声明时就能明确知道使用的数据类型,提高了代码的可读性和维护性。
泛型的实现是通过类型擦除(Type Erasure)的机制来实现的。在编译期间,泛型类型会被擦除,编译器会将泛型代码转换为非泛型的代码。泛型的类型信息在编译后被擦除掉,这也是 Java 泛型的一个限制,称为类型擦除的特性。
关于泛型的效率问题,泛型并不会导致额外的运行时开销。因为泛型在编译期间被擦除,生成的字节码和非泛型代码是一样的,没有额外的类型检查操作。在运行时,泛型并不会影响代码的性能。实际上,泛型代码可能会比非泛型代码更加高效,因为它可以减少类型转换和提供更好的类型检查,避免了一些运行时的异常。
Spring 框架通过三级缓存解决了循环依赖的问题。循环依赖指的是两个或多个 Bean 之间相互引用,形成一个循环链,在实例化过程中可能导致无限循环或者空指针异常。
Spring 解决循环依赖的过程主要分为三个阶段:
1. 实例化对象阶段:
2. 属性填充阶段:
3. 完成对象创建阶段:
这样通过三级缓存,Spring 能够在实例化和属性注入的过程中解决循环依赖的问题,确保循环依赖的 Bean 能够正确地被实例化和注入属性,避免了无限循环或者空指针异常的发生。
动态代理主要分为两种实现方式:基于接口(Interface-based)的动态代理和基于类(Class-based)的动态代理。
1. 基于接口的动态代理:
java.lang.reflect.Proxy
类实现,要求被代理类必须实现一个或多个接口。java.lang.reflect.Proxy
和 java.lang.reflect.InvocationHandler
接口,动态生成代理类,代理类实现了被代理接口,并且持有一个 InvocationHandler
对象,当调用代理类的方法时,会委托给 InvocationHandler
处理。Proxy.newProxyInstance()
创建代理对象,传入 ClassLoader
、接口列表和 InvocationHandler
实例。2. 基于类的动态代理:
区别与选择:
通常情况下,如果被代理类实现了接口,优先选择 JDK 动态代理,否则可以考虑使用 CGLIB 动态代理。
为什么一个注解就能实现了?如果有父类或者上层接口那么具体在哪?
AOP(面向切面编程)通常通过动态代理来实现。Spring AOP 使用了动态代理来在运行时创建代理对象,从而实现横切关注点的注入。
Spring AOP 的底层实现原理:
Spring AOP 主要基于 JDK 动态代理和 CGLIB 动态代理两种技术来实现代理。
注解实现 AOP 的方式:
通过注解标记需要被增强的方法或者类,例如 @Before
、@After
、@Around
等。Spring AOP 使用切点表达式(Pointcut Expression)来确定在哪些方法或类上应用切面逻辑。
对于继承或接口的处理:
注解主要是用来标识切面和切点,告诉 Spring 在哪里以及如何应用切面逻辑。在代理对象创建后,Spring AOP 将切面逻辑织入到代理对象的方法调用中,实现了横切关注点的功能。
Mysql默认的是什么级别?会出现幻读问题吗?怎么解决幻读的?
MySQL 支持四种事务隔离级别,分别是:
MySQL 默认的事务隔离级别是 可重复读(Repeatable Read)。
幻读问题:
幻读问题是指在一个事务中,由于其他事务插入了新的数据行,导致前后两次查询结果不一致的现象。它与不可重复读问题类似,但不是指同一条数据的多次读取结果不一致,而是指一个范围查询,新插入的数据导致查询结果不一致。
幻读的解决方法:
SELECT ... FOR UPDATE
,在读取数据时对数据行进行加锁,避免其他事务插入新数据。这些日志在不同的阶段记录信息,服务于不同的目的,共同确保了数据库的一致性、持久性和可恢复性。
手撕算法:
最长重复子数组问题可以通过动态规划来解决。动态规划的思路是利用数组来记录状态,以解决问题。以下是 Java 中动态规划的一种实现方式:
假设有两个数组 A
和 B
,我们可以使用一个二维数组 dp
来记录状态,其中 dp[i][j]
表示以 A[i-1]
和 B[j-1]
结尾的最长重复子数组的长度。
public int findLength(int[] A, int[] B) {
int m = A.length;
int n = B.length;
int maxLen = 0;
// 创建二维数组dp,初始值为0
int[][] dp = new int[m + 1][n + 1];
// 利用动态规划填充dp数组
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (A[i - 1] == B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
maxLen = Math.max(maxLen, dp[i][j]); // 更新最大长度
}
}
}
return maxLen;
}
这段代码中,dp[i][j]
的值由其左上方的值 dp[i-1][j-1]
决定,如果 A[i-1]
和 B[j-1]
相等,则 dp[i][j]
取 dp[i-1][j-1] + 1
,表示两个子数组的末尾元素相等,长度增加了1。
最终返回的 maxLen
即为最长重复子数组的长度。