字节一面总结

面试问题

1 线程和进程的区别

  1. 进程 是操作系统资源分配的最小单位。 线程(英语:thread)操作系统能够进行运算调度的最小单位

  2. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。

    线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

  3. 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。

    进程间通信方式:

    • 管道
    • 消息队列:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。
    • 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
    • 信号量: 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
    • 套接字
    • 信号
  4. 多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

2 死锁的原理

多个线程/进程在抢占CPU执行权的时候出现了互相等待的状态。

四个条件

这四个条件是死锁产生的必要条件,只要发生死锁,一定存在这四个条件

互斥条件 :一个资源每次只能被一个进程/线程使用

请求保持条件:一个进程在申请新的资源的同时保持对原有资源的占有(只有这样才是动态申请,动态分配);

循环等待:资源申请者不能强行地从资源占有者手中夺取资源,资源只能由占有者自愿释放;

不可抢占:若干进程之间形成一种头尾相连的循环等待资源。

3 url中输入HTTPS后的全流程

  1. 解析URL

    检查这些请求是HTTPS还是HTTP,如果是HTTPS的话则使用HTTPS协议进行访问

  2. 查找IP地址

    首先检查本地缓存中是否存在该域名,如果存在则直接使用缓存中的IP地址进行访问。

    如果不存在,检查域名是否在本地的Hosts文件中,如果找到直接返回域名对应的IP。

    向DNS服务器发送一个域名查询请求,然后就执行DNS查询过程,最终返回域名对应的IP地址。

  3. 建立连接

    • 当浏览器得到了目标服务器的 IP 地址,以及 URL 中给出来端口号(http 协议默认端口号是 80, https 默认端口号是 443),它会调用系统库函数 socket ,请求一个 TCP流套接字。进行网络数据的传输。
    • 连接建立之后,则根据HTTP协议进行数据交换,资源通常是 HTML 文件,也可能是 PDF,图片,或者其他类型的内容。
  4. 浏览器获得资源文件后,HTML,css,js等文件则根据自身内核的机制,进行页面渲染,然后呈现给用户。

  5. 浏览器发送连接请求,附上自己支持的加密算法

  6. 服务器接收到客户端请求,想浏览器发送对应的CA证书,证书包含非堆成加密的公钥以及证书签名。

  7. 浏览器判断证书是否合法,并生成随机会话密钥,使用公钥加密随机会话密钥后发送给服务器

  8. 服务器采用私钥解密随机会话密钥

4 cookie和session的区别

cookie数据保存在客户端,session数据保存在服务器端

Cookie的工作原理:

(1)浏览器端第一次发送请求到服务器端
(2)服务器端创建Cookie,该Cookie中包含用户的信息,然后将该Cookie发送到浏览器端
(3)浏览器端再次访问服务器端时会携带服务器端创建的Cookie
(4)服务器端通过Cookie中携带的数据区分不同的用户

img

Session的工作原理:

(1)浏览器端第一次发送请求到服务器端,服务器端创建一个Session,同时会创建一个特殊的Cookie(name为JSESSIONID的固定值,value为session对象的ID),然后将该Cookie发送至浏览器端
(2)浏览器端发送第N(N>1)次请求到服务器端,浏览器端访问服务器端时就会携带该name为JSESSIONID的Cookie对象
(3)服务器端根据name为JSESSIONID的Cookie的value(sessionId),去查询Session对象,从而区分不同用户。
name为JSESSIONID的Cookie不存在(关闭或更换浏览器),返回1中重新去创建Session与特殊的Cookie
name为JSESSIONID的Cookie存在,根据value中的SessionId去寻找session对象
value为SessionId不存在(Session对象默认存活30分钟),返回1中重新去创建Session与特殊的Cookie
value为SessionId存在,返回session对象

在这里插入图片描述

区别:

cookie数据保存在客户端,session数据保存在服务端。

session
简单的说,当你登陆一个网站的时候,如果web服务器端使用的是session,那么所有的数据都保存在服务器上,客户端每次请求服务器的时候会发送当前会话sessionid,服务器根据当前sessionid判断相应的用户数据标志,以确定用户是否登陆或具有某种权限。由于数据是存储在服务器上面,所以你不能伪造。

