文章目录
- 引言
- 正文
- 面经整理
- 牛客1
- 1、ArrayList和LinkedList的区别?ArrayList是线程安全的吗?如果不是如何实现线程安全?
- 2、ConcurrentHashMap怎么实现并发控制?
- 3、Synchronized和ReentrantLock分别介绍一下?如何实现可重入锁的?
- 4、ReentrantLock如何实现公平锁?
- 5、如何实现分布式锁?如何保证释放锁是自己的?
- 6、如何给锁续期?你是怎么做的?
- 7、ConcurrentHashMap的get方法加锁吗?不加锁,通过什么实现数据的正确性?
- 8、ConcurrentHashMap是如何实现并发控制的?
- 9、哈希结构的数据结构有哪些?
- 10、反射的应用场景是什么?在使用过程中有什么问题?
- 11、内存泄露如何排查?查到了如何处理?
- 12、你知道协程吗?协程为什么能够如此轻量级?
- 13、你知道哪些能够实现范围查找的数据结构,给我讲一遍?在讲讲是怎么实现的!
- 14、ZSet底层了解吗?插入、删除、查询是怎么实现的?时间复杂度是多少?
- 一面凉经
- 总结
引言
- 今天面试滴滴提前批,心里还是蛮荒张的,毕竟是第二次面大厂,尽力就好了!
正文
面经整理
牛客1
1、ArrayList和LinkedList的区别?ArrayList是线程安全的吗?如果不是如何实现线程安全?
底层数据结构:
- ArrayList:基于动态数组。
- LinkedList:基于双向链表。
访问速度:
- ArrayList:随机访问快(O(1))。
- LinkedList:随机访问慢(O(n))。
插入和删除:
- ArrayList:插入和删除慢(O(n)),特别是在中间位置。
LinkedList:插入和删除快(O(1)),只需调整指针。
内存消耗:
- ArrayList:相对更节省内存,只有数据存储。
- LinkedList:每个元素需要额外存储前后节点的指针。
实现线程安全
- 不是线程安全的
线程安全的方式
- Collections.synchronizedList:
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
- 使用线程安全的替代类
List<String> threadSafeList = new CopyOnWriteArrayList<>();
2、ConcurrentHashMap怎么实现并发控制?
分段锁机制(Segment-based locking)(在Java 7及之前):
- 将Map分成多个段(Segment),每个段维护自己的锁。并发操作可以在不同的段上同时进行,减少锁竞争。
CAS操作和Synchronized(在Java 8及之后):
- CAS(Compare-And-Swap):用于高效的并发更新操作。
- Synchronized:用于复杂操作如初始化或扩容时的同步控制。
红黑树:
- 当单个桶中的元素数目超过一定阈值(8),链表结构转换为红黑树,提升查找性能。
分配更小的锁范围:
- 仅对必要部分加锁,减小锁的粒度,提高并发性。
3、Synchronized和ReentrantLock分别介绍一下?如何实现可重入锁的?
Synchronized
- 内置锁:Java语言级别的关键字,用于修饰代码块或方法。
- 自动释放锁:线程退出临界区时自动释放锁。
- 不可中断:线程在等待锁时不能被中断。
- 简单易用:语法简单,易于理解。
- 单一条件队列:每个锁只有一个条件队列。
ReentrantLock
- 显式锁:属于java.util.concurrent.locks包,需要手动加锁和释放锁。
- 可中断:等待锁时可以响应中断。
- 公平锁和非公平锁:可以选择锁的公平性。
- 多个条件队列:支持多个Condition对象,实现更灵活的等待/通知机制。
- 提供更多功能:如尝试获取锁tryLock()、超时获取锁lockInterruptibly()等。
都是可重入锁,然后通过计数器实现
4、ReentrantLock如何实现公平锁?
- ReentrantLock使用AbstractQueuedSynchronizer (AQS) 来管理其同步状态。
- AQS 维护一个 FIFO(First-In-First-Out)队列来管理线程的等待顺序。
- 对于公平锁,ReentrantLock在尝试获取锁时,会检查队列中是否有其他等待的线程,如果有,则当前线程会被放入队列中等待,而不会“插队”。
5、如何实现分布式锁?如何保证释放锁是自己的?
- 使用的jedis客户端实现分布式锁
- key是锁的名字
- value是线程的唯一标识好
- px指定的过期时间
- nx防止覆盖
public boolean acquireLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}
- 释放锁
public boolean releaseLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return "1".equals(result.toString());
}
在释放锁时,通过 Lua 脚本,原子性地检查锁的标识是否与当前客户端匹配,仅当匹配时才释放锁。
获取锁:
- 使用 SET lockKey requestId NX PX expireTime 保证原子性。
释放锁:
- 使用 Lua 脚本保证检查和删除操作的原子性。
标识:
- 每个客户端在获取锁时都带上唯一标识 (requestId),在释放锁时进行验证。
6、如何给锁续期?你是怎么做的?
为什么要续期
- 在使用分布式锁时,锁的过期时间可能会较短,如果任务执行时间超过锁的过期时间,就需要对锁进行续期
- 以确保锁在任务执行期间不会被其他客户端抢占。
如何续期
获取锁:
- 使用 SET lockKey requestId NX PX expireTime 原子性地获取锁,并启动续期线程。
续期线程:
- 定期执行续期操作,检查锁是否属于当前客户端,如果是则通过 PEXPIRE 更新锁的过期时间。
释放锁:
- 停止续期线程,通过 Lua 脚本检查锁标识并删除锁。
7、ConcurrentHashMap的get方法加锁吗?不加锁,通过什么实现数据的正确性?
- 不加锁
- 通过无锁操作和 volatile 变量来确保并发访问的正确性,而不会使用传统的加锁机制。
8、ConcurrentHashMap是如何实现并发控制的?
分段锁(Segmented Locking):
- 早期版本(Java 7 及之前)的 ConcurrentHashMap 使用了分段锁,将整个哈希表分成多个段,每个段有一个独立的锁,允许多个线程并发地访问不同段的数据。
无锁操作(Lock-Free Operations):
- Java 8 及之后的版本改进了设计,使用了更复杂的无锁技术,如 CAS(Compare-And-Swap)操作来更新数据,从而减少锁的使用,提高并发性能。
volatile 变量:
- 关键字段使用 volatile 修饰,确保线程间的可见性,保证数据一致性。
9、哈希结构的数据结构有哪些?
10、反射的应用场景是什么?在使用过程中有什么问题?
应用场景
-
框架和库:
- 许多框架**(如Spring)使用反射来动态创建对象、调用方法和访问字段**。例如,依赖注入、AOP(面向切面编程)都大量依赖反射。
-
序列化与反序列化:反射用于将对象序列化为字节流或从字节流反序列化为对象。常见的库如Jackson、Gson等在处理JSON对象时使用反射。
-
测试框架:测试框架如JUnit使用反射来调用测试方法、创建测试实例,以及访问和修改私有字段和方法。
-
动态代理:反射是Java动态代理机制的基础,允许在运行时创建代理类,实现接口的实例,用于拦截方法调用(如Java的java.lang.reflect.Proxy)。
-
通用工具库: 反射用于开发通用的工具库,如Bean操作库(Apache Commons BeanUtils),可以动态地操作JavaBeans属性。
使用问题
-
性能开销:
- 反射的操作(如类加载、方法调用、字段访问)比直接调用慢,可能导致性能瓶颈。过多的反射操作可能显著影响应用程序的性能。
-
安全性问题:
- 反射可以绕过Java的访问控制机制,访问和修改私有字段和方法,可能引发安全漏洞,破坏封装性和类的设计契约。
-
代码可读性和可维护性:
- 使用反射的代码通常较难阅读和理解,因为它依赖于字符串(如类名、方法名)和动态类型检查,导致调试和维护变得复杂。
-
编译时检查缺失:
- 反射的操作在编译时无法检查,只有在运行时才能发现错误(如类名拼写错误、方法不存在),增加了运行时异常的风险。
11、内存泄露如何排查?查到了如何处理?
排查内存泄露
- 监控内存使用情况,是否持续增长,VisualVM
- 获取堆转储,使用VisualVM生成
- 分析堆转储,通过的引用链条确定哪些对象无法被垃圾回收
- 定位位置,通过引用链找到
处理内存泄露
- 修复泄露点,关闭文件,remove 相关的ThreadLocal变量
- 使用弱引用,强引用不会被垃圾回收
- 优化缓存,确保缓存大小适当,使用弱引用或者软引用引用缓存,防止内存泄漏
12、你知道协程吗?协程为什么能够如此轻量级?
用户态切换:
- 协程切换发生在用户态,不涉及内核态的上下文切换。
- 线程的上下文切换需要操作系统介入,需要保存和恢复CPU寄存器、堆栈等状态,代价较高,而协程切换只需保存和恢复少量的程序状态(如局部变量和程序计数器)。
低开销的栈:
- 协程通常使用更小的栈(通常是几KB),而线程的栈空间一般较大(通常是几MB)。这使得在相同内存条件下,可以创建更多的协程。
按需调度:
- 协程由开发者显式调度,只有在需要时才会进行切换。线程则由操作系统调度,可能会频繁地在不同线程之间切换,导致额外的调度开销。
减少锁竞争:
- 由于协程是单线程执行的多任务处理,多个协程在同一线程中执行,不会引发线程之间的竞争问题,减少了使用锁和上下文切换的开销。
13、你知道哪些能够实现范围查找的数据结构,给我讲一遍?在讲讲是怎么实现的!
二分搜索树
创建
- 从根开始,与当前节点比较,如果小于当前节点,则移动到左子树,否则移动到右子树,直到找到合适的空位置插入。
范围查找
- 从根节点开始,递归或者迭代的在范围内查找,将范围内的节点收集起来
平衡二分搜索树
- 树的高度是平衡的,需要经过左旋或者右旋以及对应的组合,实现二叉树的平衡,
B树和B+树
- 创建:节点包含多个键,按照顺序排列,插入是找到合适的叶子节点,如果节点满了,分裂节点,并调整父节点
跳表
- 创建:随机生成层数,在每一层插入节点并保持节点有序
- 范围查找:从顶层开始,逐层下降找到范围起点,然后在底层顺序查找所有符合范围的节点。
14、ZSet底层了解吗?插入、删除、查询是怎么实现的?时间复杂度是多少?
底层结构
- 跳跃表(Skip List):
- 用于快速的范围查找和有序性维护,跳跃表处理排序和范围查找。
- 哈希表(Hash Table):
- 用于快速的元素访问,哈希表处理快速的成员查找。
插入、删除、查询
插入:
- 先检查元素是否存在于哈希表中。如果存在且分数不同,则需要在跳跃表中删除旧的节点。
- 将新元素添加到哈希表。
- 将新节点插入到跳跃表的正确位置。
删除
- 从哈希表中删除该成员。
- 从跳跃表中删除该节点。
单次查询
- ZSCORE:获取元素的分数。
- 直接从哈希表中查找,时间复杂度为 O(1)。
- ZRANK:获取元素的排名。
- 需要在跳跃表中查找并计算排名,时间复杂度为 O(log N)。
- ZREVRANK:获取元素的逆序排名。
- 类似于 ZRANK,但从另一方向计算排名,时间复杂度也是 O(log N)。
范围查询
- ZRANGE:按排名范围获取元素。
- 在跳跃表中查找并遍历范围,时间复杂度为 O(log N + M),其中 M 是结果集的大小。
- ZREVRANGE:按逆序排名范围获取元素。
- 类似于 ZRANGE,但从另一方向遍历,时间复杂度也是 O(log N + M)。
- ZRANGEBYSCORE:按分数范围获取元素。
- 在跳跃表中查找并遍历范围,时间复杂度为 O(log N + M),其中 M 是结果集的大小。
一面凉经
1、Java中的垃圾回收机制有哪些?
垃圾回收机制干什么的
- 回收不在被使用的对象所占用的内存,防止内存泄露并优化程序性能。
- 打标记==》分空间==》搞压缩==》划分代==》小增量==》多线程==》多分区
常见的垃圾回收算法
-
标记-清除算法
- 遍历所有对象引用,标记出所有存活的对象。清除所有标记的对象。
- 简单高效,但是效率低,清除后会产生大量的内存碎片。
-
复制算法
- 双份空间,只用一份,一块满了,活着的去另外一边,然后清除当前这块地
- 高效并且没有内存碎片,但是需要双倍内存空间,对象复制成本高。
-
标记压缩
- 标记所有存活对象,然后将存活对象压缩到一边,删除其余空间
- 避免了内存碎片,但是压缩对象影响性能
-
分代回收GGC
- 根据生命周期划分为不同的代,对于不同的代采用不同的垃圾回收算法。
- 老年代,标记清除或者标记压缩
- 新生代采用复制算法。
- 根据生命周期划分为不同的代,对于不同的代采用不同的垃圾回收算法。
-
增量回收IGC
- 将垃圾税收分解为多个步骤,而不是一次性回收整个堆内存,减少垃圾回收的暂停时间。
- 减少停顿时间,实现复杂
-
并行垃圾回收PGC
- 多线程并行回收垃圾,加快垃圾回收速度
- 多核处理效率高,但是阻塞其他线程
-
G1垃圾回收器 G1 GC
- 大堆内存、低内存的垃圾回收器,将内存划分为多个大小相等的区域,优先回收垃圾最多的区域。每个区是采用标记压缩
- 停顿时间少,适用于多核环境和大内存使用,快速垃圾回收。
java1.8中默认的垃圾回收算法是什么
-
1. Parallel GC(并行垃圾回收器)
- 特点
- 年轻代收集:使用多线程的复制算法(copying)。
- 老年代收集:使用多线程的标记-压缩算法(mark-compact)。
- 多线程:使用多个线程同时进行垃圾回收操作,适用于多核 CPU,提高垃圾回收效率。
- 吞吐量优先:Parallel GC 设计的目标是最大化吞吐量,最小化垃圾回收对应用程序的影响时间。
- 特点
-
2. G1 GC(Garbage-First Garbage Collector)
- 特点
- 分区:将整个堆内存划分为多个相等大小的独立区域(Region),每个区域可以充当 Eden、Survivor 或 Old 区。
- 混合收集:年轻代和老年代一起进行收集(mixed collection),在一次垃圾回收过程中,既回收年轻代,又回收部分老年代。
- 并发标记:G1 GC 使用并发标记阶段来标记存活对象,从而减少应用程序暂停时间。
- 停顿时间目标:允许用户指定期望的停顿时间,通过调优停顿时间目标来满足应用程序的低延迟要求。
- 特点
2、操作系统的中零拷贝介绍一下?
传统文件传输
- 涉及两次系统调用,read和write函数,4次用户态和内核态的切换以及数据拷贝。
零拷贝
- 只需要一次sendfile或者两次系统调用mmap和write,减少了两次用户态和内核态的切换次数,再加上DMA技术,数据拷贝只需要两次。
- 两次数据拷贝过程,都不需要通过CPU,两次都是由DMA来搬运的。
通过零拷贝能够提高文件传输的速率,Kafka消息队列IO的吞吐量高的原因,就是使用了零拷贝技术
3、MySQL中的调优经验有哪些?
1. 表结构和索引优化
- 选择合适的数据类型,避免使用 NULL。
- 为查询频繁的列建立索引,使用覆盖索引。
- 避免冗余索引,优化复合索引顺序。
2. 查询优化
- 避免 SELECT *,只选择需要的列。
- 将复杂查询拆分为简单查询,或使用临时表。
- 尽量减少大表间的 JOIN,并使用 LIMIT 限制结果集大小。
3. 配置优化
- 调整 innodb_buffer_pool_size、query_cache_size(旧版)和 key_buffer_size。
- 设置合适的 max_connections 和 thread_cache_size。
- 调整 innodb_log_file_size 和临时表大小配置。
5. 监控和分析
- 启用慢查询日志,使用 EXPLAIN 分析查询执行计划。
- 使用性能监控工具(如 performance_schema)监控数据库性能。
6. 分库分表和读写分离
- 通过垂直和水平分区减少单表数据量。
- 配置主从复制实现读写分离,提升并发性能。
7. 定期维护
- 定期执行 OPTIMIZE TABLE 重建和优化表。
- 定期备份并验证恢复方案。
- 通过这些关键点,可以有效提高 MySQL 的性能和稳定性。
4、TCP四次挥手为什么是2MSL
-
确保最后的 ACK 能够被正确接收:
- 客户端发送的最后一个ACK报文可能会丢失,然后服务器未收到,会重新发FIN。
- 2个MSL,有足够的时间确保最后一个ACK报文能够被服务器接收
-
防止旧报文干扰新链接
- MSL是报文在网络中存活的最长时间
- 等待2MSL能够,确保当前连接中的所有报文都会在网络中被清除掉,避免影响新链接。
总结
- 最大的败笔,就是项目讲的是算法的,他让我随机抽一个算法,我应该讲java的项目,后面再面试,绝对要把项目讲好,不能再讲别的了,绝对不能讲算法类的项目,你要做的是后端开发的项目!投的也是这个岗位!