八股
腾讯
1、map和unordered_map底层,解释下哈希碰撞,如何使用哈希设计出类似红黑树的效果
集合 | 底层 | 是否有序 | 是否重复 | 能否更改数值 | 查询效率 | 增删效率 |
set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
multiset | 红黑树 | 有序 | 是 | 否 | O(log n) | O(log n) |
unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
映射 | 底层 | 是否有序 | 是否重复 | 能否更改数值 | 查询效率 | 增删效率 |
map | 红黑树 | 有序 | key否 | key否 | O(log n) | O(log n) |
multimap | 红黑树 | 有序 | key是 | key否 | O(log n) | O(log n) |
unordered_map | 哈希表 | 无序 | key否 | key否 | O(1) | O(1) |
哈希函数 hashFunction = hashCode(类型) % tableSize,为了保证映射出的索引数值都落在哈希表上,我们再对数值做一个取模的操作。
哈希碰撞发生在两个不同的键通过哈希函数映射到了相同的哈希值,即映射到了哈希表的同个位置。
拉链法:每个哈希表的槽位中存储一个链表,所有哈希到同一槽位的元素都存储在这个链表中。
线性探测法:当发生碰撞时,从碰撞位置开始,逐个检查后续位置直到找到一个空槽位。
利用哈希表实现快速定位,每个桶内存储一个有序链表,在链表中按照一定规则(如键的自然顺序)插入和删除元素。这种设计在大多数情况下能提供快速查找和插入,同时通过扩容和重新哈希保持整体效率。通过哈希表的扩容和重新哈希操作,维持整体性能,类似于红黑树通过旋转和颜色变化维持平衡。有序链表保证了元素的顺序性,类似于红黑树中元素按顺序排列的性质。
2、TCP滑动窗口、拥塞控制
滑动窗口:在确认应答策略中,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段,这样性能较差,使用滑动窗口,就可以一次发送多条数据,从而就提高了性能。
- 滑动窗口机制用于控制数据包的流动,使得接收方能够处理并确认数据包。
- 窗口大小(窗口容量)决定了发送方在等待确认之前可以发送的数据量。
- 当窗口满了,发送方必须等待接收方的确认,以释放窗口空间,继续发送数据。
拥塞控制:在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包。通过拥塞控制,避免发送方数据填满整个网络。
拥塞控制用于防止网络阻塞,主要包括以下几种算法
- 慢启动:从一个小的初始拥塞窗口开始,逐步增加发送数据量。
- 拥塞避免:当慢启动达到一定阈值后,进入拥塞避免阶段,缓慢增加发送数据量。
- 快速重传和快速恢复:在检测到数据包丢失时,快速重传丢失的数据,并进入快速恢复阶段,避免拥塞窗口过度减小。
3、窗口满的情况,write函数的阻塞和非阻塞分别返回什么
在阻塞模式下,write
函数会阻塞等待滑动窗口有空间,数据才能写入,避免数据丢失。
在非阻塞模式下,write
函数会立即返回,如果滑动窗口满了,会返回 -1
并设置相应的 errno
。这要求程序设计需要处理 EAGAIN
或 EWOULDBLOCK
错误(errno的标记,资源暂时不可用,非阻塞IO中出现),通过合适的重试机制或者选择合适的时机重新尝试写入。
4、MySQL索引,B+树,Mysql存储引擎,查询优化
MySQL索引类似书籍的目录,提高查询速度。
B+树:所有叶子节点在同一层,非叶子节点存储键值,叶子节点存储数据记录。在插入、删除和更新操作后会自动重新平衡。B+树的叶子节点链表是双向的,为了实现倒序遍历或排序。
B+树和B树区别:
在B+树中,数据都存储在叶子节点上,而非叶子节点只存储索引信息;
而B树的非叶子节点既存储索引信息也存储部分数据。
B+树的叶子节点使用链表相连,便于范围查询和顺序访问;
B树的叶子节点没有链表连接。
B+树的查找性能更稳定,每次查找都需要查找到叶子节点;而B树的查找可能会在非叶子节点找到数据,性能相对不稳定。
在Mysql中,索引的存储方式与存储引擎有关,包括InnoDB和MyISAM。
Mysql默认InnoDB,在事务支持、并发性能、崩溃恢复方面具有优势。
事务支持:可以进行ACID(原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability)属性操作。MyISAM不支持事务。
并发性能:InnDB采用了行级锁定的机制,可以提供更好的并发性能,MyISAM存储引擎只支持表锁,锁的粒度较大。
崩溃恢复:InnoDB通关redlog日志实现崩溃恢复,可以在数据库发生异常情况时,通关日志文件进行恢复,保证数据的持久性和一致性。MyISAM是不支持崩溃恢复的。
索引结构:InnoDB是聚簇索引,MyISAM是非聚簇索引,聚簇索引的数据文件和索引文件存储在一起,数据按主键顺序存储。非聚簇索引的数据文件和索引文件分开存储。每个 MyISAM 表存储为三个文件:.frm
(表定义文件)、.MYD
(数据文件)、.MYI
(索引文件)。
InnoDB:适用于需要事务支持、高并发写操作、数据一致性要求高的场景,如金融系统、电商平台等。
MyISAM:适用于读操作多、写操作少、不需要事务支持的场景,如数据仓库、日志分析等
5、查询优化:
1.优化SQL、索引,选择合适的索引列(经常作为查询条件、排序的列)。2.使用Redis缓存高频查询结果。3.使用数据库连接池,一般访问数据库需要先和MySQLServer创建连接,然后发送SQL语句,再把结果通过网络返回给应用,然后关闭MysqlServer的连接,因此大量的数据库访问,消耗的TCP三次握手和四次挥手所花费时间很多。一般连接池维护以下资源:1、连接池里面保持固定数量的活跃TCP连接,供应用使用。 2、如果应用瞬间访问MySQL的量比较大,那么连接池会实时创建更多的连接给应用使用。 3、当连接池里面的TCP连接一段时间内没有被用到,连接池会释放多余的连接资源,保留它设置的最 大空闲连接量就可以了。
6、RPC和微服务的理解
RPC(Remote Procedure Call) 是一种通信协议,允许一个程序调用另一个地址空间(通常在远程服务器上)中的过程或方法,就像调用本地过程一样。RPC 的目的是简化分布式计算,使得程序员不需要关注底层的网络通信细节。
微服务架构 是一种软件架构风格,将应用程序分解为小的、独立部署的服务。每个服务运行在自己的进程中,并通过轻量级的通信机制(通常是 HTTP/HTTPS、消息队列等)进行通信。微服务架构的核心思想是将单一的大型应用程序拆分成多个小的、松耦合的服务,以便于独立开发、部署和扩展。
7、进程、线程、协程
进程是独立的程序实例,资源独立,隔离性好,开销大,适用于需要高度隔离的任务,如不同应用程序之间的运行。
线程是进程内的执行单元,共享资源,切换开销小,适用于需要高效并发的任务,如并行计算等。
协程是一种用户态的轻量级线程,切换开销小,控制简便适用于I/O密集型任务,但不能并行执行CPU密集型任务(不能利用多核CPU,因为协程始终在一个线程内运行),异步操作和更新UI等。
Android中,更新UI的机制主要依赖于Handler
、Looper
和MessageQueue
Handler用于在不同线程之间传递和处理消息Message,Looper不断从MessageQueue中取出消息,并通过Handler分发处理
在Kotlin,协程允许在不阻塞主线程的情况下执行耗时操作,使得UI保持响应。传统的线程编程,如果不小心在主线程中执行了耗时操作,会导致UI卡顿。协程提供了一种简洁、强大且高效的方式来处理异步操作
CoroutineScope(Dispatchers.Main).launch {
try {
val result = withContext(Dispatchers.IO) {
performLongRunningTask()
}
textView.text = result
} catch (e: Exception) {
// Handle exception
}
}
8、值传递、指针传递、引用传递
值传递:将一个值传递给函数时,函数会创建该值的副本,并在函数内部使用这个副本,这意味着函数对该值的修改不会影响到原始值。
指针传递:指针传递是通过将参数的内存地址传递给函数来实现的,函数可以通过指针来直接访问和修改原始值。
引用传递:引用传递是将参数的引用传递给函数,函数可以通过引用直接访问和修改原始值,而无需创建副本。
9、智能指针的使用和原理,C++内存管理
程序内存分区:
栈:编译器管理分配和回收,存放局部变量和函数参数。(stack)
堆:程序员使用new、malloc、delete、free进行堆内存的分配和释放。(heap)
全局/静态区:存储全局变量和静态变量。未初始化全局区(.bss),已初始化全局(.data)
常量区:存储常量数据。(.rodata)
代码区:存放程序的代码。(.text)
内存泄漏:程序未能释放掉不再使用的内存。
如何防止内存泄漏:将内存的分配封装在类中,构造函数分配内存、析构函数释放内存。使用智能指针。
智能指针包括:
auto_ptr,C++11已经抛弃,存在内存崩溃问题。
unique_ptr(独占智能指针):保证同一时间内只有一个智能指针可以指向该对象。
shared_ptr(共享智能指针):多个智能指针可以指向相同对象。该对象和相关资源在最后一个引用被销毁时释放,使用引用计数机制表明资源被几个指针共享。调用release时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
weak_ptr(弱引用智能指针):指向一个shared_ptr管理的对象。如果两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。把其中一个shared_ptr改为weak_ptr即可解决shared_ptr相互引用时的死锁问题。
南京银行
1、Mysql数据库中的分组和排序用什么?
分组(group by)、排序(order by)
2、Linux系统中怎么把一个只读文件修改权限为可执行文件
文件权限的表示方式如下:
r
(read) - 读权限w
(write) - 写权限x
(execute) - 执行权限
chomd -x filename。将文件修改为可执行文件
chmod 777 -R file
777
: 表示授予所有用户(文件拥有者、所在组的用户、其他用户)读、写和执行权限。7
表示rwx
(读、写、执行),三个7
分别对应文件拥有者、组用户和其他用户。
-R
: 递归地应用此权限更改,即对目录中的所有文件和子目录都应用此权限。
这里的 755
表示:
- 文件拥有者有读、写、执行权限(7 = rwx)
- 同组用户和其他用户有读、执行权限(5 = r-x)
3、Linux系统中如何查看进程、查看端口号?
ps aux
: 显示所有用户的所有进程。ps -ef
: 以标准格式显示所有进程。
这些命令会输出有关每个进程的详细信息,包括:
- 用户(USER/UID)
- 进程ID(PID)
- 父进程ID(PPID)
- CPU 和内存使用率(%CPU, %MEM)
- 启动时间(START)
- 运行时间(TIME)
- 命令(COMMAND)
top
是一个动态显示进程信息的命令,可以实时监控系统的资源使用情况
netstat
是一个用于显示网络连接、路由表、接口统计等信息的命令。常用选项包括:
netstat -tuln
: 显示所有监听的 TCP 和 UDP 端口。netstat -tulnp
: 显示监听端口及其对应的进程ID(PID)。
4、长连接和短连接的区别和优缺点
长连接适合需要频繁通信和保持实时性的数据传输(物联网监控设备),而短连接适合一次性请求和并发量较大的场景(网页浏览、文件下载)。
长连接
优点:减少连接建立和断开开销、实时性更好。
缺点:资源占用:长时间保持连接会占用服务器资源。连接维护成本:定期发送心跳包维持连接状态。
优点:资源释放及时,实现简单,适合并发量大的场景
缺点:开销较大:每次请求都需要建立和关闭连接,增加了TCP握手和断开连接的开销,影响了通信效率。实时性较差:每次请求需要重新建立连接,可能导致延迟。
5、登录功能,怎么保证安全性?
防止明文传输:采用安全HTTPS(SSL/TLS)协议
防止暴力破解:可在前端登录界面加入图片验证,登录时要求同时输入用户名、口令、验证码,这样就很好地防止了别人采用程序或机器人进行字典攻击、暴力攻击。
华为
1、封装、继承、多态
访问权限:public、protected、private,在类的内部,没用访问权限限制。在类的外部只能通过对象访问public属性的成员。
无论public、private、protected,私有成员均不能被“派生类”访问,基类中的public和protected能被派生类成员函数访问。对于public继承,只有基类中的public对象能被派生类对象访问,保护和私有成员均不能被派生类对象访问,对于私有和保护继承,基类中的所有成员均不能被派生类对象访问。
面向对象的三大特征是:封装、继承、多态
封装:封装是将数据和函数封装在一个类中,可以隐藏类的内部实现细节,对外只暴露必要的接口,防止外部直接访问和修改内部数据。隐藏数据实现,保护对象的内部状态。
继承:是指一个类从另外一个类获得属性和方法的能力,通过继承,子类可以复用父类的代码,并且可以扩展或重写父类的方法。通过继承父类,实现代码重用和扩展。
多重继承:一个类可以从多个基类继承属性和行为,但会导致菱形继承问题,通过虚继承(virtual)解决菱形继承带来的二义性。虚继承的机制是确保从A类只产生一份共享的副本,而不是像普通继承那样每个派生类(B和C)各自拥有一份。
多态:允许同一个接口调用产生不同的行为,通过虚函数,子类可以重写父类的方法。同一接口,表现不同的行为,实现动态绑定。(如果函数的调用,在编译器编译阶段就可以确定函数的调用地址并产生代码,则是静态的,即地址早绑定。如果函数的地址不能再编译期间确定,需要在运行时确定,这就属于晚绑定)
实现多态有两种方式:
重写(override):子类重新定义父类的虚函数的做法
重载(overload):允许存在多个同名函数,而这些函数的参数表不同(参数类型,个数不同)
2、new和malloc区别,malloc如何分配内存
类型安全性:
new是C++的运算符,可以为对象分配内存并调用相应的构造函数。
malloc是C语言库函数,只分配指定大小的内存块,不会调用构造函数。
返回类型:
new返回的是具体类型的指针,不需要进行类型转换
malloc返回的是void*,需要进行类型转换,因为它不知道所分配内存的用途。
内存分配失败时的行为:
new在内存分配失败时会抛出std::bad_alloc异常
malloc在内存分配失败时会返回NULL
int* p = (int*)malloc(sizeof(int) * 10); // 分配 10 个 int 类型大小的内存
if (p == NULL) {
std::cerr << "Memory allocation failed" << std::endl;
return 1;
}
释放内存的方式:
delete会调用对象的析构函数然后释放内存,释放内存块的指针值可以设置为nullptr以避免野指针
free只是简单地释放内存块,不会调用对象的析构函数,可能会导致野指针。
野指针:指向已被释放或无效的内存地址的指针。
以下可能产生野指针:
释放后没有置空指针:
int* ptr = new int;
delete ptr;
//此时ptr成为野指针,因为它指向已经被释放的内存
ptr = nullptr;//避免野指针,应该将指针设置为nullptr,或赋予新的有效地址
返回局部变量的指针:
int* createInt(){
int x = 10;
return &x;//x是局部变量,函数结束后x被销毁,返回的指针成为野指针
}
- 避免返回局部变量的指针:
int* createInt(){
int* x = new int;
*x = 10;
return x;
}
使用new动态分配内存,并返回指向该内存的指针,调用者负责释放分配的内存。
函数参数指针被释放
void foo(int* ptr){
//操作 ptr
delete ptr;
}
int main() {
int* ptr = new int;
foo(ptr);//在foo函数中ptr被释放,main函数仍然可用,成为野指针
}
避免野指针的方法:在释放内存后将指针置为nullptr,或赋予新地址。避免返回局部变量的指针。使用智能指针(std::unique_ptr和std::shared_ptr)
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);
//使用std::unique_ptr,避免显示delete,指针会在超出作用域时自动释放
避免在函数内释放调用方传递的指针,或者通过引用传递指针。
void foo(int*& ptr){
delete ptr;
ptr = nullptr;
}
int main() {
int* ptr = new int;
foo(ptr);
}
野指针和悬浮指针区别:
野指针是指未正确初始化或指向未分配内存的指针。
#include <iostream>
int main() {
int* p; // p 是一个野指针,未被初始化
*p = 42; // 使用野指针,导致未定义行为
std::cout << *p << std::endl;
return 0;
}
悬浮指针是指针指向已释放或超出作用域内存的指针。
#include <iostream>
int main() {
int* p = new int(42); // 动态分配内存,并初始化为 42
delete p; // 释放内存
std::cout << *p << std::endl; // 使用悬浮指针,导致未定义行为
return 0;
}
预防野指针:1、初始化指针2、使用智能指针
预防悬浮指针:1、释放后置空2、使用智能指针
3、堆和栈的区别
堆和栈是两种常见的内存管理方式
栈:编译器管理分配和回收,存放局部变量和函数参数,分配和释放速度快(stack)
堆:程序员使用new、malloc、delete、free进行堆内存的分配和释放,管理复杂且速度较慢(heap)
4、线程和进程的区别
进程是独立的程序实例,资源独立,隔离性好,切换开销大,适用于高度隔离的任务,如不同应用程序之间的运行。
线程是进程内的执行单元,共享资源,切换开销小,适用于需要高效并发的任务,如并行计算等。
5、进程间通信
Linux内核提供了不少进程间通信的方式:管道、消息队列、共享内存、信号、信号量、socket
6、为什么进程切换开销更大
每个进程拥有独立的内存空间,包括代码段、数据段、堆和栈,进程切换时,操作系统需要切换整个内存地址空间,这涉及到大量的上下文切换开销。而线程之间共享同一个进程的内存空间,不需要这样切换。
在多进程环境下,如果请求的进程数量超过了实际的 CPU 核心数,操作系统会如何处理:
操作系统会使用进程调度算法(如轮询)来在CPU核心之间分配进程的执行时间。
由于进程数量超过了 CPU 核心,操作系统会频繁进行上下文切换。这可能导致开销增加,因为每次上下文切换都需要保存和加载进程状态,影响总体性能。
7、OSI网络模型
OSI 7层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
TCP/IP模型: 网络接口层、网络层、传输层、应用层
http属于应用层,tcp在传输层,ip在网络层
8、TCP和UDP区别
- 连接:
TCP是面向连接的传输层协议,传输数据前需要建立连接,UDP是不需要连接的,即刻传输数据。
- 服务对象:
TCP是一对一的两点服务,UDP支持一对一、一对多、多对多
- 可靠性:
TCP是可靠交付数据,数据可用无差错、不丢失、不重复、按序到达。
UDP是尽最大努力交付,不保证可靠交付数据。
- 拥塞控制、流量控制:
TCP有拥塞控制、流量控制,保证数据传输的安全性。UDP则没有,即使非常拥堵了,也不影响UDP的发送效率。
- 首部开销:
TCP首部长度较长,会有一定的开销,TCP首部在没有使用字段时是20个字节,使用字段会变长,UDP首部只有8个字节,并且是固定不变的,开销较小。
- 传输方式:
TCP是流式传输,没有边界,保证顺序和可靠性。UDP是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
9、TCP三次握手和四次挥手
TCP是面向连接的,使用TCP前必须建立连接,而建立连接就是通过三次握手来进行的。
1.SYN:客户端向服务器发送一个SYN(同步序列编号)请求报文,表示希望建立连接,并且指明初始序列号。
2.SYN-ACK:服务器接收到SYN报文后,响应一个SYN-ACK报文。这个报文包含服务器的初始序列号和对客户端SYN的确认(ACK)。
3.ACK:客户端收到服务器的SYN-ACK报文后,再发送一个ACK报文,确认服务器的SYN报文,双方进入连接状态,准备传输数据。
为什么需要三次握手:确保双方能够同步彼此的序列号和确认信息,防止因网络延迟或重复的旧报文导致的连接错误。如果只进行两次握手,服务器无法确认客户端是否正确接收到自己的SYN-ACK报文,存在潜在的连接不一致问题。三次握手确保了双方都能确认对方的序列号和连接状态,避免因网络问题造成的混乱。
TCP四次挥手:
1.FIN:客户端主动调用关闭连接的函数,发送一个FIN报文,表示不再发送数据,但仍可以接收数据。
2.ACK:服务器收到FIN报文后,发送一个ACK报文,确认收到FIN报文,但可能还有未发送完的数据。(文件结束符放在已排队等候的其他已接收的数据之后)
3.FIN:服务器端,read数据自然会读到EOF,read()返回0,服务器有数据要发送的话,发送完数据才调用关闭连接的函数,如果服务端应用程序没有数据要发送,旧可用直接调用关闭连接的函数。这时服务器发送一个FIN包
4.ACK:客户端收到FIN报文后,发送一个ACK报文,确认收到被动服务器的FIN报文。服务器收到ACK后进入CLOSE,客户端2MSL后也进入CLOSE
四次挥手的原因主要是因为TCP是全双工通信,需要双方独立地关闭发送和接收通道。
如果只进行三次挥手,服务器可能在发送完数据前就关闭了连接,导致数据丢失。四次挥手确保双方都能独立确认对方的关闭请求,保证数据完整性和连接可靠性。
10、poll、epoll和select区别?
poll
、epoll
和 select
是在网络编程中用于实现 I/O 多路复用的系统调用,它们的主要作用是在一个线程中监控多个文件描述符(比如网络套接字),从而避免创建多个线程或进程。
select
:简单易用,兼容性好(所有UNIX-like系统都支持),但性能和扩展性较差(文件描述符数量有限,性能随监控的文件描述符数量增加而下降,每次调用需要在用户态和内核态切换)。poll
:比select
更灵活,没有文件描述符限制,但性能仍然受限于监控的文件描述符数量。epoll
:最适合大规模、高并发场景,性能优秀(减少了内核与用户空间的切换),但仅支持 Linux 平台。(使用红黑树监听socket)
epoll包括边缘触发和水平触发模式。边缘触发:只有第一次满足条件的时候才触发,之后就不会再传递同样的事件(当被监控的socket描述符上有可读事件时,服务端只会从epoll_wait中苏醒一次)。水平触发:内核中有数据需要读时,一直不断地把这个事件传递给用户(当被监控的socket描述符上有可读事件时,服务器端会不断从epoll_wait中苏醒直到内核缓冲区数据被read函数读完才结束)。
如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。
如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。
如果使用边缘触发模式,I/O事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞V/О搭配使用,程序会一直执行/O操作,直到系统调用(如read 和write)返回错误,错误类型为EAGAIN 或 EWOULDBLOCK。
11、消息队列的作用?
消息队列(Message Queue)是一种在分布式系统和并发编程中用于进程间通信(IPC)和消息传递的机制。消息队列允许多个进程或线程之间异步地发送和接收消息,从而实现解耦、异步处理、负载均衡等功能。
常见的消息队列:RabbitMQ、ActiveMQ、Redis
主要作用:
1.解耦:解耦是指让生产者和消费者在逻辑上独立,彼此不直接依赖。
示例:在电商网站中,订单处理系统和库存更新系统可以通过消息队列解耦,订单系统只需将订单信息发送到消息队列,而库存系统从消息队列获取订单信息进行处理。
2.异步处理:需要长时间执行的任务放入队列,立即返回结果给用户,由其他进程异步处理这些任务。
示例:在用户上传图片后,立即返回上传成功的消息,实际的图片处理(如压缩、转换格式)在后台异步进行,用户无需等待处理完成。
3.负载均衡:将任务分配给多个消费者进行处理,以便均匀分布负载,防止单个消费者过载。
示例:在高流量的网页服务器中,通过消息队列将请求分配给多个服务器处理,避免某一台服务器过载。
4.流量削峰填谷:在秒杀活动中,大量请求会在短时间内涌入,通过消息队列将请求排队处理,避免数据库和服务器崩溃。
有一个生产者线程,负责接收来自采集器的UDP包。每当接收到一个UDP包时,生产者线程会将其转换为DatagramCollection
对象。然后,生产者线程将这个对象放入一个ConcurrentLinkedQueue
中,这个队列充当我们的消息队列。”设置了一个消费者线程,从ConcurrentLinkedQueue
中取出DatagramCollection
对象进行处理。消费者线程解析这些UDP包,根据数据的类型(电流通道或电压通道)将其分发给相应的处理线程。”“根据解析结果,如果是电流通道的数据,消费者线程会将其分发给专门的电流处理线程进行处理,并更新UI。如果是电压通道的数据,则分发给电压处理线程进行处理,并更新UI。”
在我的项目中,ConcurrentLinkedQueue
作为消息队列,实现了生产者和消费者的解耦。生产者线程只需将数据放入队列,无需关心数据的处理过程。消费者线程从队列中取出数据进行处理,实现了数据的异步处理和高效分发。通过这种机制,我们确保了数据处理的高效性和实时性。”
“这种设计使得我们的系统具有很高的扩展性和可靠性。我们可以通过增加生产者或消费者线程的数量,轻松应对更大的数据流量。同时,使用ConcurrentLinkedQueue
保证了线程安全和高效的并发处理。在未来,我们可以考虑引入更复杂的消息队列系统,如RabbitMQ或Kafka,以实现更高的吞吐量和可靠性。”
ConcurrentLinkedQueue
适用于简单、单机、多线程的消息传递场景,而RabbitMQ适用于复杂、分布式、需要高级消息处理功能的场景。ConcurrentLinkedQueue
是Java内置的数据结构,而RabbitMQ是一个独立的第三方消息代理系统,具有更强的功能和可靠性。
美团
1、select、poll、epoll
select、poll、epoll是在网络编程中用于实现IO多路复用的系统调用,主要作用是在一个线程中监控多个文件描述符(比如网络套接字)。
select:简单易用、兼容性好(所有UNIX-Like)都支持,但性能和扩展性较差(文件描述符数量有限),性能随监控的文件描述符数量增加而下降,每次调用需要在用户态和内核态切换。
poll:比select更灵活,没有文件描述符限制,但性能仍受限于监控的文件描述符数量。
epoll:最适合大规模、高并发场景、性能优秀(减少了内核与用户空间的切换),但仅支持Linux平台。
epoll包括边缘触发和水平触发模式。边缘触发:只有第一次满足条件的时候才触发,之后就不会再传递同样的事件(当被监控的socket描述符上有可读事件时,服务端只会从epoll_wait中苏醒一次)。水平触发:内核中有数据需要读时,一直不断地把这个事件传递给用户(当被监控的socket描述符上有可读事件时,服务器端会不断从epoll_wait中苏醒直到内核缓冲区数据被read函数读完才结束)。
如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。
如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。
如果使用边缘触发模式,I/O事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞V/О搭配使用,程序会一直执行/O操作,直到系统调用(如read 和write)返回错误,错误类型为EAGAIN 或 EWOULDBLOCK。
2、如何使用rsync命令进行文件同步
-a
:归档模式,递归同步目录并保持文件属性。-v
:详细输出模式,显示同步过程。
本地同步:rsync -a 源目录或文件 目标目录或文件(rsync -a 源目录或文件 目标目录或文件)
远程同步:rsync -av root@192.168.1.77:/etc/hosts /dir1/ #j将192.168.1.77服务器/etc/hosts文件拷贝到本地/dir1文件夹下
3、解释数据库的悲观锁和乐观锁
针对这个问题,我的理解如下,为了得到最大的性能,一般数据库都使用并发控制,不过带来的问题就是数据访问的冲突,为了解决这个问题,大多数数据库用的方法就是数据的锁定。
悲观锁:顾名思义,就是很悲观。每次去拿数据的时候都认为别人会修改数据,所以每次在拿数据的时候都会上锁,这样别人想拿到这个数据就会block直到拿到锁,传统的关系型数据库中很多用到这种锁机制,比如行锁、表锁、读锁、写锁等都是在做操作之前先上锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据时都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
4、Const可以修饰哪些
const是一个C++的限定符,它限制一个变量不允许被改变,使用const可以在一定程度上提高程序的安全性和可靠性。
const可以修饰全局/局部变量,使用const修饰,被修饰的变量不允许被修改。
const修饰指针,常量指针const int* pInt;指针常量int *const pInt = &someInt;常量指针不能改变*plnt,指针常量不能改变pInt
const修饰引用,const int &a,非const引用只能绑定非const对象,const引用可以绑定任意对象,并且都当做常对象。常饮用经常用作形参,防止函数内对象被意外改变。
const修饰函数返回值,const int* func() { // return p; }防止外部对对象的内部成员进行修改
const修饰成员函数
const修饰数据成员:
const修饰类对象:该对象内的任何成员变量都不能被修改。
5、DDOS攻击和DOS攻击
DOS(Denial of Service)指拒绝服务,主要危害是使计算机或网络无法提供正常的服务。DOS攻击侧重于通过对服务器特定的漏洞,利用DOS攻击导致网络连接失败、系统崩溃、电脑死机等情况的发生。
DDOS(Distributed Denial of Service)指分布式拒绝服务,是DOS的一种,通过很多僵尸主机向目标主机发送看似合法的流量包,从而造成网络阻塞或服务器资源耗尽而导致拒绝服务。
采取措施:增强网络基础设施建设(增加服务器的处理能力和负载能力),使用防火墙和入侵监测系统(限制不必要的网络流量)
6、解释C++17中的结构化绑定以及它的使用场景
pair是将两个数据组合成一组数据,二元组。tuple可以组合三个及以上的数据。
C++17 引入了结构化绑定(structured bindings),这一特性极大地简化了处理元组(tuple)、对(pair)等数据结构的方式。结构化绑定允许你将一个包含多个值的对象拆解成多个变量,从而提高代码的可读性和简洁性。
7、设计一个简单的文章版本控制系统 允许查看和恢复历史版本
数据模型设计:
文章:包含标题、作者和内容。每次修改时,系统应记录下修改的时间和版本号。
版本:每个版本记录修改后的内容和相关的元数据,比如版本号和时间戳。
保存新版本:
每次文章内容被修改时,创建一个新的版本对象,并将当前文章内容及其修改时间保存到版本中。
将新版本添加到版本历史记录中,并更新当前版本标识符。
查看历史版本:
提供一个接口(如列表或表格)展示所有保存的版本。可以按时间顺序或版本号排序,允许用户查看每个版本的内容和相关信息。
恢复历史版本:
提供一个功能允许用户选择一个历史版本并将文章内容恢复到该版本。恢复操作将使当前文章的内容回滚到选定版本的内容。
8、如何显示版本的差异比较和冲突解决机制
版本差异比较:
差异算法: 使用算法如 diff 来计算两个版本之间的不同之处。算法会标识出新增、删除或修改的部分。
可视化差异: 在用户界面上,高亮显示新增、删除或更改的部分。常见的方法包括使用不同的颜色或标记来区分不同类型的更改。
冲突解决机制:
冲突检测: 在合并不同版本时,系统需要检测冲突,即相同部分在不同版本中有不同修改。
冲突标记: 显示冲突区域,并标记出不同版本中的修改内容。例如,使用分隔线或标签来区分不同版本的内容。
顺丰
- Android架构的主要组成部分:
Linux内核层:Android基于Linux内核,负责硬件抽象层、进程管理、内存管理等操作。
AndroidRunTime(ART):提供核心库和运行时环境。
应用框架层:提供了应用开发所需的各种api,包含ActivityManager、WindowManager、PackageManager等。
应用层:用户直接与应用层交互,所有Android应用都运行在这个层次上。
- 解释Android NDK是什么,以及在什么情况下会使用NDK?
Android NDK(NativeDevelopmentKit)是一套开发工具,允许开发者使用C/C++编写性能关键的代码。一般情况下,使用Java编程语言开发Android应用是足够的,但比如在如下情况会使用NDK:
1.需要与现有的C/C++代码库进行交互或复用。2.对性能有极高要求的场景(图像开发、图像处理、音视频解码等)。3.使用底层硬件的场景,例如处理设备传感器数据。
NDK可以直接访问底层硬件,提高执行效率,但也会增加应用的复杂度和调试难度。
- 简要说明对Linux操作系统的理解,与Android的关联
Linux是一个开源的类Unix操作系统,Android使用Linux内核进行系统底层的管理工作,Android中的Linux内核主要负责哪些功能:
进程管理:确保各应用在各自的进程中运行,防止跨进程的相互干扰。
内存管理:通过Linux内核进行内存分配与管理。
网络堆栈:提供基本的网络通信支持。
驱动程序支持:Android设备的硬件驱动(比如相机、音频、显示等)都依赖Linux内核进行管理
- Linux中的进程调度机制?
各个进程之间共享CPU资源,在不同时候进程之间需要切换,让不同的进程可以在CPU执行,从一个进程切换到另一个进程运行,称为进程的上下文切换。
最简单的,比如非抢占式的先来先服务(First Come First Served,FCFS)算法。
每次从就绪队列中选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。(当一个长作业运行了,后面短作业等待的时间就会很长,这种算法对长作业有利,不适用于短作业)
时间片轮转(Round Robin,RR)调度算法,每个进程分配一个时间段,称为时间片,允许该进程在该时间段中运行。如果时间片用完,进程还在运行,会把进程从cpu中释放出来,并把cpu分配给另外一个进程。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换。(关键点在于时间片设置的长短,时间片太短会导致过多的进程上下文切换,降低了CPU的效率。如果设得太长又可能引起短作业进程的响应时间变长)。通常设为20ms-50ms。
最高优先级(HighestPriorityFirst,HPF)调度算法,从就绪队列中选择最高优先级的进程进行运行。该算法分为两种处理优先级高的方法,抢占式和非抢占式:
非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。
抢占式:当就绪队列中出现优先级高的进程,把当前进程挂起,调度优先级高的进程运行。
多级反馈队列调度算法:是时间片轮转算法和最高优先级算法的综合发展。
多级:表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
反馈:如果有新的进程加入优先级高的队列,立即停止当前正在运行的进程,转而去运行优先级高的队列。
对于短作业可以在第一级队列中很快被处理完,对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待时间变长了,但是运行时间也会更长,该算法很好兼顾了长短作业,同时有较好的响应时间。
- Linux编程经验
我在Linux环境下主要使用C/C++进行开发,工具链主要包括:
GCC/G++:用于编译C/C++代码。
GDB:用于调试程序,能够进行断点调试。
Makefile/CMake:用于管理多文件项目的编译过程。
Valgrind:用于内存监测和性能调优
valgrind --leak-check=full ./your_program
Valgrind 会在终端中输出详细的内存使用报告,有助于查找和修复内存管理问题。
VSCode/Vim:作为文本编辑器或IDE
通过这些工具,编写、调试并优化Linux下的应用程序、处理多线程、文件IO和网络编程等任务。
- Android中的Handler机制是如何工作的
Android的Handler机制用于线程间通信,特别在UI线程(主线程)中进行异步操作时,非常有用。
Looper:每个线程有一个Looper,负责轮询消息队列(MessageQueue),取出消息并处理。
Handler:通过Handler发送消息(Message)或者可执行任务(Runnable)到MessageQueue中,(sendMessage()或post()方法将消息加入队列)
MessageQueue:存储消息和任务,Looper不断从队列中取出消息,并交给对应的Handler处理。
这种机制可以避免在主线程中执行耗时操作,确保界面流畅。
- Android中的四大核心组件
Activity:安卓中的用户界面层,通常对应一个单独的屏幕,负责用户的交互和展示UI元素。具有复杂的生命周期,常见的回调方法包括onCreate(),onStart(),onResume(),onPause(),onStop(),onDestroy()等。
Service:后台运行的组件,用于执行长时间运行的操作,例如播放音乐、处理文件等。(onStartCommand(),onBind(),onDestroy())。
BroadcastReceiver:广播,在应用之间或应用内部传递消息。在onReceive()方法中处理接收到的广播。如网络状态变化。
ContentProvider:用于在不同应用之间共享数据,通过定义数据接口,其他应用可以通过URI访问和操作数据。如联系人。
- 反射
反射是一种运行时动态访问类、方法、属性的能力,允许我们在编译时不知道类型的情况下操作对象。反射通常用于框架、库的开发。
动态性强,可以在运行时加载和使用类和对象。反射操作较慢,绕过了编译时的优化。增加了代码的复杂性。安全性较低,因为它可以绕过访问控制修饰符。
- EventBus
EventBus是Android中的事件发布/订阅框架,用于简化组件之间的通信。通过发布者和订阅者之间的松耦合,简化了事件的传递流程,避免了繁琐的接口回调。
注册订阅者:使用EventBus.getDefault().register(this)。
发布事件:使用EventBus.getDefault().post(new Event())
发布事件,所有订阅了该事件类型的对象都会接收到通知。
事件订阅方法:使用@Subscribe
注解标注接收事件的方法
EventBus简化了组件间的通信,特别是在Activity、Fragment和Service之间。过度使用EventBus可能导致代码难以理解和维护。
都用于实现发布者和订阅者之间的通信,发布者发布事件或消息,多个订阅者可以订阅并接收相应的事件或消息。但是应用场景、通信机制、目的都有显著不同。
EventBus和基于Redis的发布-订阅功能都与观察者模式有密切的联系。观察者模式核心思想,定义对象之间的一对多依赖关系,当一个对象(被观察者)发生变化时,所有依赖它的对象(观察者)都会自动收到通知。
- linux怎么查询 log文件中指定内容
grep用于搜索文本文件中匹配特定模式的工具。可用来查找日志文件中的内容
grep "搜索内容" 文件名
查找日志文件中包含error的所有行
grep "error" /var/log/syslog
查找包含nginx的所有进程
ps aux | grep "nginx"
-
ps aux
:显示所有用户的进程(a)、以用户格式显示(u),显示所有进程(x)。 -
|
:管道符,将ps aux
的输出传递给grep
进行进一步过滤。 -
grep "nginx"
:从ps aux
的输出中筛选包含nginx
的行。
- 移动端屏幕适配问题
1.定义灵活的布局宽度,比如使用match_parent、wrap_content。
2.使用动态字体调整。例如TextView的autoSizeTextType属性,根据控件大小自动调整字体大小。
3.使用屏幕适配框架,第三方库如AutoLayout能帮助开发者更方便地进行屏幕适配。
- http和https的区别
HTTP使用明文传输,不进行加密,数据容易被拦截和窃取。
HTTPS加入了SSL协议,传输过程中,数据会被加密,确保数据安全性。
HTTP端口号是80,HTTPS端口号是443
HTTPS需要申请SSL证书来验证服务器的真实性,确保访问者与真实服务器通信。
- HTTP Request的几种区别
- GET:请求指定的资源,常用于获取数据,参数通常放在URL中,幂等性高。
- POST:提交数据到服务器,常用于提交表单数据,参数通常放在请求体中,数据量较大,非幂等。
- PUT:更新服务器上的资源,幂等。
- DELETE:删除指定的资源,幂等。
- HEAD:与GET类似,但只返回HTTP头部,不返回主体内容。
GET和POST区别:
-
参数传递方式:
- GET:参数通过URL传递,适合小数据量(通常不超过2048字符)。
- POST:参数通过请求体传递,支持大数据量传输。
-
安全性:
- GET:参数暴露在URL中,安全性较低。
- POST:参数在请求体中传输,相对较安全,但不完全可靠。
-
幂等性:
- GET:幂等,请求多次结果一致。
- POST:非幂等,每次请求可能会产生不同的结果。
-
缓存:
- GET:请求可以被缓存。
- POST:默认不缓存。
-
线程安全怎么保证:
锁机制:使用互斥锁来控制对共享资源的访问。
线程局部存储:每个线程都拥有自己的存储,不共享变量
并发容器:使用如ConcurrentQueue等线程安全的容器
-
死锁产生的必要条件:
死锁是指多个进程/线程在同一资源上相互占用,并请求锁定对方的资源,从而形成一个闭环等待。
死锁发生的必要条件:
互斥条件:多个线程不能同时使用同一个资源。
持有并等待:线程已经持有至少一个资源,并请求新的资源时不会释放当前资源。
不可剥夺:资源不能被强行剥夺,必须由持有它的线程主动释放。
循环等待:多个线程形成环形依赖,导致互相等待资源。
- 解决死锁的问题
确保资源在请求时总是按照固定顺序进行的,来破坏环形依赖的条件。使用银行家算法(避免死锁。银行家算法会在分配给进程资源前,判断分配后是否产生死锁现象,如果系统当前资源能满足其执行,则尝试分配,如果不满足就让进程等待。
- 数组、链表的区别
存储方式:数组是连续的内存空间,按索引访问速度快。链表是非连续的内存空间,节点通过指针连接,访问需要遍历。
插入和删除:数组需要通过移动元素进行插入删除、效率低。链表插入和删除效率高,只需要修改指针。
内存利用:数组内存预先分配,可能会浪费空间,链表动态分配内存,利用率高。
- volatile和sychronized
当一个变量被声明为volatile时,线程在读取该变量时会直接从内存中读取,而不会使用缓存,当一个线程修改了volatile修饰的变量值,其他线程能够立即看到最新的值,从而避免了线程间的数据不一致。但volatile并不能解决多线程并发下的复合操作,比如i++这种操作不是原子操作,如果多个线程同时对i进行自增操作,volatile不能保证线程安全。
sychronized保证了多个线程访问共享资源时的互斥性,同一时刻只允许一个线程访问共享资源,通过对代码块/方法添加sychronized实现同步。
- 为什么会有分页机制,解决了什么问题
- 减少内存碎片:内存分页能将内存划分成固定大小的页,避免因为不连续的内存请求导致的内存碎片问题。
- 进程隔离和内存保护:通过页表可以实现进程的虚拟内存与物理内存的映射,防止进程之间互相干扰。
- 有效利用内存:分页机制允许进程只加载实际使用的内存页,而不必一次性占用大量连续的物理内存。
- 网络协议
- HTTP/HTTPS:用于万维网的数据传输。
- TCP/UDP:TCP 提供可靠的连接,UDP 提供无连接、快速传输。
- IP:用于在网络之间路由数据包。
- DNS:将域名解析为IP地址。
- ARP:将IP地址映射为物理MAC地址。
- FTP/SMTP/IMAP:分别用于文件传输、邮件传输和邮件获取。
-
应用层数据包是怎么一步一步往下传的
- 应用层(如HTTP):生成应用层数据包。
- 传输层(如TCP/UDP):将应用层数据封装为段/数据报,添加端口信息。
- 网络层(如IP):将传输层数据包封装为IP包,添加源IP和目标IP地址。
- 数据链路层(如以太网):将IP包封装为帧,添加源MAC和目标MAC地址。
- 物理层:将帧转换为电信号或光信号,通过物理介质传输。
- 怎么用UDP实现可靠连接
UDP本身不提供可靠性,因此需要在应用层手动实现,例如:
- 确认机制:发送方发送数据后,等待接收方的确认ACK。如果没有收到确认,发送方会重传。
- 序列号机制:为每个数据包添加序列号,接收方可以根据序列号检测丢包、乱序等问题。
- 超时重传:设置超时机制,当发送的数据包未在规定时间内收到确认时,自动重传。
-
说一下WebSocket是干什么的
WebSocket 是一种全双工通信协议,用于在客户端和服务器之间建立持久连接。与传统的 HTTP 请求-响应模式不同,WebSocket 允许服务器主动推送消息给客户端,适用于需要实时数据传输的场景,如聊天应用、实时股票更新等。
- 设计一个线程池,需要考虑哪些因素
- 线程数量:根据系统资源和任务类型选择合适的线程数。
- 任务队列:管理等待执行的任务,避免任务过载。
- 线程回收机制:处理空闲线程的回收,避免资源浪费。
- 任务调度策略:确定任务执行顺序,如FIFO、优先级等。
- 异常处理:处理线程执行过程中出现的异常,保证线程池稳定运行。
-
线程什么时候回收,执行完任务怎么处理的
线程回收通常在任务执行完毕、并且线程处于空闲状态超过指定时间后。常见的处理方式:
- 固定线程池:线程执行完任务后继续等待新任务,不会被立即销毁。
- 动态线程池:当空闲时间超过设定值,线程会被销毁,减少资源占用。
- Redis和MySQL各自的优劣是什么
- Redis:
- 优点:速度快(基于内存)、支持丰富的数据结构、支持持久化、适用于高频访问场景。
- 缺点:基于内存,数据量大时成本较高,不适合复杂查询。
- MySQL:
- 优点:支持复杂查询、关系型数据模型、支持事务和数据一致性,适用于大规模数据的存储和管理。
- 缺点:相对较慢,尤其在大规模高并发读写时性能不如Redis。
- 用户态和内核态的区别?
内核态和用户态是操作系统中的两种运行模式。
在内核态下,CPU可以执行所有的指令和访问所有的硬件资源。这种模式下的操作具有更高的权限,主要用于操作系统内核的运行。在用户态下,CPU只能执行部分指令集,无法直接访问硬件资源,这种模式下操作权限较低,主要用于运行用户程序。
- 进程间通信
1. 管道(Pipe)
-
无名管道:常用于父子进程之间的通信,是单向通信的,数据从一端写入,从另一端读取。
-
命名管道(FIFO):允许不相关进程之间通信,支持双向通信,但必须在数据流同步方面进行管理。
2. 消息队列(Message Queue)
-
消息队列是一种先进先出(FIFO)的队列,允许进程通过发送和接收消息进行通信。消息队列能够保存消息,直到接收进程读取,并且支持不同优先级的消息。
3. 共享内存(Shared Memory)
-
共享内存允许多个进程访问同一块内存区域,速度最快,因为数据不需要通过系统调用来传递。但需要配合同步机制(如信号量、互斥量)来避免多个进程同时修改同一内存区域引发数据竞争问题。
4. 信号量(Semaphore)
-
信号量是一个用于控制多个进程对共享资源访问的同步机制,可以用于避免竞争条件、死锁问题。信号量提供两种基本操作:
P
操作(等待)和V
操作(释放)。
5. 信号(Signal)
-
信号是操作系统中用于通知进程发生了某个事件的机制。进程可以通过信号进行简单的通信和控制,比如通知进程停止、继续、或终止。
6. 套接字(Socket)
-
套接字通常用于网络通信,但也可以在本地进程间进行通信。支持双向通信,并且可以跨网络进程进行数据交换。
每种IPC机制都有其适用场景:
管道、消息队列适用于小数据量、非实时的数据交换。
共享内存适用于需要高效、大量数据共享的场景。
信号量主要用于同步和互斥操作。
信号适合简单的进程间通知。
套接字适合分布式系统或网络应用中的通信。
- Java和C++区别
平台独立性:Java程序与平台无关,将程序编译成字节码运行在Java虚拟机(JVM)上。C++是平台相关的,编译后生成机器码,必须针对每个目标平台进行编译。
内存管理:Java有自动的垃圾回收机制,不需要开发者手动管理内存分配和释放。C++需要手动管理内存,使用new和delete来分配和释放内存。
指针:Java不支持直接使用指针,简化了内存操作。C++支持指针,可以直接访问和操作内存。
面向对象:Java是严格的面向对象编程,所有代码必须写在类中,且不支持多重继承。C++支持面向对象、面向过程,并且支持多重继承。
应用场景:Java主要应用于企业级应用,Web开发,Android开发等。C++广泛应用于系统编程、游戏开发、嵌入式系统等领域。
C++作为C的扩展,大多数C代码可以在C++编译器中运行。C是面向过程的,主要通过函数调用和过程来实现程序逻辑。C++是面向对象的,引入了类和对象的概念,支持面向对象的特性如封装、继承、多态。C不支持函数重载和运算符重载、不支持构造函数和析构函数。C++支持。C没有命名空间的概念,所有标识符在同一作用域内必须唯一,C++引入了命名空间,可以将标识符分组,以避免命名冲突。C不支持模板和泛型编程,代码通用性较低。C不支持异常处理机制,错误处理通过返回值或全局变量errno来实现。C需要使用malloc和free手动管理内存,C++除了支持malloc和free外,还引入了new和delete用于动态内存管理。C++还引入了智能指针,除了继承C的功能外,还包括STL,提供丰富的数据结构和算法支持。
- 判断对象是否为垃圾
引用计数法:为每个对象分配一个计数器,每当有一个对象引用时,计数器加1,引用失效时,计数器减一,当计数器为0时,表述对象不再被任何变量引用,可以回收。
可达性分析法:从一组GC Roots(垃圾收集根)的对象出发,向下追溯他们引用的对象,如果一个对象到GCroots没有任何引用链相连,这个对象就被认为是不可达的,可以被回收。
- 垃圾回收算法有哪些?
标记-清除算法:分为"标记"和"清除"两个阶段,首先通过可达性分析,标记所有需要回收的对象,然后统一回收所有被标记的对象。
标记-整理算法:与标记-清除算法的标记过程一致,但标记之后不会直接清理,而是将所有内存都移动到内存的一端,移动结束后直接清理掉剩余部分。
- 数据库范式
1NF:每个表格列必须是原子的, 且每列只能包含单一值。
2NF:满足1NF,并且每个非主属性完全依赖于主键,而不是主键的一部分
3NF:满足2NF,并且每个非主键属性直接依赖于主键,而不是依赖于其他非主键属性。
- 键入网页后,到网页显示期间发生了什么?
域名(DomainName):域名实际是IP地址(在网络层面上标识定位设备)的别名,通过DNS(域名系统)将域名解析为IP地址,如www.google.com->172.217.16.196。com是顶级域名,google是二级域名,www是主机名(万维网服务器)。
URL(统一资源定位符):互联网上资源的完整地址(包括协议、域名、路径和查询参数等)。
https://www.example.com/path/to/resource?id=123&search=abc
协议://域名路径(/path/to/resource)查询参数。
1.DNS解析:浏览器首先向DNS服务器查询输入的域名对应的IP地址。
2.建立连接:浏览器通过TCP协议与服务器建立连接(TCP三次握手)
3.HTTP请求:浏览器向服务器发送HTTP请求,请求网页内容。
4.数据包拆分:服务器将网页内容分成多个数据包,每个数据包都有一定大小(通常根据MTU设定,约1500字节)。
5.传输:这些数据包通过网络(使用IP协议)传输到客户端,途中经过多个路由器。
6.数据包重组:客户端收到数据包后,TCP协议按照序列号将数据包重新组装。
7.网页显示:浏览器重新解析重组后的网页数据,并呈现给用户。
物理层(网线等):负责将数据以电信号或光信号形式再设备之间传输。
数据链路层(网卡、交换机):处理MAC地址和数据帧,将数据从一个设备传输到另一个设备,并确保局域网中的正确传输。
网络层(路由器):负责跨网络传输数据包,通过IP地址定位目标设备。
传输层及以上(设备操作系统):在设备的操作系统中处理数据包的实际内容(如TCP/UDP协议),最终将应用数据传递到对应的应用程序中。
1. 数据封装过程(发送端)
当应用程序准备发送数据时,数据会经过以下几个协议层进行封装:
-
应用层(HTTP):
- 应用程序生成的原始数据(例如HTTP请求或响应)被传递到传输层。这部分数据被称为“应用数据”。
-
传输层(TCP):
- 传输层负责将应用数据封装成TCP段(segment)。TCP段的头部包含了源端口号、目的端口号、序列号、确认号、控制位等信息,用于确保数据可靠传输。
- 封装后的数据成为TCP数据包(包括TCP头部和应用数据)。
-
网络层(IP):
- TCP数据包被进一步封装在IP数据包中。IP头部包含源IP地址、目的IP地址、协议号等信息,用于在网络中定位目标设备。
- 封装后的数据称为IP数据包。
-
数据链路层(以太网):
- IP数据包再被封装在数据链路层的帧(frame)中。以太网头部包含源MAC地址、目的MAC地址、帧类型等信息,用于局域网中设备之间的通信。
- 封装后的数据称为以太网帧。
-
物理层:
- 以太网帧通过物理层(例如网线或无线信道)转化为电信号或光信号,在网络上传输。
2. 数据传输过程(网络中)
-
局域网中的传输:
- 在局域网(LAN)中,交换机或集线器根据以太网帧的目的MAC地址,将帧转发给目标设备或下一跳设备(如路由器)。
-
跨网络的传输(路由器):
- 当数据包需要穿越多个网络时,数据包会经过一个或多个路由器。
- 路由器接收到以太网帧后,会解封装出IP数据包,根据IP头部的目的IP地址查找路由表,确定数据包的下一跳地址。
- 然后,路由器将IP数据包重新封装成新的以太网帧,并发送到下一跳的设备。
3. 数据解封装过程(接收端)
当数据包到达目标设备时,设备会按以下步骤解封装数据:
-
物理层:
- 设备的网卡接收到电信号或光信号,并将其还原为数据链路层的帧。
-
数据链路层(以太网):
- 网卡检查帧中的目的MAC地址,确认该帧是发送给本设备的。
- 然后,网卡解封装出IP数据包,并将其传递给网络层。
-
网络层(IP):
- 网络层检查IP数据包中的目的IP地址,确认该IP数据包是发送给本设备的。
- 然后,网络层解封装出TCP数据包,并将其传递给传输层。
-
传输层(TCP):
- 传输层检查TCP数据包中的端口号,确认该数据是发送给哪个应用程序的。
- 然后,传输层根据序列号等信息对数据包进行排序、重组(如果有必要),并进行错误校验。
- 最后,传输层解封装出原始的应用数据,并将其传递给应用层。
-
应用层(HTTP):
- 应用层接收到数据,并将其交给相应的应用程序(例如浏览器)进行处理,最终呈现给用户。
当一个主机(比如电脑或路由器)想要与同一局域网中的另一台设备进行通信时,它需要知道目标设备的MAC地址,而它通常只有目标设备的IP地址。ARP协议通过广播的方式,帮助发送方获取目标的MAC地址。
- 实现KV存储
- 哈希表提供O(1)时间复杂度的快速查找,配合双向链表来维护访问顺序。
- LRU算法通过将最近使用的节点移动到链表头部,并在缓存满时删除链表尾部的节点,确保最近使用的数据留在缓存中,而最久未使用的数据被淘汰。
假设缓存容量为3,初始为空,然后依次执行以下操作:
-
插入 Key=1, Value=A:
- 哈希表添加
1 -> Node(1, A)
。 - 双向链表:
Head <-> Node(1, A) <-> Tail
。
- 哈希表添加
-
插入 Key=2, Value=B:
- 哈希表添加
2 -> Node(2, B)
。 - 双向链表:
Head <-> Node(2, B) <-> Node(1, A) <-> Tail
。
- 哈希表添加
-
插入 Key=3, Value=C:
- 哈希表添加
3 -> Node(3, C)
。 - 双向链表:
Head <-> Node(3, C) <-> Node(2, B) <-> Node(1, A) <-> Tail
。
- 哈希表添加
-
访问 Key=1(访问 Key=1 的节点):
- 将
Node(1, A)
移动到链表头部。 - 双向链表:
Head <-> Node(1, A) <-> Node(3, C) <-> Node(2, B) <-> Tail
。
- 将
-
插入 Key=4, Value=D(此时缓存已满):
- 移除最久未使用的节点,即
Node(2, B)
。 - 从哈希表中删除Key 2。
- 插入新的
Node(4, D)
,并更新哈希表4 -> Node(4, D)
。 - 双向链表:
Head <-> Node(4, D) <-> Node(1, A) <-> Node(3, C) <-> Tail
。
- 移除最久未使用的节点,即
-
LRU体现:每次访问时,都将访问的节点移动到链表头部,确保链表尾部始终是最久未使用的数据。
深信服
- TCP沾包的现象、如何解决?
指在TCP传输中,接收方在一次读取操作中获取了多个发送方的数据包,导致数据粘在一起,无法正确区分每个数据包的边界。这通常发生在应用层,尤其是连续发送数据时,由于TCP是面向字节流的协议,不保证每次发送的数据包对应每次接收的数据包。
- 固定长度包: 每个数据包的长度是固定的,接收方根据预定义的长度进行拆分。
- 使用分隔符: 在每个数据包之间添加特殊的分隔符,接收方通过解析分隔符来区分数据包。
- 包头加长度信息: 在数据包的包头部分附加长度信息,接收方根据该长度读取完整的数据包。