cookie
sessionid是服务器和客户端连接时候随机分配的,如果浏览器使用的是cookie,那么所有数据都保存在浏览器端,比如你登陆以后,服务器设置了cookie用户名,那么当你再次请求服务器的时候,浏览器会将用户名一块发送给服务器,这些变量有一定的特殊标记。服务器会解释为cookie变量,所以只要不关闭浏览器,那么cookie变量一直是有效的,所以能够保证长时间不掉线。

5 MYSQL的索引

索引是存储引擎用于提高数据库表的访问速度的一种数据结构

优点:

  • 加快数据查找的速度
  • 为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度
  • 加快表与表之间的连接

缺点:

  • 建立索引需要占用物理空间
  • 会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长

作用:

数据是存储在磁盘上的,查询数据时,如果没有索引,会加载所有的数据到内存,依次进行检索,读取磁盘次数较多。有了索引,就不需要加载所有数据,因为B+树的高度一般在2-4层,最多只需要读取2-4次磁盘,查询速度大大提升。

建立索引的条件:

  1. 经常用于查询的字段
  2. 经常用于连接的字段建立索引,可以加快连接的速度
  3. 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度

什么情况下不建索引?

  1. where条件中用不到的字段不适合建立索引
  2. 表记录较少
  3. 需要经常增删改
  4. 参与列计算的列不适合建索引
  5. 区分度不高的字段不适合建立索引,如性别等

6 并列索引

ABC

查询AC只能部分命中,索引的部分命中

什么是最左匹配原则?

如果 SQL 语句中用到了组合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个组合索引去进行匹配。当遇到范围查询(><betweenlike)就会停止匹配,后面的字段不会用到索引。

(a,b,c)建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引。

(a,b,c,d)建立索引,查询条件为a = 1 and b = 2 and c > 3 and d = 4,那么a、b和c三个字段能用到索引,而d无法使用索引。因为遇到了范围查询。

如下图,对(a, b) 建立索引,a 在索引树中是全局有序的,而 b 是全局无序,局部有序(当a相等时,会根据b进行排序)。直接执行b = 2这种查询条件无法使用索引。

最左前缀

当a的值确定的时候,b是有序的。例如a = 1时,b值为1,2是有序的状态。当a = 2时候,b的值为1,4也是有序状态。 当执行a = 1 and b = 2时a和b字段能用到索引。而执行a > 1 and b = 2时,a字段能用到索引,b字段用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b值不是有序的,因此b字段无法使用索引。

7 抽象类和接口的区别

8 抽象类和接口分别在什么场景下使用

使用抽象类的情况:

  • 需要为一些类提供公共的实现代码时,应优先考虑抽象类
  • 定义某个领域的固有属性
  • 既想约束子类具有共同的行为(但不再乎其如何实现),又想拥有缺省的方法,又能拥有实例变量

使用接口:

  • 约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现
  • 实现类需要具备很多不同的功能,但各个功能之间可能没有任何联系。
  • 使用接口的引用调用具体实现类中实现的方法(多态)

9 JVM垃圾回收机制、算法

垃圾收集器:Serial收集器

算法:

  • 标记清除算法: 标记要回收的对象,标记完成后统一回收所有被标记的对象。效率问题、空间问题
  • 复制算法:将内存分为两部分,每次使用一部分,使用完之后将存活对象拷贝到另一块内存当中,再将所有内存全部清除。
  • 标记-整理算法:标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存 。
  • 分代收集算法:⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分
    配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集

10 JAVA中线程、进程同步方式

11 JAVA类的加载过程

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化
  6. 使用
  7. 卸载

12.Linux中的内存模型

内存管理分为连续分配管理以及非连续分配管理两种

连续分配管理 :块式管理。

非连续分配管理:页式管理、段式管理以及段页式管理.

在分页式内存管理当中,由于存在虚拟内存,存在

  1. 虚拟地址到物理地址的转换要快。
  2. 解决虚拟地址空间⼤,⻚表也会很⼤的问题

为了解决虚拟地址到物理地址的转换速度,操作系统在 ⻚表⽅案 基础之上引⼊了 快表 来加速虚拟地
址到物理地址的转换。其中的内容是⻚表的⼀部分或者全部内容。作为⻚表的 Cache,它的作⽤与⻚表相似,但是提⾼了访问速率。由于采⽤⻚表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问⼀次⾼速缓冲存储器,⼀次主存,这样可加速查找并提⾼指令执⾏速度。

  1. 根据虚拟地址中的⻚号查快表;
  2. 如果该⻚在快表中,直接从快表中读取相应的物理地址;
  3. 如果该⻚不在快表中,就访问内存中的⻚表,再从⻚表中得到物理地址,同时将⻚表中的该映射
    表项添加到快表中;
  4. 当快表填满后,⼜要登记新⻚时,就按照⼀定的淘汰策略淘汰掉快表中的⼀个⻚。

