NIO蔚来 后台应用开发 一面
基本内容
- Java基本容器类
- ArrayList和LinkedList区别
- HashMap底层原理
- Mysql慢查询排查
- Mysql索引机制和原理
- Redis如何做到高性能
- Redis应用场景
- 线程池的参数
- 死锁产生的条件
- JVM垃圾回收机制
讲解
Java基本容器类
- ArrayList: java.util.ArrayList是一个动态数组实现,它可以自动调整大小。它提供了在列表末尾快速添加和删除元素的能力。
ArrayList<String> list = new ArrayList<>(); list.add("元素1"); list.add("元素2");
- LinkedList: java.util.LinkedList是一个双向链表实现。它提供了在列表开头和末尾快速添加和删除元素的能力。
LinkedList<String> linkedList = new LinkedList<>(); linkedList.add("元素1"); linkedList.add("元素2");
- HashSet: java.util.HashSet是一个集合实现,它基于哈希表。它不保证元素的顺序,且不允许重复元素。
HashSet<String> set = new HashSet<>(); set.add("元素1"); set.add("元素2");
- TreeSet: java.util.TreeSet是一个基于红黑树的有序集合实现。它按照元素的自然顺序或者通过提供的比较器进行排序。
TreeSet<String> treeSet = new TreeSet<>(); treeSet.add("元素1"); treeSet.add("元素2");
- HashMap: java.util.HashMap是一个基于哈希表的键值对映射实现。它不保证键值对的顺序,且不允许重复键。
HashMap<String, Integer> map = new HashMap<>(); map.put("键1", 1); map.put("键2", 2);
- TreeMap: java.util.TreeMap是一个基于红黑树的有序键值对映射实现。它按照键的自然顺序或者通过提供的比较器进行排序。
TreeMap<String, Integer> treeMap = new TreeMap<>(); treeMap.put("键1", 1); treeMap.put("键2", 2);
- LinkedHashMap: java.util.LinkedHashMap是一个基于哈希表和双向链表的有序键值对映射实现。它保持了元素的插入顺序。
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>(); linkedHashMap.put("键1", 1); linkedHashMap.put("键2", 2);
ArrayList和LinkedList区别
- 内部数据结构:
- ArrayList 使用动态数组来存储元素。它提供了对元素的快速随机访问,因为它可以通过索引直接访问数组元素。
- LinkedList 使用双向链表结构,每个元素都包含对前一个和后一个元素的引用。这使得在列表中间插入或删除元素更为高效,但在随机访问方面性能较差。
- 随机访问性能:
- ArrayList 支持快速的随机访问,因为它可以通过索引直接访问元素。时间复杂度为 O(1)。
- LinkedList 需要遍历链表直到达到所需位置,因此随机访问的性能较差。时间复杂度为 O(n)。
- 插入和删除性能:
- 在 ArrayList 中,插入和删除元素可能涉及到元素的移动,特别是在列表的开头或中间。因此,插入和删除操作的性能可能较低,时间复杂度为 O(n)。
- LinkedList 在插入和删除方面更为高效,因为只需要改变相邻节点的引用。在列表的开头或中间插入或删除元素的性能较好,时间复杂度为 O(1)。
- 空间复杂度:
- ArrayList 通常比 LinkedList 占用更少的内存,因为它只需要存储元素的值和一些额外的数组信息。
- LinkedList 需要存储每个元素的值以及对前一个和后一个元素的引用,因此可能占用更多的内存。
- 适用场景:
- 如果需要频繁随机访问元素,并且对内存占用不太敏感,可以选择 ArrayList。
- 如果需要频繁在列表中间进行插入和删除操作,或者对内存占用有限制,可以选择 LinkedList。
HashMap底层原理
- 哈希表: HashMap 使用一个数组(称为哈希表)来存储键值对。数组的每个位置称为桶(bucket),每个桶可以存储一个或多个键值对。数组的长度通常会随着元素的增加而动态调整,以保持较低的负载因子。
- 哈希函数: 当你将一个键值对放入 HashMap 中时,HashMap 会使用键的哈希码(通过调用键的 hashCode() 方法得到)来确定该键值对在数组中的位置。哈希码是通过哈希函数映射到数组索引的。
- 解决哈希冲突: 由于哈希函数的映射并不是一一对应的,可能会出现不同键产生相同哈希码的情况,这就是哈希冲突。HashMap 使用链地址法来解决冲突,即在同一个桶中用链表(JDK7 中是链表,JDK8 中引入了红黑树)存储多个键值对。
- 红黑树: 为了提高在桶中查找效率,JDK8 引入了红黑树。当链表的长度达到一定阈值时,链表会被转换为红黑树。红黑树在查找操作上具有更好的性能,尤其是在元素数量较大的情况下。
- 负载因子: 负载因子是衡量哈希表空间利用率的一个指标。当哈希表中的元素数量超过数组大小乘以负载因子时,哈希表会进行扩容,即重新分配更大的数组,并重新计算每个元素的位置。默认负载因子是 0.75。
- 扩容与重新哈希: 当 HashMap 需要扩容时,会创建一个新的更大的数组,然后将所有的键值对重新哈希到新的数组中。这个过程可能比较耗时,但是由于哈希表大小的增加,可以保持较低的负载因子,从而提高 HashMap 的性能。
HashMap 的时间复杂度通常是 O(1)(假设没有哈希冲突),但在极端情况下可能会达到 O(n)(所有键映射到同一个桶中)。在实际应用中,HashMap 提供了高效的键值对存储和检索能力。
Mysql慢查询
慢查询是指执行时间超过一定阈值(通常以秒为单位)的SQL查询。慢查询的排查和优化是数据库性能优化的重要一环。以下是一些排查MySQL慢查询的步骤:
- 开启慢查询日志:
- 打开MySQL配置文件(通常是my.cnf或my.ini)。
- 在[mysqld]部分添加或修改如下配置:
slow_query_log = 1 slow_query_log_file = /path/to/slow-query.log long_query_time = 2
- slow_query_log用于启用慢查询日志,slow_query_log_file指定日志文件路径,long_query_time指定慢查询的阈值(单位为秒)。
- 查看慢查询日志:
- 打开慢查询日志文件,查看其中的慢查询语句。
- 通过分析慢查询日志,你可以找到执行时间较长的SQL语句,以及可能需要优化的部分。
- 使用EXPLAIN分析查询计划:
- 使用EXPLAIN关键字分析查询计划,查看MySQL是如何执行查询的。
- 例如:EXPLAIN SELECT * FROM your_table WHERE your_condition;
- 分析结果中的字段含义可以参考MySQL官方文档。
- 使用索引:
- 确保查询中的列上有合适的索引。通过EXPLAIN可以看到是否使用了索引。
- 使用SHOW INDEX FROM your_table;查看表的索引信息。
- 优化查询语句:
- 通过修改SQL语句、添加索引、分表分库等方式来优化查询性能。
- 使用SELECT只选择所需的列,而不是使用SELECT *。
- 避免在WHERE子句中对列进行函数操作,这可能会阻止索引的使用。
- 使用数据库性能工具:
- 使用MySQL性能工具,如MySQL Performance Schema、pt-query-digest等,来帮助识别慢查询并分析性能瓶颈。
- 定期优化统计信息:
- 通过定期更新统计信息来保持索引的有效性,使查询优化器能够做出更好的执行计划。
- 使用ANALYZE TABLE your_table;可以更新表的统计信息。
- 硬件和配置优化:
- 确保数据库服务器硬件足够强大,适应负载。
- 调整MySQL的配置参数,例如innodb_buffer_pool_size、innodb_log_file_size等,以适应系统性能。
Mysql索引机制和原理
- B-Tree索引:
- 原理: B-Tree索引按照排序顺序存储索引列的值,通过二分查找等算法实现快速数据检索。
- 例子: 如果有一个表users,有一个B-Tree索引在username列上,那么通过二分查找,可以快速找到具有特定用户名的用户。
- 聚簇索引和非聚簇索引:
- 原理: 聚簇索引将数据行和索引存储在同一个B-Tree结构中,而非聚簇索引将它们分开存储。
- 例子: 在InnoDB中,如果表orders有一个聚簇索引在order_id列上,那么相邻的order_id值的数据行在物理上也是相邻存储的。
- 复合索引:
- 原理: 复合索引是对表的多个列建立的索引,按照列的顺序依次存储。
- 例子: 如果有一个复合索引在表products的(category, price)列上,那么查询某个特定类别下价格范围内的产品会受益于该索引。
- 全文索引:
- 原理: 全文索引用于对文本数据进行全文搜索,支持关键字搜索而不仅仅是简单的匹配。
- 例子: 在一篇文章的表articles上建立全文索引,可以用于搜索包含特定关键词的文章。
- 覆盖索引:
- 原理: 覆盖索引是指查询中的列都包含在索引中,不需要回表到原始数据表。
- 例子: 如果有一个索引在表customers的(customer_id, email)列上,查询某个customer_id对应的email时,由于索引已经包含了email,因此不需要再回到原始数据表。
Redis如何做到高性能
Redis实现高性能主要依赖于以下几个方面的设计和优化:
- 内存存储: Redis将所有数据存储在内存中,以保证快速的读写访问。由于内存的读写速度远高于磁盘,这使得Redis能够提供极高的性能。
- 单线程模型: Redis采用单线程模型,即每个Redis实例都是单线程的。这意味着Redis不会出现多线程之间的竞争条件和锁的开销。虽然这看似会限制并发处理能力,但由于避免了线程切换和同步开销,单线程模型在实际中表现出色。
- 非阻塞I/O: Redis使用非阻塞I/O模型,通过事件循环机制处理多个客户端请求。这允许Redis在处理一个客户端请求的同时,能够响应其他请求,从而提高并发处理能力。
- 数据结构: Redis支持丰富的数据结构,如字符串、列表、集合、哈希表等。这允许开发者在不同场景中选择最适合的数据结构,以提高数据处理效率。
- 持久化策略: Redis支持多种持久化策略,包括RDB快照和AOF日志。通过适当配置,可以在保证数据持久性的同时最小化对性能的影响。
- 集群和分片: Redis支持集群模式和分片(sharding),允许水平扩展,提高处理大规模数据和请求的能力。
- 哨兵模式: Redis的哨兵模式用于高可用性,当主节点发生故障时,能够自动切换到备用节点,提高系统的可用性。
- 网络协议: Redis使用简单的文本协议,减少了协议解析的开销。这使得Redis在网络层面也能够保持较高的性能。
- 优化算法: Redis的内部实现采用了许多高效的算法,如快速的字符串操作、哈希函数等。这些算法的选择和优化都有助于提升性能。
- 精简代码: Redis的代码相对精简,避免了复杂的逻辑和不必要的开销,从而提高了整体性能。
Redis的应用场景
Redis是一个高性能的键值存储系统,其灵活的数据结构和快速的读写操作使得它在多个应用场景中都得到了广泛应用。以下是一些常见的Redis应用场景:
- 缓存: 最常见的用途是作为缓存层,存储频繁访问的数据。由于Redis的高性能和低延迟,能够显著提升应用程序的性能,减轻数据库负载。
- 会话存储: 用于存储用户会话信息。通过将用户的登录状态和相关信息存储在Redis中,实现快速的会话访问,减轻了应用服务器的负担。
- 消息队列: Redis的发布/订阅机制和列表结构可以用作简单的消息队列系统。生产者将消息推送到列表中,而消费者从列表中弹出消息进行处理,实现异步消息传递。
- 计数器和统计: 适用于实时计数和统计功能,例如网站的访问量、点赞数、在线用户数等。通过Redis的原子操作,能够高效地进行计数。
- 分布式锁: Redis可以用于实现分布式锁,确保在分布式环境下对共享资源的访问是互斥的。
- 实时排行榜: 使用有序集合数据结构,将分数与成员关联,可以实现实时排行榜功能,比如最高分玩家、最畅销产品等。
- 地理位置服务: Redis的地理位置数据类型(Geo)可以用于存储和查询地理位置信息,例如附近的人、位置基础服务等。
- 分布式会话: 在分布式环境中,可以使用Redis来存储共享的会话信息,以确保多个服务实例之间的会话同步。
- 任务队列: 将需要异步执行的任务放入列表中,然后使用消费者来处理这些任务。这可以用于后台任务的处理,如发送邮件、生成报告等。
- 缓存加速数据库: 通过将热点数据存储在Redis中,可以显著提高数据库查询性能,减轻数据库负载。
线程池参数
- 核心线程数(Core Pool Size):
- 含义: 线程池中保持活动状态的线程数,即使它们处于空闲状态。
- 配置方式: 通过构造函数或配置文件设置。
- 最大线程数(Maximum Pool Size):
- 含义: 线程池中允许的最大线程数,包括核心线程数和临时创建的线程数。
- 配置方式: 通过构造函数或配置文件设置。
- 线程存活时间(Keep Alive Time):
- 含义: 空闲线程被保留的最长时间,超过这个时间将被终止。
- 配置方式: 通过构造函数或配置文件设置,通常和时间单位一起使用。
- 时间单位(Time Unit):
- 含义: 与线程存活时间一起使用,指定时间的单位,例如秒、毫秒等。
- 配置方式: 通过构造函数或配置文件设置。
- 工作队列(Work Queue):
- 含义: 用于存储等待执行的任务的队列。当线程池中的线程数达到核心线程数时,新任务会被放入工作队列。
- 配置方式: 通过构造函数或配置文件设置,有不同的实现,如LinkedBlockingQueue、ArrayBlockingQueue等。
- 拒绝策略(Rejected Execution Handler):
- 含义: 在达到最大线程数并且工作队列已满的情况下,定义拒绝任务的处理策略。
- 配置方式: 通过构造函数或配置文件设置,常见的有AbortPolicy(抛出异常)、CallerRunsPolicy(由调用线程处理)、DiscardPolicy(丢弃任务)等。
- 线程工厂(Thread Factory):
- 含义: 用于创建新线程的工厂。
- 配置方式: 通过构造函数或配置文件设置,通常为ThreadFactory接口的实现。
- 是否允许核心线程超时(Allow Core Thread TimeOut):
- 含义: 决定是否对核心线程采用存活时间的限制策略。
- 配置方式: 通过构造函数或配置文件设置。
死锁产生的条件
死锁是多个进程或线程因争夺资源而陷入无限等待的状态。死锁产生的条件通常有四个,被称为死锁的四个必要条件,它们是:
- 互斥条件(Mutual Exclusion):
- 资源不能被共享,即一次只能由一个进程占用。如果一个进程获得了某个资源,其他进程必须等待释放。
- 请求与保持条件(Hold and Wait):
- 进程在请求新的资源的同时,保持对已经获得的资源的占有。如果一个进程在等待其他资源的同时不释放已经占有的资源,就可能导致死锁。
- 不剥夺条件(No Preemption):
- 已经获得的资源不能被强制性地从持有它的进程中剥夺。只有当持有资源的进程自愿释放时,其他进程才能获得这些资源。
- 循环等待条件(Circular Wait):
- 存在一种进程等待序列,其中每个进程都在等待下一个进程释放资源。形成一个循环等待环路。
JVM垃圾回收机制
Java虚拟机(JVM)的垃圾回收机制是自动管理内存的一部分,负责回收不再被程序引用的无用对象,以释放内存空间。Java的垃圾回收机制主要基于两个原则:引用计数和可达性分析。
引用计数算法:
引用计数算法通过为每个对象维护一个引用计数器,记录对象被引用的次数。每当有一个新的引用指向对象时,计数器加1;当引用被销毁或不再指向该对象时,计数器减1。当计数器为零时,表示该对象不再被引用,可以被回收。
缺点:
- 无法解决循环引用的问题,即使对象之间存在循环引用,它们的引用计数都不为零,导致内存泄漏。
可达性分析算法:
可达性分析算法是Java虚拟机实际使用的主要垃圾回收算法,基于"可达性"的概念,即通过一系列的引用关系,判断对象是否还能被程序访问到。
- 根集(Root Set): 从根集出发,根集包括虚拟机栈、本地方法栈、方法区的类静态属性引用等。根集中的对象被认为是活跃对象。
- 可达对象: 通过根集引用关系,追踪所有从根集可达的对象形成一个对象图。这个图中的对象被认为是活跃对象,其它对象则被判定为垃圾。
- 垃圾收集: 对于不可达对象,说明它们不再被程序使用,可以被垃圾回收。垃圾回收器负责回收这些无用对象的内存,释放资源。
Java虚拟机中常用的垃圾回收算法包括:
- 标记-清除算法(Mark and Sweep): 标记所有可达对象,清除所有不可达对象。
- 复制算法(Copying): 将存活对象复制到一个新的空间,清理原空间中的所有对象。
- 标记-整理算法(Mark and Compact): 标记所有可达对象,将它们向一端移动,然后清理其它端的所有对象。