前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >果然是快手,面试问的很深啊...

果然是快手,面试问的很深啊...

作者头像
千羽
发布2024-01-04 12:21:14
1250
发布2024-01-04 12:21:14
举报
文章被收录于专栏:程序员千羽程序员千羽
  • 1. HashMap jdk1.7/8有什么区别?元素数量下降长会变回链表吗?
  • 2. HashMap在多线程场景下使用,jdk7/8有都什么问题?问题有什么区别?
  • 3. ConcurrentHashMap怎么保证线程安全的?1.7的分段锁怎么实现的?
  • 4. Java语言的泛型是怎么实现的?为了解决什么问题而出现的?泛型的效率一定很低吗?
  • 5. Spring的循环依赖是怎么解决的?
  • 6. 动态代理分为两种,各自是怎么实现的?
  • 7. AOP的动态代理底层怎么实现的?
  • 8. Mysql的事务隔离级别?
  • 9. undolog,redolog,binlog区别
  • 算法:718. 最长重复子数组

1. HashMap jdk1.7/8有什么区别?元素数量下降长会变回链表吗?

在 JDK 7 和 JDK 8 中,HashMap 在处理哈希冲突和内部结构上有一些区别:

JDK 7 中的 HashMap:

  1. 底层结构: 使用数组和链表的组合实现。数组的每个位置是一个链表,当发生哈希冲突时,新元素会被添加到链表的末尾。
  2. 性能问题: 在特定条件下,当链表长度过长时(比如哈希冲突严重时),会导致查询性能下降,因为在链表上进行查找的时间复杂度为 O(n)。
  3. 扩容机制: 当数组容量不足时,会触发扩容,将数组容量增加一倍,并重新哈希元素进行重新分布。

JDK 8 中的 HashMap:

  1. 优化哈希冲突: 引入了红黑树(Tree)来替代链表。当链表长度达到一定阈值(默认为 8)时,会将链表转换为红黑树,提高查询效率。
  2. 树化和退化: 当元素数量减少时,会将红黑树重新转换为链表。这种动态的树化和退化机制保持了性能的平衡,避免了频繁的结构转换。
  3. 扩容机制改进: JDK 8 中的扩容机制相比 JDK 7 更加智能,能更好地处理并发情况,减少了数组重新哈希的次数。

元素数量下降长会变回链表吗?

在 JDK 8 中的 HashMap 中,当元素数量减少时,可能会将红黑树重新转换回链表,这是为了避免维持一个过大的红黑树所带来的额外开销。

具体来说,当红黑树中节点数量降低到一定阈值以下(在 JDK 8 中是 6 个节点),就会将红黑树重新转换为链表。这种机制可以避免频繁地在元素数量波动时反复进行树化和退化,以保持数据结构在适当的大小和性能之间的平衡。

这样的转换机制是为了在元素数量变化时保持 HashMap 内部结构的合理性,并尽可能减少不必要的性能损耗。

2. HashMap在多线程场景下使用,jdk7/8有都什么问题?问题有什么区别?

JDK 7 中的 HashMap 多线程问题:

  1. 不适合并发读写: 在多线程同时读取和修改时存在线程安全问题,可能导致 ConcurrentModificationException 异常或导致数据不一致。
  2. 容易出现死循环: 在扩容时,多线程同时进行插入操作可能导致链表形成环形结构,进而造成死循环。

JDK 8 中的 HashMap 多线程问题:

  1. Segment 替换为 Node 数组: JDK 8 中的 HashMap 用 Node 数组替换了 Segment 数组。这种改变在某种程度上提高了并发性能,但在极端情况下(比如大量线程同时写入),仍可能出现线程安全问题。
  2. 不保证线程安全: HashMap 在 JDK 8 中仍然不是线程安全的。在并发写入时,仍需要额外的同步措施来保证线程安全性。

共同问题:

  1. 缺乏并发性: 在多线程高并发写入的情况下,HashMap 本身不提供足够的并发性保障,需要借助 ConcurrentHashMap 或者其他并发容器来保证线程安全。
  2. 性能问题: 在高并发场景下,由于锁竞争等原因,可能导致性能下降。

建议:

  • 如果需要在多线程环境中使用 HashMap,并且需要保证线程安全,推荐使用 ConcurrentHashMap,它提供了更好的并发性能和线程安全保障。
  • 如果是只读操作的场景,HashMap 在多线程下可能不会产生太大问题,但依然需要注意潜在的线程安全性问题。

ConcurrentHashMap 底层原理

ConcurrentHashMap 是 Java 中用于多线程环境下的并发安全的哈希表实现。其底层原理在不同的 JDK 版本中有所不同。

