文章目录
- 引言
- 面经收集
- 面经整理一
- 1. ArrayList和LinkedList
- 2. 线程安全的列表和链表有么?如果没有怎么实现?
- 3. threadlocal
- 4. synchronized锁升级过程及原理
- 5. ReentrantLock原理,以及和synchronized的对比
- 6. 线程池工作原理
- 7. redis常用数据结构
- 8. 缓存穿透以及解决方案
- 9. zset底层原理
- 10. 事务隔离级别
- 11. 索引
- 12.innodb引擎和myisam的区别
- 13.mysql的锁
- 14.JWT如何限制单个用户进行登录?
- 15.一道sql,问走没走索引
- 16.aqs
- 面经整理二
- 1、SQL的具体执行流程?
- 2、MySQL的B+树?
- 3、聚簇索引介绍一下?
- 4、spring声明式事务的过程,失效场景有哪些
- 5、session和cookie简单介绍一下,有什么区别?
- 6、说明一下Spring AOP的实现以及具体调用方式
- 7、ArrayList和LinkedList两种List?
- 实际面经
- 1、Equals和hashcode两个的区别?重写的时候有什么需要注意?
- 2、为什么String设计为final,设计为常量?(之前没有被这么问过,还是有点懵的)
- 3、synchronized如何实现可重入锁(没被这样问过,只知道monitor实现的,照着ReentrantLock说了一下)
- 4、说一下zset的具体实现?查找元素排名的过程?跳表是如何查找元素的?
- 5、Redis为什么这么快?(最重要的前三点都说了)
- 6、讲讲io多路复用(这里记岔了,记成了零拷贝了,难受!)
- 7、说一下redis的几种数据结构的具体应用场景?(这个说的也不好,当时很多没想起来,就算用过了,也没想起来)
- 8、MySQL索引的具体实现结构(基本上都说出来了)
- 9、MySQL索引的作用?索引失效的几种场景(这个基本上都说了,唯一不足可能就是对于最左索引匹配表述不清楚)
- 10、Java的多线程编程有几种方式?如何提交多线程的任务?(基本上都说出来了)
- 11、如何实现线程安全(基本上都说了)
- 12、Array List和Linkedlist的区别?(完全整理过了,基本上都能够回答出来)
- 13、ArrayList的扩容(这个完全说错了,没了解过这个,只知道redis里面的扩容,就照着那个说了)
- 14、Java中的输入输出流分为几种?(这个就说出来了字节流和字符流,但是具体的就不知道了,真尴尬)
- 总结
引言
- 还是比较想去携程,说服一下自己吧!
- 弹性工作制,八小时工作制
- 工作氛围良好,不会很push
- 喜欢旅游,刚好有旅游补贴
- 关键还有专门午睡的房间
- 所以这个博客还是先整理网上能找到的面经以及对应的八股,然后再是我实际面试的面经。
面经收集
- 目前来看,携程主要分为两面,一面主要拷问八股,然后二面主要问项目加上手撕,然后就是HR面试。
面经整理一
作者:给口饭吞
链接:https://www.nowcoder.com/feed/main/detail/5a86c00e07fe45f88fafc81c41fb5588?sourceSSR=search
来源:牛客网
1. ArrayList和LinkedList
-
- 数据结构基础
- ArrayList:基于动态数组实现,连续的内存空间存储元素。
- LinkedList:基于双向链表实现,元素通过指针链接,内存空间不连续。
-
- 访问速度
- ArrayList:随机访问元素速度快,时间复杂度为 O(1),因为可以通过索引直接访问元素。
- LinkedList:随机访问元素速度慢,时间复杂度为 O(n),因为需要从头或尾部遍历链表。
-
- 插入和删除
- ArrayList:插入和删除操作较慢,特别是在中间位置操作时,时间复杂度为 O(n),因为需要移动大量元素。
- LinkedList:插入和删除操作较快,时间复杂度为 O(1),只需要调整节点指针,但需要先遍历到指定位置(遍历时间复杂度 O(n))。
-
- 内存占用
- ArrayList:因为是数组,除了元素本身不需要额外的内存;但数组的动态扩展需要分配新内存并复制数据。
- LinkedList:每个元素除了存储数据,还需要存储前驱和后继指针,导致内存开销比 ArrayList 高。
2. 线程安全的列表和链表有么?如果没有怎么实现?
线程安全的List
-
Vector
- 通过Synchronized关键字实现线程安全,
- 全局锁,性能较低
-
CopyOnWriteArrayList
- Java 的并发包 java.util.concurrent 提供的线程安全 List 实现。执行修改操作时会复制底层数组,然后将新的数组替换为当前数组。
- 适合读多写少的情况,写操作开销大
- 底层实现:
- 所有操作都是直接共享的、只读的底层数据操作,无需加锁,性能高
- 写操作创建的是新数组副本,并且读写是分离的
-
如果是你怎么实现
- synchronized全局锁
- reentrantReadWriteLock读写锁
3. threadlocal
核心概念
- 每个线程都会有一个独立的变量副本,其他线程无法访问或修改这些变量。ThreadLocal 的这种机制避免了多线程环境下的数据共享问题,也因此避免了同步的开销。
常用场景
- 每一个线程都需要独立的变量
- 避免使用锁的开销
- 维护线程独立状态
注意事项
- 防止内存泄露,用完记得用remove
4. synchronized锁升级过程及原理
无锁状态
- 对象在创建之初,对象头的Mark Word被没有任何锁的标志位设置,自由访问
偏向锁
-
第一个线程获取就升级为偏向锁
-
当第一个线程尝试获取某个对象的synchronized锁时,会将对象头标记设置为偏向锁,并在对象头标记填上获取当前锁的线程ID。
- 作用
- 减少在单线程环境下获取锁的开销,一个线程肯定会多次获取一个锁的
- 作用
轻量级锁
- 第二个线程尝试获取偏向锁
- 当有第二个线程尝试获取偏向锁时,偏向锁就会撤销,变为轻量级锁状态
- 具体实现
- 使用线程的本地的ThreadLocal变量存储锁记录,包含了锁记录的MarkWord拷贝
- 锁竞争
- 如果获取锁失败,会通过自旋来获取锁,超过阈值,轻量级锁膨胀为重量级锁
- 具体实现
重量级锁
- 自旋失败就是重量级锁
- 如果自旋尝试失败就是重量级锁,重量级锁是通过操作系统的互斥锁实现的获取锁失败,阻塞唤醒都需要操作系统实现
5. ReentrantLock原理,以及和synchronized的对比
ReentrantLock原理
- 显式锁
- Java中的显式锁,实现了Lock接口,基于抽象队列同步器实现
- 重入性
- 同一个线程可以多次获得同一个锁,不会发生死锁的。
- 可中断性(独有)
- 支持响应中断,线程可以在等待锁的过程中被中断退出
- 超时机制(独有)
- 在指定的时间内尝试获取锁,超过这个时间将自动放弃
- 手动释放锁
- 显式调用lock和unlock进行解锁
Synchronized原理
- 内置锁
- 基于对象监视器实现,进程进入synchronized的是偶,尝试获取对象的监视器锁。
- 锁不可见,Java虚拟机会在字节码层面处理锁的获取和释放。
- 重入性
- 可重入锁,持有锁的情况下,进入同步代码块
- 不可中断性
- 一旦进入同步代码块,锁不可用,线程会一直等待,不可中断
- 无锁超时
- 不提供获取锁的超时功能,线程会一直等待锁,直到可用为止。
- 自动释放锁
- 不需要手动加锁和解锁,自动会释放锁。
6. 线程池工作原理
简介
- 通过重用现有线程来处理多个任务,避免了创建线程和销毁线程的开销,实现并发编程。
工作原理
-
任务队列
- 使用阻塞队列存放等待执行的任务,当所有线程都在忙碌时,新的任务会放入队列中,等待线程空闲时执行。
-
工作线程
- 线程池内部维护一组工作线程,线程不断从任务队列中获取任务进行执行。
-
线程池管理器
- 管理工作线程的生命周期
-
拒绝策略
- 直接丢:DiscardPolicy
- 报异常:AbortPolicy
- 谁交谁干:CallerRunsPolicy
- 新换旧:DiscardOldestPolicy
7. redis常用数据结构
-
1. String(字符串)
- String 是 Redis 中最基本的数据类型,每个键对应的值是一个字符串。字符串类型可以存储普通文本、数字等,并且支持的操作非常广泛。
-
2. List(列表)
- List 是一个双向链表,可以在头部或尾部进行插入和删除操作,列表中的元素是有序的,元素可以重复。
-
3. Set(集合)
- Set 是一个无序的集合,集合中的元素是唯一的,Redis 使用哈希表来实现集合,因此操作的时间复杂度为 O(1)。
-
4. Sorted Set(有序集合)
- Sorted Set 是一种带权重的集合,每个元素有一个分数(score),集合中的元素按分数进行排序,具有自动排序的功能。
-
5. Hash(哈希)
- Hash 类型存储的是键值对集合,适合用于存储对象或结构化数据(类似于 Java 中的 Map 结构),每个键对一个对象,对象中有多个字段。
8. 缓存穿透以及解决方案
缓存穿透是什么
- 客户请求的数据在缓存中和数据库中都不存在,每次都会访问数据库,给数据库造成很大的压力。
缓存穿透的原因
- 请求的键不存在
- 用户频繁请求无效数据,恶意用户故意发起大量请求查找不存在的商品ID
- 缓存未对空结果进行处理
- 数据库中不存在响应的数据,导致每次相同请求都要访问数据库
缓存穿透的解决办法
-
缓存空值
- 数据库中不存在的数据,将空结果缓存起来
- 特点
- 操作简单,但是会占用大量的缓存空间
-
布隆过滤器
- 缓存和数据库之间引入布隆过滤器,判断某个键是否存在。
- 拦截明确不存在的数据请求,减少对数据库的无效访问
- 特点
- 占用空间小,但是存在一定的误判率
- 缓存和数据库之间引入布隆过滤器,判断某个键是否存在。
-
参数校验
- 访问数据库前对参数进行校验
9. zset底层原理
什么是ZSet
- 特点是每个元素不仅包含一个值(member),还关联一个分数(score),并且 Redis 可以根据分数对元素进行排序
- 有序集合
底层原理
- 通过哈希表和跳表实现
- 哈希表
- 哈希表保证了元素的快速查找
- 跳表
- 提供了范围查询和按分数排序的功能
- 查找的时间复杂度是O(logN)
常规操作
- 按分数范围查询 ZRANG
- 获取排名ZRANK
- 获取分数ZSCORE
10. 事务隔离级别
11. 索引
索引的作用
- 索引是能够提高数据库从表中检索数据行的速度的一种数据结构,但是需要额外的写入和存储来维护
索引的种类
- 常见的包括 B-Tree 索引、Hash 索引、全文索引、唯一索引和主键索引等。
12.innodb引擎和myisam的区别
事务支持
- InnoDB:支持事务,是 MySQL 中实现 ACID(原子性、一致性、隔离性、持久性)特性的存储引擎。InnoDB 提供了提交(COMMIT)、回滚(ROLLBACK)和崩溃恢复等功能。
- MyISAM:不支持事务。每个查询都是独立的操作,一旦执行就无法回滚或撤销。
外键支持
- InnoDB:支持外键约束,可以在表之间建立外键关系,保证数据的完整性和一致性。当进行插入、删除或更新操作时,InnoDB 会自动检查外键约束。
- MyISAM:不支持外键约束,无法在表与表之间建立外键关系。
锁机制
- InnoDB:支持行级锁和表级锁,默认情况下使用行级锁。这意味着当多个用户同时更新不同的行时,InnoDB 可以并行执行,不会互相阻塞,适合高并发的场景。
- MyISAM:只支持表级锁。在执行查询或更新操作时,整个表会被锁定,所有其他对该表的读写操作都会被阻塞,适合读多写少的应用场景。
存储结构
- InnoDB:使用聚簇索引进行存储。
- 数据按主键顺序存储。聚簇索引的叶子节点直接保存数据行,因此主键的选择非常重要。
- MyISAM:使用非聚簇索引。
- 索引和数据是分离存储的。索引文件存储的是指向数据文件的地址,查询数据时需要通过索引定位到数据文件中的对应位置。
13.mysql的锁
全局锁
- 全局锁是对整个数据库实例加锁的操作,通常用于备份等场景,以确保在备份期间数据一致性。
表级锁
- 表级锁是对整个表加锁。MySQL 提供两种表级锁:表共享读锁和表排他写锁。
- 共享读锁
- 当一个线程对表加了读锁时,其他线程仍可以读取该表,但不能进行写操作。
- 排他写锁
- 当一个线程对表加了写锁时,其他线程的读写操作都会被阻塞,直到写锁释放。
- 共享读锁
行级锁
- 行级锁是 MySQL 中粒度最细的锁,作用于数据表中的某一行或多行数据。MySQL 的 InnoDB 存储引擎支持行级锁。
- 常见的行级锁
- 共享锁和排他锁
间隙锁
- 间隙锁是 InnoDB 引擎的一种特殊锁类型,用于避免幻读现象。它不仅会锁住查询结果的行,还会锁住这些行之间的“间隙”。
14.JWT如何限制单个用户进行登录?
之前完整的画过类似的内容,这里完全整理一下
- 参考连接
解决办法
- 通过在 JWT 中引入一个唯一的 sessionId 并将其与服务端存储的标识符进行对比,可以有效限制单个用户的登录,并实现一个用户在不同设备上登录时自动让之前的会话失效的机制。这种方式结合了 JWT 的无状态特性和服务端的状态存储能力,从而灵活控制用户的登录状态。
15.一道sql,问走没走索引
Explain查询执行计划,检查是否使用了索引
- 使用Explain查询执行计划,检查是否走了索引,主要是观察一下几个字段
- type:连接类型,表示查询的范围。常见的类型包括:
- ALL:全表扫描,没有使用索引,效率最低。
- index:全索引扫描,使用了索引但效率较低。
- range:使用了索引范围查询,效率较高。
- ref:使用普通索引查询,效率较高。
- const/eq_ref:使用主键或唯一索引查询,效率最高。
- possible_keys:查询中可能使用到的索引。
- key:实际使用的索引,如果该列为空,则表示未使用索引。
- Extra:额外信息,常见的有:
- Using where:表示在结果过滤时使用了 WHERE 条件。
- Using index:表示覆盖索引查询,意味着所有需要的数据都来自索引,而无需访问表。
- Using filesort:表示 MySQL 需要额外排序,说明未能有效利用索引进行排序。
- Using temporary:表示使用了临时表。
- type:连接类型,表示查询的范围。常见的类型包括:
16.aqs
什么是AQS
- 一个基础框架类,用于构建锁和其他同步器。
- 是基于FIFO队列的线程阻塞机制
AQS的核心概念
-
基于一个状态变量(state)和一个FIFO 等待队列来实现线程的排队和同步管理。
-
State状态变量
- AQS 内部维护的一个 int 类型的变量,用于表示锁的状态
-
FIFO队列
- 当获取锁的线程无法成功获得时,会被挂起并加入到一个 FIFO 队列中。当锁被释放时,AQS 会从队列中唤醒下一个等待的线程。
面经整理二
作者:感谢信垃圾桶
链接:https://www.nowcoder.com/discuss/599929326565421056?sourceSSR=search
来源:牛客网
1、SQL的具体执行流程?
解析
- 语法检查
- 是否符合SQL语法规范
- 语义检查
- 引用的表和引用列是否存在
- 生成抽象语法树
优化
- 基于解析树生成多种执行计划,选择最优
- 是否使用索引
- 参考字段等信息
生成执行计划
- 定义读数据方式
- 数据库连接方式
- 排序方式等
执行
- 数据读取
- 计算
- 返回结果
返回结果
- 查询语句,会直接返回结果给客户端
- 修改操作,会返回影响行数为客户端
2、MySQL的B+树?
特点
-
平衡性
- 所有叶子节点处于同一层,无论查找哪个元素,经过的层数是相同
-
叶子节点存储数据
- 所有数据存储在叶子节点
- 叶子节点通过链表连接,能够实现范围查询
多路查找
- 每一个节点包含多个子节点,一次IO可以读取更多的数据,煎炒磁盘访问次数
树的结构
-
根节点
- 用于划分数据范围,指向不同的子节点
-
内部节点
- 存储键值,但是不存储实际数据
-
叶子节点
- 底层节点,存储实际的数据行和指向数据行的指针。
- 叶子节点通过链表连接,便于区间查询
3、聚簇索引介绍一下?
简介
- 特殊的索引,表的数据行的存储顺序和索引的顺序一致,是默认的索引类型
特点
-
物理存储顺序和索引顺序一致
- 聚簇索引将表中的数据按照索引列进行存储
- 数据行的物理存储顺序就是索引列的顺序
-
主键索引
- 表的主键索引会自动生成为聚簇索引
-
叶子节点存储数据
- 在聚簇索引的B+树结构中,叶子节点存储的是完整的数据行,不仅仅是索引键
- 通过聚簇索引找到对应数据行之后,不需要再次查询其他地方,数据就在叶子节点
- 在聚簇索引的B+树结构中,叶子节点存储的是完整的数据行,不仅仅是索引键
4、spring声明式事务的过程,失效场景有哪些
事务相关
- 声明式事务
- 简介
- 通过配置或者注解的方式声明事务边界,由Spring自动管理事务的开启、提交和回滚。
- 不需要显式编写管理事务的代码
- 具体实现逻辑是通过Spring AOP实现
- 特点
- 简洁性
- 非侵入性
- 不需要在代码中显式地调用事务管理代码,只需要在类或者方法上进行声明即可
- 基于AOP
- 基于AOP实现声明式事务管理,在方法调用前后动态加入事务处理逻辑
- 使用方式
- @Transactional
- 简介
- 非声明式事务(编程式事务)
- 简介
- 编程式事务,开发者显式编写代码管理事务的边界,
- 需要手动调用事务管理器,控制事务 的开发提交和回滚。
- 特点
- 显式控制
- 灵活性
- 使用方式
- 开发者通过 TransactionTemplate 或 PlatformTransactionManager 来管理事务的控制逻辑。
- 简介
声明式事务的失效场景有哪些?
-
方法可见性——只对public生效
- Spring AOP通过动态代理来管理事务,默认情况下只对public 生效
- Spring AOP通过动态代理来管理事务,默认情况下只对public 生效
-
内部方法调用
- 如果类中非事务方法调用了同类的另一个带有@Transactional的方法,事务不会生效
- 通过Spring AOP代理实现,内部方法调用不会触发代理逻辑
-
异常类型不匹配
- 默认情况下,声明式事务只会对运行时异常进行回滚,不会对受检异常进行回滚
- 如果在事务方法中,抛出了受检异常,而未进行特殊配置,事务不会回滚。
- 需要特殊处理,声明回滚的异常类型
- 使用final修饰的类
- Spring AOP基于JDK动态代理或CGLIB字节码生成机制来实现,如果某个方法或类被final修饰,CGLIB无法代理,导致事务失效。
- Spring AOP基于JDK动态代理或CGLIB字节码生成机制来实现,如果某个方法或类被final修饰,CGLIB无法代理,导致事务失效。
5、session和cookie简单介绍一下,有什么区别?
简介
- 都是用来维护用户状态和存储用户信息的两种机制的,主要是解决HTTP协议的无状态性
- 参考连接
Session
-
简介
- 在服务端保存用户的机制,用来跟踪用户在多个请求的状态
-
工作原理
- 服务器创建Session
- 当用户第一次访问服务器的时候,创建唯一的session对象,并将与用户相关的信息的存储在服务器端的session对象中。
- Session ID存储
- 服务端通过响应将Session ID发送给客户端,通过cookie传递
- 客户端保存session ID,通过cookie或者改写URL,发送给服务端
- 服务端存储信息
- 客户端发送请求时,服务器会根据对应Session ID找到对应session对象
- 服务器创建Session
-
特点
- 数据存储在服务端,安全
- 生命周期
- 默认是一次会话,当用户关闭浏览器或者Session超时时,Session会失效
Cookie
- 简介
- 存储在客户端用来保存用户信息和用户状态的小文件
- 由服务器发送并存储在客户端的数据
- 工作原理
- 服务端创建cookie
- 服务端在响应中包含set-cookie头部,并将这些数据以键值对的形式存储在客户端
- 客户端存储cookie
- 客户端浏览器将cookie保存起来,并在后续的每次请求中将cookie通过cookie头部发送给服务器
- 服务器读取cookie
- 服务器可以从请求中读取cookie,基于其中的一些信息做处理
- 服务端创建cookie
- 特点
- 数据存储在客户端
- 生命周期
- 小,不过4KB
- 安全性差
- 通过HTTPS加密传输和HTTPONLY(拒绝JS修改)
6、说明一下Spring AOP的实现以及具体调用方式
简介
- 将通用功能从业务逻辑中分离出来的方法,保持代码模块化的同时,实现功能的复用。
实现方式
-
基于JDK动态代理
- 要求被调用类必须实现一个接口,Spring会基于接口生成一个代理类,当代理的方法被调用时,AOP的逻辑会被织入
-
特点
- 基于接口实现,性能较高
- 只能代理实现接口的类
-
基于GCLIB代理
- 目标类没有实现任何接口,Sprint会通过CGLIB库创建目标类的子类,并对目标类的方法进行代理
-
特点
- 代理没有实现接口的类
- 使用字节码增强,性能低于JDK代理
AOP核心概念
-
切面(Aspect):切面是横切关注点的模块化封装,它将具体的功能逻辑封装到一个类中。切面由通知和切点组成。
-
通知(Advice):通知是实际要执行的功能逻辑,即在目标方法执行的特定时刻触发的操作。常见的通知类型有:
- 前置通知(Before):目标方法执行前调用。
- 后置通知(After):目标方法执行后调用,无论方法是否成功。
- 返回通知(AfterReturning):目标方法成功执行后调用。
- 异常通知(AfterThrowing):目标方法抛出异常时调用。
- 环绕通知(Around):目标方法执行前后都执行的通知,负责控制方法的执行。
- 切点(Pointcut):切点是定义在何处应用通知的表达式,可以是类、方法等位置。
-
连接点(JoinPoint):连接点是在程序执行过程中可能插入切面的具体位置,如方法调用等。
-
目标对象(Target Object):被AOP代理的目标类。
7、ArrayList和LinkedList两种List?
-
- 数据结构基础
- ArrayList:基于动态数组实现,连续的内存空间存储元素。
- LinkedList:基于双向链表实现,元素通过指针链接,内存空间不连续。
-
- 访问速度
- ArrayList:随机访问元素速度快,时间复杂度为 O(1),因为可以通过索引直接访问元素。
- LinkedList:随机访问元素速度慢,时间复杂度为 O(n),因为需要从头或尾部遍历链表。
-
- 插入和删除
- ArrayList:插入和删除操作较慢,特别是在中间位置操作时,时间复杂度为 O(n),因为需要移动大量元素。
- LinkedList:插入和删除操作较快,时间复杂度为 O(1),只需要调整节点指针,但需要先遍历到指定位置(遍历时间复杂度 O(n))。
-
- 内存占用
- ArrayList:因为是数组,除了元素本身不需要额外的内存;但数组的动态扩展需要分配新内存并复制数据。
- LinkedList:每个元素除了存储数据,还需要存储前驱和后继指针,导致内存开销比 ArrayList 高。
实际面经
1、Equals和hashcode两个的区别?重写的时候有什么需要注意?
关系
- 相同对象必须有相同的哈希值
- 两个对象的equals方法返回true,hashcode必须是一样
- 相同哈希值不一定保证对象相等
- 两个对象的hashcode相同,equals方法不一定返回trtue
这个说的是对的!
2、为什么String设计为final,设计为常量?(之前没有被这么问过,还是有点懵的)
1、不可变性带来的线程安全(提到了)
- String类一旦创建就不能被修改,多个线程可以同时共享一个String,然后无需担心数据竞争和同步的问题
2、优化内存使用(提到了)
- 创建一个String对象的时候,相同字符串已经在常量池中,Java就不会创建新的对象,返回池中已经有的对象。
- 保证了字符串的唯一性
3、安全性(没提到)
- String常用于存储敏感数据,可变的话会出现安全漏洞
4、设计间接性(没提到)
- String是一个基础的数据类型,广泛用于Java编程中各个方面,不可变的话,易于维护。
3、synchronized如何实现可重入锁(没被这样问过,只知道monitor实现的,照着ReentrantLock说了一下)
可重入锁的原理
- 当一个线程获取某个对象的锁后,再次进入该对象的synchronized代码块,不会阻塞。
- JVM内部为每一个锁为了一个计数器和拥有该锁的线程引用
- 计数器
- 记录线程获取锁的次数,归零锁才会被释放
- 拥有锁的线程引用
- 用于跟踪当前持有该锁的线程。
- 当线程尝试重新进入他已经持有的锁时,JVM会检查锁的持有者是否为当前线程
- 如果是,允许重新进入,计数器加1
- 计数器
这里就没有说拥有锁的线程引用,不过大差不差!
4、说一下zset的具体实现?查找元素排名的过程?跳表是如何查找元素的?
简介
- 有序集合,在集合中按照顺序存储和操作数据
ZSET的具体实现?
-
ZIPLIST
- 简介
- 当数据量较小并且每一个数据体积较小的时候,使用ziplist有序集合
- 通过有序插入实现对于元素的排序,并没有专门使用什么其他的算法
- 简介
-
SkipLIST + Hash
-
跳表
- 简介
- 分层数据结构,用来快速查找有序数据
- 通过在不同的层跳过一些元素,减少遍历的步骤,提高查找速度
- 时间复杂度时O(logN)
- 结构
- 指针
- 邻接指针 + 跳跃指针
- 排序字段
- 每一个元素都有一个score,根据的分数以logN的时间复杂度实现插入、删除和查找元素。
- 保存内容
- 分数score + 元素名称key
- 指针
- 简介
-
哈希表
- 简介
- 通过哈希表来存储ZSet中元素和分数之间的映射关系
- 通过成员名称快速找到分数
- 保存内容
- 元素名称key + 分数score
- 简介
-
适用场景
- 排行榜
- 数据筛选
-
-
查找元素排名过程
- 通过哈希表找到对应元素的score
- 通过哈希表,根据元素名称快速找到对应的分数
- 通过跳表找到对应score所在的位置
- 跳表中的元素是有序的,每一个元素包含多级索引
- 通过多级索引定位到特定元素之后,会计算该节点的路径上的节点数量来计算该元素的排名
- 通过哈希表找到对应元素的score
跳表的优势
-
实现简单
- 跳表的插入和删除操作比较简单直观
- 平衡树需要旋转和调整,实现再平衡
-
性能接近平衡树
- 时间复杂度都是logN,但是跳表有可能会退化为N,但是概率较低
-
适合范围查找
- 跳表是按照顺序存储的,可以直接按照最底层的链表前后进行查找
跳表查找元素的过程
-
从最高层开始,层级查找
- 从最高层开始从左往右,顺序遍历的节点,跳过大范围的元素
- 直到找到一个节点,比target小,后继节点比target大
-
逐层下降
- 在每一层,查找操作不断通过当前节点指向下一层的指针移动到下一层
- 直到找到最底层,就会在该层确定元素是否存在
-
结束条件
- 找到目标节点
- 或者在最底层,目标节点不存在
扩展:跳表、红黑树类和B树类
- 跳表
- 更加适合内存中的场景
- 适合范围查找,容易实现和维护
- 红黑树和AVL树
- 适合内存高效查找
- 严格保证时间复杂度的情况下优先
- B树
- 适合磁盘和数据库场景
- 每一个节点保存多个键
- 减少磁盘IO次数,适合大规模数据的存储和管理
5、Redis为什么这么快?(最重要的前三点都说了)
1、完全基于内存操作
- 所有操作都是基于内存的,访问速度快,不需要访问磁盘
- 持久化方式采用异步的方式,持久化到磁盘上
2、单线程事件驱动模型
-
单线程设计
- 减少上下文切换的开销
- 避免了多线程下的锁竞争问题
-
事件驱动模型
- 使用epoll和select等IO多路复用机制来处理大量的客户端链接
- 高效处理IO操作,单线程也能够处理大量并发请求
3、高效的数据结构
- 每一种数据类型都有针对性的专门优化
- String:INT和RAW
- List:ZIPLIST和QUICKLIST
- Hash:ZIPLIST和HASHTABLE
- Set和ZSet:SKIPLIST和HASHTABLE
4、针对小数据集的高效编码
- 针对小数据集使用了紧凑编码
6、讲讲io多路复用(这里记岔了,记成了零拷贝了,难受!)
简介
- 允许单个线程使用一个系统调用,监控多个IO事件
- 对应IO事件的文件描述符状态发生变化时,内核会通知应用程序,进行响应的IO操作
核心
- 允许在一个线程中高效处理多个IO通道,不需要为每一个IO操作都创建一个新的线程或者进程
常见的IO多路复用的机制
-
select
- 基本流程
- 程序将所有文件描述符的列表复制给内核
- 内核轮询检查每一个文件描述符,将有IO事件发生的文件描述符标记为可读可写
- 再将整个文件描述符号集合,拷贝会用户态,用户进行的遍历,找到目标的文件描述符
- 特点
- 文件描述符数量有上限
- 每次调用都需要复制传递整个文件描述符列表,效率低
- 基本流程
-
poll
- 基本流程
- 同上,但是使用更灵活的数据结构,没有数量限制
- 使用动态数据,以链表的形式组织
- 同上,但是使用更灵活的数据结构,没有数量限制
- 特点
- 两次传递所有文件描述符
- 需要遍历所有文件描述符列表
- 基本流程
-
epoll
- 基本流程
- 注册文件描述符符号
- 将需要监控的文件描述符注册到内核的红黑树中,
- 使用事件驱动代替遍历轮询
- 内核里维护一个链表来记录就绪事件
- 有事件发生的时候,通过回调函数,内核会将其加入到就绪事件列表中
- 后续返回事件发生的文件描述符
- 注册文件描述符符号
- 基本流程
IO多路复用的优势
- 处理大量并发链接
- 一个线程可以高效监控上百个网络链接,不需要为每一个网络连接创建一个线程或者进程
- 减少系统资源消耗
- 传统的多线程或者多进程模型需要为每一个链接创建一个线程或者进程,增大系统开销。
具体应用
- redis中使用epoll来监控所有客户端的网络链接
- 当有客户端发来请求时,epoll通知redis主线程处理该请求,
7、说一下redis的几种数据结构的具体应用场景?(这个说的也不好,当时很多没想起来,就算用过了,也没想起来)
String
- 缓存
- 缓存简单的键值对数据,用户信息、网页内容、配置数据等
- 计数器
- 支持原子性递增和递减操作
- 统计网站访问次数、点赞、库存数量等
- 分布式锁
Hash
- 存储对象信息
- 存储用户信息、商品信息等结构化的数据
- 字段计数
- 对单个字段进行递增或者递减操作,记录用户的经验值和积分等
List
- 消息队列
- 任务队列
- 分页数据
- 存储最近操作记录、日志文件等
Set
- 标签、分类管理
- 用户兴趣标签、商品类别、用户角色
- 社交关系
- 存储社交网络中的好友,关注者列表等
- 去重
- 统计独立访客的IP地址,过滤重复数据
ZSet
- 排行榜
- 事件排序
8、MySQL索引的具体实现结构(基本上都说出来了)
- 包括B+树、哈希索引、全文索引等
B+树索引
- 概要
- 平衡的多路搜索树
- 所有叶子节点在同一层,叶子节点按照顺序排列,内部节点只保存键和指针,不存储具体的数据信息
哈希索引
- 概要
- 哈希索引是基于哈希表实现的,索引列的值经过哈希函数计算后映射到对应的哈希桶中,再通过哈希桶指向存储数据的位置。
全文索引
- 概要
- 处理大量文本数据的索引结构,提高对文本字段进行全文索引的能力
- 实现
- 对待搜索的文本进行分词处理
- 创建反向索引并且加速关键词的索引
9、MySQL索引的作用?索引失效的几种场景(这个基本上都说了,唯一不足可能就是对于最左索引匹配表述不清楚)
索引的作用
- 加速查询
- 建立表中某些列的有序数据结构,减少需要扫描的行数,提高select查询速度
- 优化排序和分组操作
- 如果表中涉及order by和group by,索引能够避免数据库对结果进行额外的排序
索引失效的几种场景
-
在索引列上使用函数表达式
- 当在查询条件中,对索引列使用了函数或者运算符的时候,索引会失效
-
查询条件中有隐式类型转换
- 如果索引列的数据类型和查询条件的类型不匹配,MySQL会进行隐式类型转换,导致索引失效
- 这个举了一个例子,但是也没讲清楚,是String转int失效,还是int转String失效?
- age = ‘25’(String),String转为int会失效
- age(String) = 25,int转为String不会失效
-
Like以通配符%开头
- like “%12”这个肯定完蛋
- like “12%”这个可以成功
-
查询条件使用OR
- 在某些情况下,OR 关键字会导致索引失效,特别是当 OR 两边的列没有联合索引时,MySQL 会进行全表扫描。
-
前缀索引未被完全使用
- 如果使用的是联合索引a,b,c,查询条件没有使用最左边的列,直接使用c,就会索引失效
- 符合最左匹配原则
-
使用!=、<>、not in或者not exists等
- !=和<>都是不等于操作
- 不等于操作会导致查询结果不连续,不会使用索引
-
范围查询后无法使用索引
- 联合索引中,如果某个列使用了范围查询(<、>、between和LIKE),则该
总结一下
- 最左索引匹配原则
* - 范围模糊查询失效
- 函数运算失效
10、Java的多线程编程有几种方式?如何提交多线程的任务?(基本上都说出来了)
多线程编程的几种方式
-
继承Thread类
- 通过继承 Thread 类并重写 run() 方法实现多线程。每个 Thread 对象都代表一个独立的线程。
-
实现Runnable接口
- 实现 Runnable 接口,重写 run() 方法,然后将 Runnable 对象作为参数传递给 Thread 对象。
-
实现 Callable 接口
- Callable 是 Runnable 的增强版本,允许任务在完成后返回结果或抛出异常。
- 通常配合 Future 和 FutureTask 使用。
- Callable 是 Runnable 的增强版本,允许任务在完成后返回结果或抛出异常。
-
使用线程池(Executor 框架)
- Executor 框架是 Java 提供的线程池框架,支持创建和管理线程池,能够很好地解决频繁创建销毁线程的问题,提高性能。
如何提交多线程任务
-
直接启动 Thread 实例
- 创建 Thread 或 Runnable 实例后,直接调用 start() 方法启动线程执行。
-
使用 ExecutorService 提交任务
- ExecutorService 是 Executor 框架中的接口,可以用 submit() 或 execute() 方法提交任务。
- submit():
- 提交 Runnable 或 Callable 任务,并返回一个 Future 对象,可以用于获取任务执行结果或取消任务。
- execute():
- 仅用于提交 Runnable 任务,不会返回任务结果。
11、如何实现线程安全(基本上都说了)
- 使用 synchronized 关键字
- 使用 ReentrantLock
- 使用原子类(没提到)
- concurrent包中得atomic包提供的原子类,包括AtomicInteger、AtomicLong等
- 使用 volatile 关键字
- 使用 java.util.concurrent 包中的并发容器
- 使用线程安全的 Collections 工具类
- 使用线程局部变量(ThreadLocal)
12、Array List和Linkedlist的区别?(完全整理过了,基本上都能够回答出来)
-
- 数据结构基础
- ArrayList:基于动态数组实现,连续的内存空间存储元素。
- LinkedList:基于双向链表实现,元素通过指针链接,内存空间不连续。
-
- 访问速度
- ArrayList:随机访问元素速度快,时间复杂度为 O(1),因为可以通过索引直接访问元素。
- LinkedList:随机访问元素速度慢,时间复杂度为 O(n),因为需要从头或尾部遍历链表。
-
- 插入和删除
- ArrayList:插入和删除操作较慢,特别是在中间位置操作时,时间复杂度为 O(n),因为需要移动大量元素。
- LinkedList:插入和删除操作较快,时间复杂度为 O(1),只需要调整节点指针,但需要先遍历到指定位置(遍历时间复杂度 O(n))。
-
- 内存占用
- ArrayList:因为是数组,除了元素本身不需要额外的内存;但数组的动态扩展需要分配新内存并复制数据。
- LinkedList:每个元素除了存储数据,还需要存储前驱和后继指针,导致内存开销比 ArrayList 高。
13、ArrayList的扩容(这个完全说错了,没了解过这个,只知道redis里面的扩容,就照着那个说了)
扩容时机
- 添加元素,底层数组已经装满,size = capacity,触发扩容机制。
扩容机制
- 需要扩容时,会创建一个更大的新数组,大小为原数组的1.5倍
- 将旧数组的内容复制到新数组中,直接复制,并没有说渐进性的复制
扩容过程
- 计算新容量
- 新容量为当前容量的1.5倍
- 创建新数组
- 根据新容量创建一个新数组
- 复制旧数组中的数据到新数组中
- 使用System.arraycopy实现
- 更新ArrayList的引用
- 将ArrayList的内部数组引用指向新数组
总结
- 扩容的容量是1.5
- 非渐进式扩容,直接复制新的数据,使用System.arraycopy实现
14、Java中的输入输出流分为几种?(这个就说出来了字节流和字符流,但是具体的就不知道了,真尴尬)
基于流的IO
-
字节流
- 应用
- 处理二进制数据,非文本文件(图片、视频、音频等)
- 输入字节流InputStream
- FileInputStream、ByteArrayInputStream、BufferedInputStream
- 输出字节流OutputStream
- FileOutputStream、ByteArrayOutputStream、BufferedOutputStream
- 应用
-
字符流
-
应用
- 处理字符数据,通常用来处理文本文件
- 以字符char为单位进行操作
- 处理字符数据,通常用来处理文本文件
-
输入字符流Reader
- FileReader、BufferedReader、InputStreamReader
-
输出字符流Writer
- FileWriter、BufferedWriter、OuputStreamWriter
-
这里大概分类知道就行了,具体应用看这个之前整理的博客
- 跳转链接
总结
-
这个老师问的很详细,而且很多八股我都没有准备过,或者说回答的并不好,长经验了,不说什么了,继续准备面试吧!
-
以下几个问题回答的并不好,后续好好背一下
- ArrayList的扩容
- Java中输入输出流分为几种?
- IO多路复用相关
- synchronized锁的可重入性的实现
-
八股一段时间不看,就忘记了,或者说不知道怎么说了,每天早上还是得起来背一下,读一下,单纯图一个熟悉和顺口,保证后面的面试都能够顺顺利利!加油!
-
明天早上把这个东西好好整理一下!