字节跳动面试题
Java
1.ThreadLocal
ThreadLocal实现线程本地存储的功能,同一个ThreadLocal所包含的对象,在不同的Thread中有不同的实例,获取ThreadLocal对象时其实是在Thread类中的Map类型的threadLocals变量中通过ThreadLocal变量为键值进行获取。
2.volatile的作用和原理
被volatile修饰的变量保证Java内存模型中的可见性和有序性。
- 可见性:当一个线程修改了一个被volatile修饰的变量的值,新值会立即被刷新到主内存中,其他线程可以立即得知新值。
- 有序性:禁止进行指令重排序。
volaitle底层是通过内存屏障来实现可见性和有序性。内存屏障是一个CPU的指令,他的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性。内存屏障告诉编译器和CPU,不管什么指令都不能和这条内存屏障指令重排序,另一个作用是强制刷出各种CPU的缓存资源,因此任何CPU上的线程都能读取到这些数据的最新版本。
3.J.U.C中的锁
Java提供了两种锁机制来控制多个线程对共享资源的互斥访问。
实现 | 公平锁 | 等待可中断 | 条件 | 性能 | |
synchronized | JVM | 非公平 | 不可中断 | / | 大致 |
ReentrantLock | JDK | 非公平/公平 | 可中断 | 可绑定多个Condition | 相同 |
除非要使用ReentrantLock的高级功能,否则优先使用synchronized,synchronized是JVM实现的一种锁机制,JVM原生的支持它,而ReentrantLock不是所有的JDK版本都支持。synchronized锁释放由JVM保证,ReentrantLock需要显式的释放。
4.atomic包里的一些问题
atomic是使用volatile和CAS来实现的
5.HashMap的扩容
当HashMap的容量到达threshold时,需要进行动态扩容,将容量扩大为原来的两倍,然后将存储的数据进行rehash。
6.Semaphore信号量用来做什么?
Semaphore信号量类似于操作系统的信号量,可以控制对互斥资源的访问线程数。
7.Java内存模型
CPU和内存之间增加高速缓存。
所有的变量都存储在主内存中,每个线程有自己的工作内存,工作内存存储在高速缓存中,保存了该线程使用变量的拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
内存模型的三大特性:
- 原子性:Java内存模型保证了read、load、use、assign、store、write、lock、unlock操作具有原子性
- 实现:原子类、synchronized
- 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
- Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性
- 实现:volatile、synchronize、final
- 有序性:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。
- 实现:volatile、synchronized
8.Java内存空间是怎么分配的?
- 对象优先在Eden区分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态对象年龄判定
- 空间分配担保
8.Full GC触发条件
- System.gc()
- 老年代空间不足
- 空间分配担保失败
8.类加载机制
- 加载:1.通过类文件加载二进制字节流 2.在方法区创建类的动态存储结构 3.在内存中创建class对象作为方法去的访问入口。
- 验证:验证class文件的字节流是否符合虚拟机要求。
- 准备:为类变量分配内存并设置初始值。
- 解析:将常量池的符号引用替换为直接引用的过程。
- 初始化:执行Java程序代码。
8.新生代和老年代可以转换吗?
对象优先分配在新生代的Eden区,通过长期存活(达到一定岁数)的对象进入老年代和动态对象年龄判定使对象从新生代进入老年代。
9.这些内存里面的垃圾怎么回收?
引用计数法和可达性分析法。回收算法包括:标记-清除、标记-整理、复制、分代收集算法。
10.怎么判断是垃圾?GCRoot可以为哪些?
可达性分析法中,从GC Root出发,不可达的是可以被回收的对象。
- Java虚拟机栈局部变量表中引用对象。
- 本地方法栈JNI中引用的对象。
- 方法区中类静态变量引用的对象。
- 方法去中常量引用的对象。
11.G1收集器
垃圾收集器都存在 Stop The World 的问题,G1对这个问题进行了优化,G1对整个新生代和老年代一起回收,把堆划分为多个大小相等的独立区域region,使得每个region可以单独进行垃圾回收,通过记录每个region垃圾回收时间以及回收所获得的空间(通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region。
初始标记 -> 并发标记 -> 最终标记 -> 筛选回收
特点:
- 空间整合:基于标记-整理和复制,不会产生内存空间碎片
- 可预测的停顿:也可以并发执行
8.BIO、NIO、AIO
BIO,同步阻塞IO,一个线程处理一个连接,发起和处理IO请求都是同步的
NIO,同步非阻塞IO,一个线程处理多个链接,发起IO请求是非阻塞的,处理IO请求是同步的(轮询)
AIO,异步非阻塞IO,一个有效请求一个线程,发起和处理IO请求都是异步的。
9.AQS
描述 | |
FutureTask | 用来封装Callable的返回值 |
BlockingQueue | 当队列中没有元素时take()被阻塞,当队列满时put()被阻塞 |
ForkJoin | 大的计算任务拆分成小任务,并行计算 |
10.JUC
描述 CountDownLatch countDown()会使计数器减1,当计数器为0时,调用await()的线程会被唤醒 CyclicBarrier await()会使计数器减1,当计数器为0时,所有调用await()的方法会被唤醒 Semaphore 类似于操作系统的信号量,可以控制对互斥资源的访问线程数
11.实现线程安全的方法
- 不可变
- synchronized和ReentrantLock
- CAS、AtomicInteger
- TreadLocal
12.IO与NIO
I/O | NIO | |
数据打包和传输方式 | 流 | 块 |
是否阻塞 | 阻塞 | 非阻塞 |
13.NIO
- 通道(Channel):对原I/O包中的流的模拟,可以通过它读取和写入数据,流是单向的,通道是双向的,可以同时用于读、写或者同时用于读写。
- 缓冲区:不会直接对通道进行读写数据,而是要先经过缓冲区。
- 选择器(Selector):在Socket NIO用于IO复用。
14.Class.forName()怎么执行的?
15.守护线程是什么?守护线程是怎么退出的?
守护线程是在程序运行时提供后台服务的线程,不属于程序运行中不可或缺的部分。
当程序中所有非守护线程结束时,程序也就终止,同时杀死所有的守护线程。
16.Stack与ArrayList的区别
Stack是用Vector实现的,Queue是用ArrayList实现的,所以比较Stack与ArrayList的区别就是比较这两者之间的区别。
- 一个先进先出,一个后进先出
- 一个线程不安全,一个线程安全
17.HashMap的rehash过程
HashMap中使用一个技巧,和将哈希值与旧容量进行&运算,如果位上为0则在原位置,如果为1则在下边。
18.hashcode和equals的区别
equals用来判断实体在逻辑上是否相等,当重写equals方法时要重写hashcode方法。
- 如果两个对象通过equals判定相等,则hashcode相等。
- hashcode相等,equals不一定相等。
19.equals和==的区别?我要比较内容呢?
- equals:用来比较逻辑上是否相等
- ==:用来判断两个对象地址是否相同,即是否是同一个对象。
20.Java代码编译过程
词法分析 -> 语法分析 -> 语义分析 -> 字节码生成
21.如何设计hash函数
22.常用的线程池
23.分段锁
JVM
1.运行时数据区域
程序计数器 | JVM栈 | 本地方法栈 | 堆 | 方法区 | 运行时常量池 | |
功能 | 记录正在执行的虚拟机字节码指令的地址 | 栈帧用于存储局部变量表、操作数栈、常量池引用等信息 | 与JVM栈类似,为本地方法服务 | 对象分配区域,垃圾收集的主要区域 | 用于存访加载的类信息、常量、静态变量、即时编译器编译后的代码 | 方法区的一部分,存放生成的字面量和符号引用 |
线程私有 | 线程私有 | 线程私有 | 公有 | 公有 | 公有 | |
垃圾收集 | 不需要 | 不需要 | 不需要 | 需要(垃圾回收的主要区域) |
类的卸载:1.类实例被回收2.加载类的classloader被回收3.class对象没有被引用 方法区在jdk1.8以前放在永久代中,jdk1.8以后放在本地内存中,而不是JVM内存 |
需要 |
2.垃圾收集算法
新生代 | 老年代 | |||
垃圾收集算法 | 复制(Eden:Survivor) | 标记-清除/标记-整理 | ||
GC | Minor GC | Full GC | ||
触发条件 | Eden空间满时 |
1.调用System.gc() 2.老年代空间不足 3.空间分配担保失败 |
||
3.类加载过程:
- 加载:从各种渠道获取二进制字节流转化为方法区的运行时存储结构,在内存中生成一个class对象作为访问入口。
- 验证:确保字节流符合当前虚拟机的要求
- 准备:为类变量分配内存并设置初始值
- 解析:将常量池的符号引用替换为直接引用的过程
- 初始化:虚拟机执行类构造器clinit方法的过程。<clinit>()是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的
4.引用类型
描述 | |
强引用 | 不会被回收 |
软引用 | 只有在内存不够的情况下才会被回收 |
弱引用 | 一定会被回收,只能存活到下一次垃圾回收发生之前 |
虚引用 | 不会对其生存时间造成影响,唯一目的是在这个对象被回收时收到一个系统通知 |
5.垃圾收集算法
描述 | 不足 | |
标记-清除 | 标记要收集的对象,然后清除 | 标记和清除效率都不高,造成内存碎片 |
标记-整理 | 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存 | 对标记-清除算法的补充 |
复制 | 将内存划分为相等的两块,每次只使用其中的一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后把使用过的内存空间进行一次清理 | 只使用了内存的一半 |
分代收集 |
他根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 一般将堆分为新生代和老年代。
|
6.垃圾收集器
多线程与单线程 | 串行与并行 | 描述 | 适用场景 | |
Serial收集器 | 单线程 | 串行 | 简单高效 | Client |
ParNew收集器 | 多线程 | 串行 | Serial的多线程版本 | Server |
Parallel Scavenge收集器 | 多线程 | 串行 | 动态调整以提供最合适的停顿时间或者最大吞吐量 | 注重吞吐量以及CPU资源敏感场合 |
Serial Old收集器 | 单线程 | 串行 | Serial的老年代版本 | Client |
Parallel Old收集器 | 多线程 | 串行 | Parallel Scavenge的老年代版本 | 注重吞吐量以及CPU资源敏感场合 |
CMS收集器 | 多线程 | 串行/并行 |
吞吐量低 无法处理浮动垃圾 标记-清除导致碎片 |
|
G1收集器 | 多线程 | 串行/并行 |
空间整合:基于 标记-整理 可预测的停顿 |
7.内存分配与回收
描述 | 特点 | 触发条件 | ||
Minor GC | 回收新生代 | 频繁,快 | Eden区空间不够时 | |
Full GC | 回收老年代和新生代 | 少,慢 |
1. System.gc() 2. 老年代不足(大对象、长期存活的对象进入老年代) 3. 空间分配担保失败 |
内存分配策略
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代:年龄计数器
- 动态对象年龄判定
- 空间分配担保
计算机网络
1.简述TCP的三次握手、四次挥手,为什么要三次握手?为什么client会进入TIME_WAIT?
TCP的三次握手:
三次握手过程中主要对序号(seq)、确认序号(ack)、标志位(ACK、SYN)进行操作。
(1)client端发送连接请求:SYN=1(建立新连接),seq=x
(2)server端接收请求并返回确认报文:SYN=1(建立新连接),ACK=1(ack有效),ack=x+1,seq=y
(3)client接收到确认报文,再次发送确认消息:ACK=1(ack有效),seq=x+1(client上一条请求seq+1),ack=y+1
(4)server端收到确认后,连接建立
TCP的四次挥手:
(1)client端发送连接释放报文:FIN=1,seq=u
(2)server收到之后发出确认,此时TCP属于半关闭状态,server能向client发送数据反之不能:ACK=1,seq=v ack=u+1
(3)当server处理完毕后,发送连接释放报文:FIN=1,ACK=1,seq=w,ack=u+1
(4)client收到后发出确认,进入TIME-WAIT状态,等来2MSL(最大报文存活时间)后释放连接:ACK=1,seq=u+1,ack=w+1
(5)server收到client的确认后释放连接
为什么要进行三次握手?
第三次握手时为了防止失效的连接请求到达服务器,让服务器错误打开连接。
客户端发送的连接请求如果在网络中滞留,那么就会隔很长时间才能收到服务器的连接确认。客户端等待一个超时重传时间后,就会重新发起请求。但是这个滞留的连接请求最后还是会到达服务器,如果不进行第三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
为什么会有TIME_WAIT?
客户端接收到服务器的FIN报文后进入TIME_WAIT状态而不是CLOSED,还需要等待2MSL,理由:
确保最后一个确认报文能够到达。如果server端没收到client端发来的确认报文,那么就会重新发送连接释放请求报文。
为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
2.TCP的拥塞控制
慢开始:最初,发送方只能发送一个报文段(假设),当收到确认后,将拥塞窗口(cwnd)加倍,呈指数型增长
拥塞避免:设置一个慢开始门限ssthresh,当cwnd>=ssthresh,进入拥塞避免,每个轮次只将cwnd加1
快重传:在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到M1和M2,此时收到M4,应该发送对M2的确认。在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。
快恢复:在这种情况下,只是丢失个别报文段,不是网络拥塞,因此执行快恢复,令ssthresh=cwnd/2,cwnd=ssthresh,此时直接进入拥塞避免。
3.浏览器输入url请求服务器的过程,分析其中哪些部分用到缓存。
输入url
浏览器查找浏览器缓存
若浏览器缓存中未找到,查找本机host文件
若本机host文件中未找到,则查找路由器、ISP缓存
若路由器、ISP缓存中未找到,则向配置的DNS服务器发起请求查找(若本地域名服务器未找到,会向根域名服务器->顶级域名服务器->主域名服务器)
获取到url对应的ip后,发起TCP三次握手
发送http请求,将响应显示在浏览器页面中
四次挥手结束
4.ARP(地址解析协议)
ARP实现由IP地址得到MAC地址。
主机A知道主机B的IP地址,但是ARP高速缓存中没有该IP地址到MAC地址的映射,此时主机A通过广播的方式发送ARP请求分组,主机B收到该请求后会发送ARP响应分组给主机A告知其MAC地址,随后主机A向其高速缓存中写入主机B的IP地址到MAC地址的映射。
5.HTTP的流量控制,具体的控制算法
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。
6.计算机网络体系结构
设备 | 地址 | 通信方式 | 数据单位 | 协议 | 描述 | ||
应用层 | 报文 | HTTP、DNS、FTP、DHCP、SMTP(邮件发送)、POP3和IMAP(邮件接收) | 为特定应用程序提供数据传输服务 | ||||
传输层 | 报文段 |
TCP
|
为进程提供数据传输服务 | ||||
用户数据报 |
UDP
|
||||||
网络层 |
路由器(路由选择和分组转发) 路由协议选择:RIP/OSPF(内部) BGP(外部) |
IP地址 | 分组 |
IP协议(分类、子网划分、无分类) NAT:将本地IP转换为全球IP |
为主机提供数据传输服务 | ||
地址解析协议(ARP):由IP地址得到MAC地址 |
|||||||
网际控制报文协议(ICMP):封装在IP数据包中,但不属于高层协议,是为了更有效地转发IP数据包和提高交付成功的机会。 |
|||||||
网际组管理协议(IGMP) |
|||||||
数据链路层 | 交换机(自学习交换表:MAC地址到接口的映射) | 一台主机有多少个网络适配器就有多少个MAC地址 | 广播信道(星型、环形、直线型) | 帧 | 信道复用技术 |
频分复用 时分复用 统计时分复用 波分复用 码分复用 |
为同一链路的主机提供数据传输服务 |
点对点信道 | CSMA/CD | ||||||
物理层 | 集线器 |
单工通信 半双工通信 全双工通信 |
比特 | 在传输媒体上传输数据比特流 |
在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。
7.路由选择协议
RIP |
OSPF | BGP | |
名称 | 开放最短路径优先 | 边界网关协议 | |
使用范围 | 内部 | 内部 | 外部 |
描述 | 基于距离向量的路由选择协议 | 洪泛法 | 每个自治系统必须配置BGP发言人,发言人之间通过TCP连接来交换路由信息 |
特点 |
实现简单开销小 最大距离为15,限制了网络规模 故障传播时间长 |
更新过程收敛快 | 只能寻找一条比较好的路由,而不是最佳路由 |
8.UDP和TCP比较
UDP | TCP | |
连接 | 无连接 | 面向连接 |
可靠 | 尽最大能力交付 | 可靠交付 |
拥塞控制 | 无 | 有 |
面向 | 面向报文 | 面向字节流 |
通信 | 一对一、一对多、多对一和多对多 | 一对一,全双工通信 |
HTTP
1.HTTP的过程
类似于浏览器输入url请求服务器的过程?
2.HTTPS怎么建立请求
HTTPS = HTTP + SSL(Secure Sockets Layer, 安全套接字层)
HTTPS 可以防窃听(非对称密钥加密)、防伪装、防篡改(加密和认证)
客户端发送请求到服务器端
服务器端返回证书和公开密钥,公开密钥作为证书的一部分而存在
客户端验证证书和公开密钥的有效性,如果有效,则生成共享密钥并使用公开密钥加密发送到服务器端
服务器端使用私有密钥解密数据,并使用收到的共享密钥加密数据,发送到客户端
客户端使用共享密钥解密数据
SSL加密建立...
3.GET和POST比较
GET | POST | |
作用 | 获取资源 | 传输实体 |
参数 | 查询字符串 | request body |
安全(不会改变服务器状态) | 安全 | 不安全 |
幂等性 | 满足 | 不满足 |
缓存 | 可缓存 | 多数情况下不可以 |
MySQL
1.mysql的索引,最左匹配原则
索引可以加快对数据的检索。常见的有B+Tree索引,哈希索引。
最左匹配原则:
当索引是联合索引,在查询条件中,mysql是从最左边开始命中的,如果出现了范围查询(>、<、between、like),就不能进一步命中了,后续退化为线性查找,列的排列顺序决定了可命中索引的列数。
2.mysql的主从复制
mysql为了保持高可用,会采用一主多从的结构,一个master节点,多个slave节点,master节点可以进行写操作,而slave节点只能进行读操作。
binlog线程:将主服务器上的数据更改写入二进制日志中
I/O线程:从主服务器上读取二进制日志,并写入从服务器的重放日志中
SQL线程:读取重放日志并重放其中的SQL语句
3.mysql的聚集索引、非聚集索引
聚集索引:以主键创建的索引,在叶子结点上存储的是表中的数据
非聚集索引:以非主键创建的索引,叶子结点上存储的是主键和索引列
使用非聚集索引查询出数据时,拿到叶子上的主键再去查到想要查找的数据。(回表)
4.mysql联合索引,要注意什么?
联合索引即索引由多个列(a,b,c,d)组成,要注意索引的命中,最左匹配原则,从左开始命中,遇到范围查询就不能进一步匹配。
5.为什么数据库要使用B+树来实现索引?
更少的查找次数(B+树相比红黑树更矮胖)
利用磁盘预读特性(一次IO能完全载入一个节点)
6.MySQL索引
描述 | 特点 | 使用场景 | |
B+ Tree索引 |
使用B+ Tree作为底层实现 |
对树进行搜索,查找速度快 分为聚簇索引和非聚簇索引 |
查找、排序、分组 |
哈希索引 |
使用哈希作为底层实现 |
无法用于排序与分组 只支持精确查找,时间复杂度为O(1) |
当索引值使用的频繁时,会在B+ Tree索引之上再创建一个哈希索引 |
全文索引 | 全文索引使用倒排索引实现,记录着关键词到其所在文档的映射 | 查找文本中的关键词 | |
空间数据索引 | 从所有维度来索引数据 | 用于地理数据存储 |
索引优化:
- 独立的列:索引列不能是表达式的一部分,也不能是函数的参数。
- 多列索引:多个列为条件查询时,使用多列索引。
- 索引的顺序:让选择性最强的索引放在最前面。
- 前缀索引:对于BLOB、TEXT、VARCHAR类型的列,必须使用前缀索引,只索引开始的部分字符。
- 覆盖索引:索引包含所有需要查询的字段的值。
索引的优点:
- 大大减小了服务器需要扫描的行数。
- 帮助服务器避免排序和分组。
- 将随机I/O变为顺序I/O。
7.InnoDB和MyISAM比较
InnoDB | MyISAM | |
默认 | 是 | 否 |
隔离级别 | 四个隔离级别 | |
事务 | 支持 | 不支持 |
锁 | 行级/表级 | 表级 |
外键 | 支持 | 不支持 |
备份 | 在线热备份 | |
崩溃恢复 | 概率高,恢复慢 | |
特性 | 压缩表和空间数据索引 | |
使用场景 | 读写分离的读表 |
8.切分
- 水平切分:将同一个表中的记录拆分到多个结构相同的表中。
- 切分策略:
- 哈希取模
- 范围:ID或者时间
- 映射表:使用单独的一个数据库来存储映射关系
- 切分策略:
- 垂直切分:将一个表按列切分成多个表,通常按关系紧密度或者使用频率来切分。
9.MySQL数据库是怎么插入的?
10.事务怎么回滚?里面有什么日志?
11.一百万条数据记录,如何分页显示最后一条?
设一个列从1开始自增,并设为索引,以这个列为条件进行查询。
12.数据库事务隔离级别,可重复度和可串行化实现的原理
隔离级别:读未提交、读已提交、可重复度、串行化
- 可重复度:MVCC(多版本并发控制)
- 串行化:MVCC + Next-Key Locks(Record Locks(锁定索引) + Gap Locks(锁定索引间的间隙))
数据库
1.数据库并发一致性问题
数据库并发一致性问题是由于隔离性导致的。
- 丢失修改:新的修改覆盖了老的修改。
- 读脏数据:读取其他线程rollback了的数据。
- 不可重复读:数据的值被修改。
- 幻影读:整条数据的插入和删除。
2.封锁
- 封锁粒度:表级锁 行级锁
- 封锁类型:读写锁 意向锁
- 封锁协议:三级封锁协议 两段锁协议
3.多版本并发控制
基于 | 描述 | |
系统版本号 | 系统 | 没开始一个事务,系统版本号+1 |
事务版本号 | 事务 | 事务开始时的系统版本号 |
创建版本号 | 行数据 | 数据创建时的系统版本号 |
删除版本号 | 行数据 | 数据删除时的系统版本号 |
4.异常和数据库范式
描述 | |
1NF | 属性不可分 |
2NF | 每个非主属性完全依赖于键码 |
3NF | 每个非主属性不传递函数依赖于键码 |
5.连接
关键字 | 描述 | |
内链接 | INNER JOIN | 等值连接 |
自连接 | INNER JOIN | 自己连接自己 |
自然连接 | MATURAL JOIN | 所有同名列的等值连接 |
外连接 | LEFT OUTER JOIN | 保留左表没有关联的行 |
RIGHT OUTER JOIN | 保留右表没有关联的行 | |
OUTER JOIN | 保留所有没有关联的行 |
数据结构
1.B+树和B树的区别
B+树的数据都在叶子结点上,而B树的非根非叶节点也是数据节点,所以B+树的查询更稳定。
B+树有两个指针,一个指向root节点,一个指向叶子节点的最左侧,因此其范围查询效率更高,而B树则需要中序遍历B树。
同阶的情况下,B+树节点中的关键字要比B树多一个,并且B+树的中间节点不保存数据,所以磁盘也能够容纳更多结点元素,因此B+树更加矮胖,查询效率也更高。
2.红黑树
红黑树是一个自平衡二叉查找树。时间复杂度O(log n)
- 节点颜色为红或者黑
- 根结点是黑色
- 叶节点(NIL结点,空结点)为黑
- 红节点的孩子为黑(路径上不能有两个连续的红节点)
- 从根到叶子节点路径中的黑节点数相等
3.红黑树和平衡二叉树的区别
平衡二叉树和高度相关,保持平衡的代价更高(多次旋转),因此适用于插入、删除较少,查询较多的场景。
红黑树和高度无关,旋转次数较少,因此适用于插入、删除较多的场景。
框架
1.Mybatis动态代理
2.Spring IOC是什么?怎么实现的?
3.Spring IOC里面的反射机制怎么实现的?
Redis
1.redis分片,客户端请求怎么处理?
Redis的分片是指将数据分散到多个Redis实例中的方法,分片之后,每个redis拥有一部分原数据集的子集。在数据量非常大时,分片能将数据量分散到若干主机的redis实例上,进而减轻单台redis实例的压力。
- 范围分片
- 哈希分片
分片的位置:
- 客户端分片
- 代理分片
- 服务器分片
2.redis的zset底层实现
跳跃表来实现。
跳跃表相比于红黑树的优点:
- 存取速度快,节点不需要进行旋转
- 易于实现
- 支持无锁操作
3.redis和mysql的区别
- redis是key-value非关系型数据库,MySQL是关系型数据库
- redis基于内存,也可持久化,MySQL是基于磁盘
- redis读写比MySQL快的多
- redis一般用于热点数据的缓存,MySQL是存储
4.redis加锁
redis为单进程单线程模式,采用队列模式将并发访问变为串行访问,redis本身没有锁的概念,但可以用redis实现分布式锁。
- INCR
Redis Incr 命令将 key 中储存的数字值增一。 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
- SETNX
- SET:以上两种都没有设置超时时间,SET可以实现超时时间
分布式锁的核心思想是将设置锁和超时时间、删除锁分别作为一个原子操作进行。
5.redis的淘汰策略
- volatile-lru:在设置超时时间的数据中进行lru
- volatile-ttl:在设置超时时间的数据中挑选即将过期
- volatile-random:在设置超时时间的数据中随机挑选
- allkeys-lru:所有数据的lru
- allkeys-random:所有数据的随机
- noeviction:禁止驱逐数据
6.redis无法被命中怎么办?会出现什么问题?
无法被命中:无法直接通过缓存得到想要的数据
解决方案:
- 缓存尽可能聚焦在高频访问且时效性不高的业务热点上。
- 将缓存容量设置为热点数据的容量。
- 缓存预加载。
- 设置合适的缓存淘汰策略。
7.Redis和MySQL复制和分片
复制 | 分片 | |
MySQL | 三个线程(binlog线程、I/O线程、SQL线程),目的是实现读写分离 | 水平切分、垂直切分 |
Redis | 使用RDB快照进行复制,发送期间使用缓冲区记录执行的写命令,在RDB快照发送完毕后,发送缓冲区中的写命令 | 水平切分 |
8.Redis是什么?Sorted List是什么?skiplist是什么?怎么实现的?怎么插入一个值?怎么进行查询?和其他数据结构进行对比?
9.Redis的hash和Java的map的区别
消息队列
JVM
1.四种引用类型
- 强引用:如用new关键字创建,不会进行回收。
- 软引用:在内存不足的情况下会进行回收。
- 弱引用:只能存活到下一次垃圾回收。
- 虚引用:不影响其生存周期,只是在回收的时候收到一个系统通知。
2.可达性分析算法的root
可达性分析算法是从GC root出发,只有通过GC root可达的对象是被引用的对象,不可达的对象属于可以回收的对象。
操作系统
1.进程和线程的区别
- 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
- 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程,会引起进程切换。
- 系统开销:创建、撤销或切换进程,系统都要为之分配、回收资源或保存环境,开销远比线程大。
- 通信方面:线程间可以通过直接读取统一进程中的数据进行通信,但是进程通信需要借助IPC。
主线程是什么?
2.操作系统的内存管理
- 分页地址映射:分页是一种动态重定位技术,通过页表将逻辑地址映射为物理地址。
- 段式存储:分页有一个不可避免的问题就是用户视角的内存和实际物理内存的分离,分段则是将逻辑地址空间分成一组段,每个段长度可以不同,并且可以动态增长,由段表来维护。
- 段页式存储:段内分页。
3.分页式的页表放在哪
进程控制块(PCB)中。
4.进程的PCB里还有哪些东西?
- 进程状态
- 程序计数器
- CPU寄存器
- CPU调度信息
- 内存管理信息
- 记账信息
- I/O状态信息
5.MMU(内存管理单元)
内存管理单元(MMU)管理着地址空间和物理内存的转换,根据其内存管理方式的不同,其中包括基地址寄存器、界限地址寄存器的值以及段表和页表。
6.进程通信
- 管道(父子进程间通信)
- 命名管道FIFO(去除管道只能在父子进程间进行通信,常用于客户-服务器应用程序中)
- 信号量
- 消息队列
- 共享内存(生产者消费者的缓冲池)
- 套接字(可用于不同机器间的进程通信)
7.共享内存
采用共享内存的进程间通信需要通信进程建立共享内存区域。通常一块共享内存区域驻留在生成共享内存段的进程的地址空间。需要使用信号量用来同步对通向存储的访问。
8.Inode
9.应用程序是如何读取文件的?
LINUX
1.linux脚本,杀掉包含一个关键字的所有进程
ps -ef | grep 关键字 | awk '{print $2}' | xargs kill -9
2.自旋锁和互斥锁
都属于linux内核中的内核锁。
互斥锁通过对共享资源的锁定和互斥解决利用资源冲突问题,互斥锁是选择睡眠的方式来对共享工作停止访问的。
自旋锁不会引起调度者睡眠,而是一直循环。
Socket
1.linux I/O模型,说说select和epoll的区别
- Socket等待数据到达
- 复制到内核缓冲区中
- 从内核缓冲区复制到应用进程缓冲区中
描述 | 特点 | |
阻塞式I/O | 应用进程被阻塞,知道数据从内核缓冲区复制到应用进程缓冲区才返回 | 阻塞期间,其他进程继续执行,CPU利用率高 |
非阻塞式I/O | 轮询I/O是否完成 | 多次执行系统调用,CPU利用率低 |
I/O复用 | select poll epoll | 单个线程具有处理多个I/O时间的能力 |
信号驱动I/O | 执行系统调用后立即返回,内核在数据到达时向应用进程发送SIGIO信号,应用进程收到后将数据从内核复制到应用进程 | CPU利用率比非阻塞I/O高 |
异步I/O | 系统调用立即返回,不会阻塞,内核完成所有操作后向应用进程发送信号 |
异步I/O通知应用进程I/O完成 信号驱动I/O是通知应用进程可以开始I/O |
select | poll | epoll | |
timeout精度 | 1ns | 1ms | 1ms |
描述符数据结构 | 数组(因此有最大限制) | 链表 | 链表 |
复制描述符 | 每次调用都需要复制 | 每次调用都需要复制 | 第一次复制、修改 |
返回结果 | 不返回准备好的描述符 | 不返回准备好的描述符 | 准备好的描述符加入到一个链表中管理 |
支持 | 几乎所有系统 | 较新的系统 | Linux系统 |
适用场景 | 实时性高,所有平台 | 实时性低,描述符适中 | 描述符多,描述符变化小 |
select和epoll的区别
- select、poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次(在epoll_waitd的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。
- select、poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll是在设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进行睡眠的进程。select和poll要遍历整个fd集合,epoll只要判断一下就绪链表是否为空就行了。
2.多路复用模型
分布式
1.分布式事务
CAP定理:
- 一致性(Consistency):多个数据副本是否能保持一致的特性。
- 可用性(Availability):分布式系统在面对各种异常时可以提供正常服务的能力。
- 分区容忍性(Partition tolerance):分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务。
在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP理论实际上是要在可用性和一致性做权衡。
BASE:
BASE是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。
BASE理论是对CAP中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
ACID要求强一致性,通常运用在传统的数据库系统上。而BASE要求最终一致性,通过牺牲强一致性来达到可用性,通常运用于大型分布式系统中。
解决方案:
(1)两阶段提交(2PC)
基于XA协议实现
存在问题:1.同步阻塞 2.单点问题 3.数据不一致 4.太过保守
(2)TCC
针对每个操作,都要注册一个与其对应的确认和补偿操作
Try/Confirm/Cancel
(3)本地消息表(异步确保)
将业务操作和本地消息表放在一个事务中。业界使用最多。
(4)Sagas事务模型
事件溯源,相当于将分布式系统中的操作都记录到数据库日志表中,要获得最新的状态,则需要重新执行一遍日志表的操作。并且可以dump某一时刻的数据表,在此基础上执行在这之后的操作。
手写代码
1.二叉树的先序遍历,层序遍历的实现
private static class Node{ int value; Node left; Node right; public Node(int value) { this.value = value; } } /** * 先序遍历非递归版 * @param root */ public void preOrder1(Node root){ if(root == null) return; Stack<Node> stack = new Stack<>(); stack.push(root); while(!stack.isEmpty()){ Node node = stack.pop(); System.out.println(node.value); if(node.right != null) stack.push(node.right); if(node.left != null) stack.push(node.left); } } /** * 先序遍历非递归版回溯法 * @param root */ public void preOrderBack(Node root){ if(root == null) return; Stack<Node> stack = new Stack<>(); while(root != null || !stack.isEmpty()){ if(root != null){ System.out.println(root.value); stack.push(root); root = root.left; }else{ Node upNode = stack.pop(); root = upNode.right; } } } public void preOrderTrans(Node root){ if(root == null) return; System.out.println(root.value); preOrderTrans(root.left); preOrderTrans(root.right); } public void bfs(Node root){ Queue<Node> queue = new LinkedList<>(); queue.add(root); while(!queue.isEmpty()){ Node top = queue.remove(); System.out.println(top.value); if(top.left != null) queue.add(top.left); if(top.right != null) queue.add(top.right); } }
2.用栈实现队列
import java.util.Stack; public class Queue { private Stack<Integer> stack1 = new Stack<>(); private Stack<Integer> stack2 = new Stack<>(); public void push(Integer value){ if(value == null){ throw new RuntimeException("value is null"); } stack1.push(value); } public Integer pop(){ if(stack1.size() == 0){ return null; } while(!stack1.empty()){ stack2.push(stack1.pop()); } Integer value = stack2.pop(); while(!stack2.empty()){ stack1.push(stack2.pop()); } return value; } public static void main(String[] args) { Queue queue = new Queue(); queue.push(1); queue.push(3); queue.push(5); queue.pop(); queue.pop(); queue.pop(); } }
3.包括max函数的栈
import java.util.Stack; public class StackMax { private Stack<Integer> stack = new Stack<>(); private Stack<Integer> s = new Stack<>(); public void push(Integer value) { stack.push(value); if (s.size() == 0 || value >= s.peek()) { s.push(value); } } public Integer pop() { Integer value = stack.pop(); if(value.equals(s.peek())){ s.pop(); } return value; } public Integer max() { return s.peek(); } public static void main(String[] args) { StackMax stackMax = new StackMax(); stackMax.push(1); System.out.println(stackMax.max()); stackMax.push(3); System.out.println(stackMax.max()); stackMax.push(4); System.out.println(stackMax.max()); stackMax.push(2); System.out.println(stackMax.max()); stackMax.pop(); System.out.println(stackMax.max()); stackMax.pop(); System.out.println(stackMax.max()); stackMax.pop(); System.out.println(stackMax.max()); } }
4.找一个n*n矩阵的最长上升序列
5.快速排序,什么时候复杂度最大
public void quickSort(int[] num, int st, int ed) { if (st >= ed) return; int left = st; int right = ed; int value = num[left]; while (left < right) { while(left < right && num[right] >= value){ right--; } num[left] = num[right]; while(left < right && num[left] < value){ left++; } num[right] = num[left]; } int mid = left; num[mid] = value; quickSort(num, st, mid - 1); quickSort(num, mid + 1, ed); } public static void main(String[] args) { QuickSort quickSort = new QuickSort(); int[] num = {3, 7, 4, 2, 5, 8, 1}; quickSort.quickSort(num, 0, 6); for (int t : num) { System.out.println(t); } }
6.归并排序
import java.util.Arrays; public class MergeSort { public int[] mergeSort(int[] num) { if (num.length <= 1) return num; int mid = num.length / 2; int[] left = Arrays.copyOfRange(num, 0, mid); int[] right = Arrays.copyOfRange(num, mid, num.length); return mergeArrays(mergeSort(left), mergeSort(right)); } private int[] mergeArrays(int[] mergeSort1, int[] mergeSort2) { int[] result = new int[mergeSort1.length + mergeSort2.length]; int i = 0, j = 0, k = 0; while (i < mergeSort1.length && j < mergeSort2.length) { if (mergeSort1[i] < mergeSort2[j]) { result[k++] = mergeSort1[i++]; } else { result[k++] = mergeSort2[j++]; } } while (i < mergeSort1.length) { result[k++] = mergeSort1[i++]; } while (j < mergeSort2.length) { result[k++] = mergeSort2[j++]; } return result; } public static void main(String[] args) { MergeSort mergeSort = new MergeSort(); int[] num = {3, 7, 4, 2, 5, 8, 1}; num = mergeSort.mergeSort(num); for (int t : num) { System.out.println(t); } } }
7.手写一个LRU
8.给你一个数组,数组长度为n。请找出数组中第k大的数
public class Solution { public int findK(int[] num, int k) { return quickSort(num, 0, num.length - 1, k - 1); } public int quickSort(int[] num, int st, int ed, int k) { if (st >= ed) return num[st]; int value = num[st]; int left = st; int right = ed; while (left < right) { while (left < right && num[right] >= value) { right--; } num[left] = num[right]; while (left < right && num[left] < value) { left++; } num[right] = num[left]; } num[left] = value; if (left == k) return value; else if (left < k) { return quickSort(num, left + 1, ed, k); } else { return quickSort(num, st, left, k); } } public static void main(String[] args) { Solution solution = new Solution(); int[] num = {1,8,8,7,4,1,5,1,5,7}; System.out.println(solution.findK(num, 1)); System.out.println(solution.findK(num, 2)); System.out.println(solution.findK(num, 3)); System.out.println(solution.findK(num, 4)); System.out.println(solution.findK(num, 5)); System.out.println(solution.findK(num, 6)); System.out.println(solution.findK(num, 7)); System.out.println(solution.findK(num, 8)); System.out.println(solution.findK(num, 9)); System.out.println(solution.findK(num, 10)); } }
附加条件:不允许改变元素在数组中的位置
在int范围内去中位数,算出其在数组中是第几大的元素(遍历数组O(n)),与k比较不断二分。
9.找到数据流中的中位数
使用大小顶堆,如果放入的是奇数个,则取大顶堆的根结点,如果是偶数个则取大小顶堆根结点的平均值。
- 如果是奇数,放入小顶堆,然后取根结点加入大顶堆。
- 如果是偶数,放入大顶堆,然后取根结点加入小顶堆。
10.删除链表中重复节点
HashSet
11.给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
12.给定过一个二叉树,原地将它展开为链表
13.给定一个二叉树,想象自己站在他的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
11.判断是否是二叉搜索树
中序遍历
12.合并两个链表,用递归和非递归实现
13.字符串是否为给定字符串的子串
14.查找两个链表的公共节点
15.小顶堆
16.一个数x,一个数n,x中删n位使得剩下的数最大
17.给定一颗二叉树,求其中root的最长路径。所谓路径是指,联通两个节点的最小边数。
18.二叉树的序列化与反序列化