JDK 7 中的 ConcurrentHashMap:

  • 分段锁(Segment): 使用分段锁来保证并发安全。ConcurrentHashMap 在 JDK 7 中采用了分段锁的机制,将整个哈希表分成多个段(Segment),每个段都类似于一个小的 HashMap,每个段拥有自己的锁,不同段之间的操作互不影响,提高了并发度。
  • 段内结构: 每个段内部还是由数组 + 链表的形式实现。只有在对同一个段的操作才需要加锁,不同段之间的操作是并发的,提高了并发度。

JDK 8+ 中的 ConcurrentHashMap:

  • CAS 和 Synchronized: 在 JDK 8 中,ConcurrentHashMap 改用了 CAS(Compare and Swap)操作以及 synchronized 来实现更加精细的并发控制。引入了 Node 数组,使用 CAS 操作进行元素的插入和修改,同时在必要时使用 synchronized 进行并发控制。
  • CAS 操作: 使用 CAS 操作代替了分段锁,减少了锁的竞争。并且引入了红黑树,对链表进行优化,提高了性能。

3. ConcurrentHashMap怎么保证线程安全的?1.7的分段锁怎么实现的?

在 JDK 7 中,ConcurrentHashMap 的分段锁是通过 Segment 实现的。每个 Segment 是一个可重入的独立锁,类似于一个小型的 HashMap,其中包含一个数组,这个数组的每个元素是一个链表或者红黑树,用来存储键值对。

具体来说,分段锁的实现包括以下几个关键点:

  1. Segment 数组: ConcurrentHashMap 内部维护了一个 Segment 数组,数组的大小默认是 16。
  2. Segment 结构: 每个 Segment 包含一个哈希表,是一个独立的哈希表结构,内部通过数组 + 链表(或红黑树)的方式存储键值对。
  3. 锁粒度: 每个 Segment 对应一个锁,当进行写操作(插入、修改等)时,只需要锁住对应的 Segment,其他 Segment 不受影响,可以并发进行读操作。
  4. 并发操作: 当进行并发操作时,例如添加、删除或者修改元素,只需要锁住对应的 Segment,而其他 Segment 不会受到影响,可以继续并发访问,减少了锁的竞争,提高了并发度和性能。

这种分段锁的实现机制有效地降低了多线程并发操作时的锁竞争,提高了并发性能。每个 Segment 的锁粒度比较细,使得只有部分数据受到锁的保护,从而允许多个线程同时访问不同的 Segment,提高了整体的并发性能。

4. Java语言的泛型是怎么实现的?为了解决什么问题而出现的?泛型的效率一定很低吗?

Java 的泛型是一种参数化类型的概念,在编写通用的代码,可以在不同类型上进行操作,提高了代码的重用性、安全性和可读性。泛型的出现主要是为了解决以下问题:

1. 类型安全:

在 Java 5 之前,集合(如 ArrayListHashMap 等)可以存储任意对象,但是在取出对象时需要进行类型转换,如果类型转换错误,会导致运行时的异常。泛型通过提供参数化类型的方式,在编译时强制进行类型检查,从而提高了类型安全性,避免了运行时的类型错误。

2. 代码重用:

通过泛型,可以编写通用的代码逻辑,使得代码可以用于不同类型的数据,避免了重复编写类似的代码。

3. 可读性和维护性:

泛型代码更加清晰易懂,因为在声明时就能明确知道使用的数据类型,提高了代码的可读性和维护性。

泛型的实现是通过类型擦除(Type Erasure)的机制来实现的。在编译期间,泛型类型会被擦除,编译器会将泛型代码转换为非泛型的代码。泛型的类型信息在编译后被擦除掉,这也是 Java 泛型的一个限制,称为类型擦除的特性。

关于泛型的效率问题,泛型并不会导致额外的运行时开销。因为泛型在编译期间被擦除,生成的字节码和非泛型代码是一样的,没有额外的类型检查操作。在运行时,泛型并不会影响代码的性能。实际上,泛型代码可能会比非泛型代码更加高效,因为它可以减少类型转换和提供更好的类型检查,避免了一些运行时的异常。

5. Spring的循环依赖是怎么解决的?

Spring 框架通过三级缓存解决了循环依赖的问题。循环依赖指的是两个或多个 Bean 之间相互引用,形成一个循环链,在实例化过程中可能导致无限循环或者空指针异常。

Spring 解决循环依赖的过程主要分为三个阶段:

1. 实例化对象阶段:

  1. 首次创建对象: 当 Spring 实例化 Bean 时,首先会初始化 Bean 的对象,但是并不会立即填充属性。
  2. 缓存对象: 在实例化过程中,Spring 会将正在创建的 Bean 放入第一级缓存中。

2. 属性填充阶段:

  1. 填充属性: 在对象实例化完成后,Spring 会开始填充属性。如果需要注入的属性是一个代理对象(例如 AOP、事务等),此时会先将未完成填充的对象暂时放入第二级缓存中,然后继续创建其他 Bean。
  2. 解决循环依赖: 当容器发现循环依赖时,会尝试解决它。如果发现循环依赖,Spring 会提前暴露一个尚未填充属性的对象引用,让另一个 Bean 可以引用到这个对象引用。