算法

题目

给定一个数n如23121;给定一组数字a如[2 4 9]求由a中元素组成的小于n的最大数

代码解析思路

该题使用到的算法主要是贪心、回溯以及二分,首先将nums数组进行排序方便进行二分查找,之后将给定数字n分解成一个列表,注意最高位在列表的最后一位,需要从前往后尽心寻找。

首先判断能取的最大数跟N的位数是否相同,不同直接取数组中的最大值即可。
注意如果出现前面取跟n一样的数,而后面出现没有数可以取的情况需要回溯减小前面的数。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package com.byte_mianshi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
public class Nmin {
private static int ret = 0;
private static boolean over = false;
public static void resolve(int n,int[] data){
Arrays.sort(data);
List<Integer> list = new ArrayList<Integer>();
int n_copy = n;
int tmp = 0;
while (n_copy >0){
tmp = n_copy % 10;
n_copy = n_copy / 10;
list.add(tmp);
}
System.out.println("数据打印开始");

for (int d: list) {
System.out.println(d);
}
System.out.println("数据打印开始");
System.out.println("数组打印开始");
for(int d : data){
System.out.println(d);
}
System.out.println("数组打印完毕");
System.out.println("数据大小:" + list.size());


int minData = 0;
for (int d: list) {
minData = minData * 10 + data[0];
}
System.out.println("最小值"+minData);
if(minData >= n){
//只能取比n少一位
dfs(data,list,n,list.size()-2,0,true);
}else{
dfs(data,list,n,list.size()-1,0,false);
}

System.out.println(ret);
}

/**
*
* @param data 可选数组数据
* @param list 分解后的n值
* @param n
* @param index 当前正在选择的位数
* @param ans 已经选择的数组成的值
* @param flag 是否可以无脑选择最大数的标志位
*/
public static void dfs(int[] data,List<Integer> list,int n,int index,int ans,boolean flag){
int len = list.size();
if(over == true){
return ;
}
if(index < 0){
//选择完毕
if(ans < n){
ret = Math.max(ret,ans);
over = true;
return ;
}
return ;
}
if(flag == false){
//找到最大的不大于list.get(index)的坐标
int f = find(list.get(index) , data);
//当前所有的数都比需要小于的数大,说明前面的数取大了,直接返回
if( f < 0){
return;
}else{
for(int i = f;i>=0;i--){
ans = ans * 10 + data[i];
if(data[i] < list.get(index)){
flag = true;
dfs(data,list,n,index-1,ans,flag);
}else{
//相等的情况
dfs(data,list,n,index-1,ans,flag);
}
ans = (ans - data[i])/10;
}
}
}else{
//说明前面有数字比n对应的数字小,可以一直取最大值
ans = ans * 10 + data[data.length-1];
dfs(data,list,n,index-1,ans,flag);
}

}

/**
* 找到不大于min的第一个序列号
* @param min
* @param data 查找的数组
* @return 数组中第一个小于等于min的序号
*/
public static int find(int min,int[] data){
int left = 0;
int right = data.length-1;
while (left <= right){
int mid = left + ((right - left)>>1);
if( data[mid] <= min){
left = mid + 1;
}else{
right = mid - 1;
}
}
if( right <0 ){
return -1;
}
return right;

}
}

测试案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NminTest {

@Test
public void Test(){
Nmin nmin = new Nmin();
int[] data = new int[] {5,6,7,8};
nmin.resolve(2333,new int[]{2,3});
nmin.resolve(233332,new int[]{2,3});
nmin.resolve(500,new int[]{4,9});
nmin.resolve(5612,new int[] {5,6,7,8});
nmin.resolve(5416,new int[] {5,4,8,2});
nmin.resolve(123434,new int[] {1,4,6,3,8});
nmin.resolve(32,new int[] {2,3});
}

}

输出结果

1
2
3
4
5
6
7
8
9
2332
233323
499
5588
5288
118888
23

Process finished with exit code 0

字节一面总结
http://example.com/2022/07/25/字节1面/
作者
JH
发布于
2022年7月25日
许可协议