3. 完成对象创建阶段:

  1. 填充属性完成: 等到所有 Bean 都完成实例化,并且属性已经填充完毕后,Spring 会从第二级缓存中取出对象,执行属性注入。
  2. 清理缓存: 最后,清理缓存,解除循环依赖的标记。

这样通过三级缓存,Spring 能够在实例化和属性注入的过程中解决循环依赖的问题,确保循环依赖的 Bean 能够正确地被实例化和注入属性,避免了无限循环或者空指针异常的发生。

6. 动态代理分为两种,各自是怎么实现的?

动态代理主要分为两种实现方式:基于接口(Interface-based)的动态代理和基于类(Class-based)的动态代理。

1. 基于接口的动态代理:

  • JDK 动态代理: 基于 java.lang.reflect.Proxy 类实现,要求被代理类必须实现一个或多个接口。
    • 实现原理: 使用 java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler 接口,动态生成代理类,代理类实现了被代理接口,并且持有一个 InvocationHandler 对象,当调用代理类的方法时,会委托给 InvocationHandler 处理。
    • 流程: 通过 Proxy.newProxyInstance() 创建代理对象,传入 ClassLoader、接口列表和 InvocationHandler 实例。
    • 限制: 只能代理实现了接口的类,无法代理没有实现接口的类。

2. 基于类的动态代理:

  • CGLIB(Code Generation Library)动态代理: 不要求被代理类实现接口,通过继承被代理类来生成代理对象。
    • 实现原理: 使用字节码技术,底层通过修改字节码生成被代理类的子类,并重写方法实现代理逻辑。
    • 流程: CGLIB 通过继承被代理类并覆盖其中的方法来实现代理。生成的代理类是被代理类的子类,因此可以直接调用父类的方法。
    • 限制: 对于被 final 修饰的类或方法无法进行代理。

区别与选择:

  • JDK 动态代理:
    • 适用于接口代理,生成的代理类是接口的实现。
    • 使用较为方便,无需引入额外的依赖。
    • 对于需要代理接口的场景,可以选择 JDK 动态代理。
  • CGLIB 动态代理:
    • 适用于类代理,生成的代理类是被代理类的子类。
    • 对于无法修改被代理类的情况或者不想强制实现接口的情况,可以选择 CGLIB 动态代理。

通常情况下,如果被代理类实现了接口,优先选择 JDK 动态代理,否则可以考虑使用 CGLIB 动态代理。

7. AOP的动态代理底层怎么实现的?

为什么一个注解就能实现了?如果有父类或者上层接口那么具体在哪?

AOP(面向切面编程)通常通过动态代理来实现。Spring AOP 使用了动态代理来在运行时创建代理对象,从而实现横切关注点的注入。

Spring AOP 的底层实现原理:

Spring AOP 主要基于 JDK 动态代理和 CGLIB 动态代理两种技术来实现代理。

  1. JDK 动态代理: 对于实现了接口的类,Spring AOP 会使用 JDK 动态代理。它会基于接口创建代理对象,并在运行时通过代理对象拦截方法的调用,将横切逻辑织入到目标方法前后。
  2. CGLIB 动态代理: 对于没有实现接口的类,Spring AOP 会使用 CGLIB 动态代理。它通过继承被代理类并重写方法的方式来创建代理对象,然后在子类中添加横切逻辑。

注解实现 AOP 的方式:

通过注解标记需要被增强的方法或者类,例如 @Before@After@Around 等。Spring AOP 使用切点表达式(Pointcut Expression)来确定在哪些方法或类上应用切面逻辑。

  1. Spring AOP 的代理对象: Spring 容器会创建被代理的对象,并使用动态代理技术创建代理对象,代理对象中包含了切面的逻辑代码。
  2. 注解的解析: Spring 框架扫描被注解标记的类或方法,解析注解,根据注解配置生成代理对象,并在运行时动态地将切面逻辑织入到被代理的对象方法中。

对于继承或接口的处理:

  • 继承关系: 如果被代理类存在父类,AOP 代理后的类仍然会保留原始类的继承关系。
  • 接口关系: 如果被代理类实现了接口,JDK 动态代理会在运行时基于接口生成代理对象,并且这个代理对象同时也是被代理类的子类。如果没有接口,则使用 CGLIB 动态代理创建代理对象。

注解主要是用来标识切面和切点,告诉 Spring 在哪里以及如何应用切面逻辑。在代理对象创建后,Spring AOP 将切面逻辑织入到代理对象的方法调用中,实现了横切关注点的功能。

8. Mysql的事务隔离级别?

Mysql默认的是什么级别?会出现幻读问题吗?怎么解决幻读的?

MySQL 支持四种事务隔离级别,分别是:

  1. 读未提交(Read Uncommitted): 最低级别的隔离,允许一个事务读取另一个事务未提交的数据。可能导致脏读、不可重复读和幻读问题。
  2. 读已提交(Read Committed): 表示一个事务只能读取另一个事务已提交的数据,避免了脏读,但仍然可能出现不可重复读和幻读问题。
  3. 可重复读(Repeatable Read): 确保在同一个事务中多次读取相同数据时,结果始终保持一致。通过在事务中对读取的数据添加共享锁来实现。
  4. 串行化(Serializable): 最高级别的隔离,确保事务串行执行,避免了脏读、不可重复读和幻读,但是会影响并发性能。

MySQL 默认的事务隔离级别是 可重复读(Repeatable Read)

幻读问题:

幻读问题是指在一个事务中,由于其他事务插入了新的数据行,导致前后两次查询结果不一致的现象。它与不可重复读问题类似,但不是指同一条数据的多次读取结果不一致,而是指一个范围查询,新插入的数据导致查询结果不一致。

幻读的解决方法:

  1. 使用更高级别的隔离级别: 例如将隔离级别设置为 Serializable,避免了幻读问题,但会影响并发性能。
  2. 使用锁: 可以在事务中对相关的数据行或表进行锁定,避免其他事务的插入操作影响到查询结果。
  3. 使用行级锁: 例如 SELECT ... FOR UPDATE,在读取数据时对数据行进行加锁,避免其他事务插入新数据。
  4. 使用索引: 合理地设计和使用索引,避免不必要的范围查询,减少幻读问题的发生。

9. undolog,redolog,binlog区别

  1. Undo Log(回滚日志):
    • 记录时机: 在事务执行过程中,当对数据进行修改时,将修改前的数据记录到 Undo Log 中。
    • 作用: 用于事务的回滚和 MVCC(多版本并发控制),保证事务的一致性和隔离性。在事务回滚或者某些读操作需要获取先前版本的数据时,可以利用 Undo Log 进行回滚或提供历史版本的数据。
  2. Redo Log(重做日志):
    • 记录时机: 在事务执行过程中,对数据进行修改时,将修改后的数据记录到 Redo Log 中。
    • 作用: 用于数据库的恢复,保证数据库的持久性。即使数据库出现宕机或异常情况,通过 Redo Log 可以重新执行崩溃前未完成的事务,确保数据不丢失。
  3. Binlog(二进制日志):
    • 记录时机: 在事务提交后,记录对数据库的修改操作。
    • 作用: 用于数据备份、复制和恢复。Binlog 记录了数据库的逻辑变更信息,包括对数据库的增删改操作,用于实现数据库的主从复制、数据恢复和数据备份等功能。

这些日志在不同的阶段记录信息,服务于不同的目的,共同确保了数据库的一致性、持久性和可恢复性。

手撕算法:

718. 最长重复子数组

最长重复子数组问题可以通过动态规划来解决。动态规划的思路是利用数组来记录状态,以解决问题。以下是 Java 中动态规划的一种实现方式:

假设有两个数组 AB,我们可以使用一个二维数组 dp 来记录状态,其中 dp[i][j] 表示以 A[i-1]B[j-1] 结尾的最长重复子数组的长度。

代码语言:javascript
复制
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 即为最长重复子数组的长度。

  • 原文链接:https://github.com/warthecatalyst/What-to-in-Graduate-School/blob/main/%E7%A7%8B%E6%8B%9B%E7%9A%84%E9%9D%A2%E7%BB%8F/%E5%8D%8E%E7%A7%91%E8%AE%A1%E7%A7%91%E7%AC%AC%E4%BA%8C%E4%BA%BA%E7%9A%84%E7%A7%8B%E6%8B%9B%E6%8A%A5%E5%91%8A.md
  • https://www.nowcoder.com/feed/main/detail/3b03fae3548442f2a95c470f61205f4a?sourceSSR=search
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-12-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 千羽的编程时光 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. HashMap jdk1.7/8有什么区别?元素数量下降长会变回链表吗?
  • 2. HashMap在多线程场景下使用,jdk7/8有都什么问题?问题有什么区别?
  • ConcurrentHashMap 底层原理
  • 3. ConcurrentHashMap怎么保证线程安全的?1.7的分段锁怎么实现的?
  • 4. Java语言的泛型是怎么实现的?为了解决什么问题而出现的?泛型的效率一定很低吗?
  • 5. Spring的循环依赖是怎么解决的?
  • 6. 动态代理分为两种,各自是怎么实现的?
  • 7. AOP的动态代理底层怎么实现的?
  • 8. Mysql的事务隔离级别?
  • 9. undolog,redolog,binlog区别
  • 718. 最长重复子数组
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档