软件开发面试题

 

  • C++基础
    • 指针/引用,封装/继承/多态,内存管理
    • 虚函数,new/malloc,语言对比
    • lambda,move
    • sort
  • 计算机网络
    • HTTP,HTTPS
    • TCP,UDP
  • Mysql
    • 关键字
    • 事务
    • 索引
    • 概念
    • 备份,日志
  • Redis
    • 锁【分布式锁】
    • 数据【底层,常用数据结构,redis 介绍】
    • 缓存【数据一致性,缓存雪崩...,过期删除,内存淘汰】
    • 持久化【持久化机制】
    • 集群【高可用】
  • 操作系统
    • 进程,线程

C++基础

指针/引用,封装/继承/多态,内存管理

【指针/引用】的区别 ?
【野指针/悬空指针】的区别 ?
【指针常量/常量指针】的区别 ?
【指针数组/数组指针】的区别 ?
【智能指针】的作用,种类 ?
【智能指针】的代码实现 ?
 面向对象的三大【特征】?
 如何用 C 实现 C++【继承】?
【组合/继承】的区别 ?
【多态】如何实现 ?
【重载/重写/隐藏】的区别 ?
 程序的【内存分区】是怎样的 ?
 结构体【内存对齐】的作用 ?
【内存泄漏】是什么 ?
【空类】的大小是多少 ?
【空类】的缺省函数有哪些 ?


【指针/引用】的区别 ?
指针是一个变量,存储的是一个地址;引用是一个别名,本质上和引用对象是同一个东西。
指针可以为空,可以不初始化;引用不能为空,且必须初始化。
指针没有类型检查,是不安全的;引用有类型检查,是安全的。
指针在初始化后可以改变指向;引用在初始化后不能改变指向。
通过 sizeof 指针时,获取的是指针变量的大小;通过 sizeof 引用时,获取的是引用对象的大小。
当指针作为参数传递时,函数会复制指针生成一个临时变量作为实参,临时变量和传入指针指向同一块地址空间;当引用作为参数传递时,不会生成临时变量,函数实参和传入引用是同一个变量。

【野指针/悬空指针】的区别 ?
野指针和悬空指针都是指针指向无效内存区域的指针,访问会导致未定义的行为。
野指针:指的是没有经过初始化的指针。为了避免,可以在定义指针后默认置为空。
悬空指针:指的是指向的内存已经被释放的指针。为了避免,可以使用智能指针,或在 free/delete 内存后及时将指针置为空。

【指针常量/常量指针】的区别 ?
指针常量:本质上是一个常量,而这个常量的值是一个指针,即指针指向的对象是不可变的,但指向对象的值是可以变的。形式为 int* const p,const 修饰的是指针 p,因此指针的指向不能变。
常量指针:本质上是一个指针,指向的对象是一个常量,因此指针的指向是可以变的,但是所指对象的值不可以变。形式为 int const* p,const 修饰的是指针指向的值 *p,因此指针指向的值不能变。

【指针数组/数组指针】的区别 ?
指针数组:本质上是一个数组,不过数组中的每个元素都是一个指针,指向不同的对象。形式为 int* p[10],元素类型为 int*
数组指针:本质上是一个指针,不过该指针指向一个数组。形式为 int (*p)[10],元素类型为 int

【智能指针】的作用,种类 ?
作用:智能指针本质上是一个类,这个类封装了一个指向动态分配对象的指针,智能指针负责这个动态分配对象的释放,避免了资源忘记释放而导致的内存泄漏问题。当所指对象的生命周期结束时,智能指针会自动的调用析构函数来释放资源。
种类
shared_ptr:它允许多个指针指向同一个对象,每当增加一个指针指向该对象,指针内部的引用计数器则会加一,每当减少一个指针指向该对象,指针内部的引用计数器则会减一,当引用计数器数值为零时,则代表没有指针指向该对象了,则自动释放动态分配的资源。
unique_ptr:它是一个独享所有权的指针,同时只能有一个指针指向对象,并拥有所指对象的资源。因此当指针被删除释放的时候,指针所指向的对象也会被删除释放。
weak_ptr:它主要是为了解决 shared_ptr 中存在的循环引用问题,这是因为两个指针间相互引用,那么指针内部的引用计数器永远不会降为零,就不会释放资源。它是一个对象的弱引用,指向对象但不会增加对象的引用计数,并且可以和 shared_ptr 相互转化。

【智能指针】的代码实现 ?

template<typename T>
class SharedPtr {
public:
   // 构造函数,初始化智能指针
   SharedPtr(T* ptr = nullptr): _ptr(ptr), _pcount(new int(1)) {}
   // 拷贝构造函数,共享同一块内存,引用计数 +1
   SharedPtr(const SharedPtr& s): _ptr(s._ptr), _pcount(s._pcount) {
   	(*_pcount)++;
   }
   // 赋值运算符重载
   SharedPtr<T>& operator=(const SharedPtr& s) {
   	if (this != &s) {
   		// 复制前,检查自己是否是最后一个指向先前某区域的指针,是的话释放之前指向的资源
   		if (--(*(this->_pcount)) == 0) {
   			delete this->_ptr;
   			delete this->_pcount;
   		}
   		// 复制其他的智能指针,现在指向一个新的对象
   		_ptr = s._ptr;
   		_pcount = s._count;
   		(*_pcount)++;
   	}
   	return *this;
   }
   // 解引用操作符重载
   T& operator*() {
   	return *(this->_ptr);
   }
   // 成员访问操作符重载
   T* operator->() {
   	return this->_ptr;
   }
   // 析构函数,处理引用计数,当为 0 时,释放资源。
   ~ SharedPtr() {
   	--(*(this->_pcount));
   	if (*(this->_pcount) == 0) {
   		delete _ptr;
   		_ptr = nullptr;
   		delete _pcount;
   		_pcount = nullptr;
   	}
   }
private:
   T* _ptr;			// 指向资源的指针
   int* _pcount;		// 指向引用计数的指针 
};

面向对象的三大【特征】?
封装:将属性和操作属性的方法捆绑在单元内部,而对外部隐藏实现细节,避免外界的干扰。比如学生是一个类,拥有姓名年龄等属性,吃饭睡觉等方法。
继承:让某种类型的对象获取另一个类型对象的属性和方法,并且可以在获取内容的基础上进行扩展。比如定义一个高中生类,它可以继承学生类中的所有属性和方法,并且增加一些特定的操作,比如考试。
多态:让不同的对象在使用相同的方法时,可以表现出不同的响应。比如对于一个学生基类,有高中生,小学生等派生类,那么调用同样的考试方法,响应的内容是不一样的。

如何用 C 实现 C++【继承】?

// C++ 中的继承,多态
struct A {
    virtual void func() {
    	cout << "AAA!" << endl;
    }
    int a;
};
// B 继承 A
 struct B : public A {
    virtual void func() {
    	cout << "BBB!" << endl;
    }
    int b;
};
// 测试
A a;					// 定义一个父类对象a
B b;					// 定义一个子类对象b
A* p1 = &a;				// 定义一个父类指针指向父类的对象
p1->func();				// 调用父类的同名函数 AAA!
p1 = &b;				// 让父类指针指向子类的对象
p1->func();				// 调用子类的同名函数 BBB!
// C 模拟实现继承
typedef void (*FUNC)();			// 定义一个函数指针来实现对成员函数的继承
struct _A {						// 父类
   FUNC _func;					// 由于 C 中结构体不能包含函数,故只能用函数指针在外面实现
   int _a;
}
typedef void (*FUNC)();			// 定义一个函数指针来实现对成员函数的继承
struct _B {						// 子类
   _A _a_;						// 在子类中定义一个父类的对象即可实现对父类的继承
   int _b;
}
void _fA() {					// 父类的同名函数
   printf("AAA!");
}
void _fB() {					// 子类的同名函数
   printf("BBB!");
}
// 测试
_A _a;							// 定义一个父类对象_a
_B _b;							// 定义一个子类对象_b
_a._func = _fA;					// 父类的对象调用父类的同名函数
_b._a_._func = _fB;				// 子类的对象调用子类的同名函数
_A* p2 = &_a;					// 定义一个父类指针指向父类的对象
p2->func();						// 调用父类的同名函数 AAA!
p2 = (_A*)&_B;					// 让父类指针指向子类的对象,由于类型不匹配所以要进行强转
P2->_func();					// 调用子类的同名函数 BBB!

【组合/继承】的区别 ?
组合是 has-a 的关系,即整体与部分的关系,当前对象可以把其他对象当作自己的成员变量。比如 X 是 Y 的组合成员,那么 X 是部分,Y 是整体;继承是 is-a 的关系,即特殊和一般的关系,子类可以重写父类的方法并对父类进行扩展。比如 X 继承于 Y,那么 X 是特殊,Y 是一般。
组合中当前对象只能通过所包含的对象调用其方法,其内部细节对外是不可见的;继承中父类的内部细节是对子类可见的。
组合中当前对象与所包含对象是低耦合的关系,修改所包含对象代码不会影响当前对象;继承中父类和子类对象是高耦合的关系,父类修改后子类需要相应的修改。

【多态】如何实现 ?
概念:多态是指让不同的对象在使用相同的方法时,可以表现出不同的响应。
实现
编辑器在发现基类中有 virtual 修饰的虚函数时,会为每个含有虚函数的类生成一份虚表,其中存放着一系列的虚函数地址。
编辑器会在对象中保存一个虚表指针指向这个虚表,从而在实际调用中找到对应的函数。
当派生类创建对象时,会自动调用构造函数,在构造函数中创建虚表,初始化虚表指针。
当派生类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表,当派生类对基类的虚函数进行重写时,派生类的虚表指针将指向自身重写的虚表。因此可以根据当前对象的虚表指针进行相应函数的调用,实现多态。

// 基类
class Base {
public:
	virtual void func() {
		cout << "Base func ~" << endl;
	}
};
// 派生类 A
class SubA : public Base {
public:
	virtual void func() {
		cout << "SubA func ~" << endl;
	}
};
// 派生类 B
class SubB : public Base {
public:
	virtual void func() {
		cout << "SubB func ~" << endl;
	}
};
// 使用
Base* base = new Base;
base->func();		// Base func ~	
Base* subA = new SubA;
subA->func();		// SubA func ~
SubB* subB = new SubB;
subB->func();		// SubB func ~

【重载/重写/隐藏】的区别 ?
重载:是指在同一作用域下,定义多个同名但是参数列表不同的函数,其中参数的类型和个数应该不同。
重写:是指在子类中,定义一个与父类中同名且参数列表,返回值均相同的函数,覆盖掉父类中的虚函数。
隐藏:是指在子类中,定义一个与父类中同名的函数,但是参数列表,返回值可以不同,使得父类中的同名函数被隐藏。

 程序的【内存分区】是怎样的 ?
程序的内存分区主要分为 5 个部分,从高到低分别是栈区,堆区,全局数据区,常量区和代码区。
栈区:在执行函数时,函数内局部变量的存储单元在栈上创建,函数执行结束时,这些变量的存储单元被释放。栈的内存分配运算有操作系统负责,效率高,但是分配的内存容量有限。
堆区:由 new 操作符所分配的内存块,都存放于堆中,但是这些内存的释放需要手动去控制,一个 new 对应一个 delete 释放。堆中的效率低,但是可以分配的内存容量大。
全局数据区:里面存放着全局变量和静态变量,如果有些变量没有初始化,那么在这里会进行自动的初始化。
常量区:里面存放着常量,不允许修改。
代码区:里面存放的是二进制的代码。

 结构体【内存对齐】的作用 ?
原因:CPU 访问数据时,并不是逐字节的访问,而是以字长为单位进行访问,比如在 32 位系统中,字长为 4 字节。这样设计可以提高 CPU 访问内存的效率,比如同样是访问 8 字节的数据,以字长为单位读取仅需要两次即可,而逐字节访问需要 8 次。
规则
结构体中的成员,会按照声明的顺序进行存储,而且第一个成员的地址就是结构体的地址。
结构体中的成员,一定会存放在整数倍自身大小的偏移量上,比如 int 只会方在 4 的倍数上。
结构体的总大小为最大对齐数的整数倍,而最大对齐数与最大的成员大小有关。
可以通过 alignof 查看当前对齐数,alignas 指定对齐数,或通过 pragma back 指定对齐数。

【内存泄漏】是什么 ?
原理:内存泄漏通常是指堆中内存的泄漏,应用程序在使用如 malloc,realloc,new 等函数分配到堆中内存时,使用完毕后应该立即使用 free,delete 释放该内存,否则这块内存就无法被再次使用,造成内存泄漏。
避免方法
可以采用记数法,当使用 new 或 malloc 时,让计数加一,delete 或 free 时,让计数减一,程序执行完毕后打印计数,如果计数不为 0 则存在内存泄漏。
虚构函数需要声明为虚函数,防止父类指针指向子类对象时,编辑器实施静态绑定只会调用父类的析构函数,而造成子类的析构不完全,造成内存泄漏。
养成 new,delete 和 malloc,free 配对的习惯。
通过 new 构造的对象数组,需要使用 delete[] 删除。

【空类】的大小是多少 ?
空类的大小为 1,编辑器会给空类中添加一个最小的字节作为区分。
空类中增加一个函数后,大小还是 1,因为对象在调用成员函数时,编辑器可以确定成员函数的地址,成员函数会绑定到 this 指针完成调用,所以不需要存储成员函数的信息。
空类中增加一个虚函数后,大小为 8,因为存在虚函数时,对象中会创建虚表指针指向虚函数表,这个指针大小为 8。

【空类】的缺省函数有哪些 ?
构造函数, Empty();
拷贝构造函数, Empty(const Empty& emp);
析构函数, ~Empty();
赋值运算符, Empty& operator=(const Empty& emp);

虚函数,new/malloc,语言对比

【构造函数】为啥不能为【虚函数】?
【析构函数】为啥需要为【虚函数】?
【纯虚函数/虚函数】的区别 ?
【基类虚表】存放在内存中哪,何时初始化 ?
【静态函数】可以为【虚函数】吗?


【构造函数】为啥不能为【虚函数】?
从虚表指针角度看:虚函数的调用需要用到虚函数表,而虚函数表由类的虚表指针指向,这个虚表指针需要调用构造函数才能初始化。如果构造函数是虚函数的话,那么就需要用虚表指针在虚函数表里面找构造函数,但此时根本就没有虚表指针。
从多态的角度看:虚函数的作用主要是为了实现父类的指针或引用调用子类的成员函数,但是在构造对象时,构造函数是被自身自动调用的,不能通过父类的指针或引用调用,因此构造函数设为虚函数无意义。

【析构函数】为啥需要为【虚函数】?
如果析构函数为虚函数,当基类指针可以指向派生类的对象,如果删除基类的指针,就会调用所指派生类对象的析构函数,而派生类的析构函数又会调用基类的析构函数,整个派生类的对象被完全释放。
如果析构函数不为虚函数,则编辑器实施静态绑定,当删除基类指针时,只会调用基类的析构函数而不会调用派生类的析构函数,造成派生类析构不完全,导致内存泄漏。

【纯虚函数/虚函数】的区别 ?

lambda,move

请介绍下【lambda 】?
请介绍下【move】?


请介绍下【lambda 表达式】?
lambda 表示式可以编写内嵌的匿名函数,用来替换独立的函数或函数对象,增加代码的可读性。
每当定义一个 lambda 表达式后,编辑器会自动生成一个匿名类,称之为闭包类型。那么在运行时,这个 lambda 表达式就会返回一个匿名的闭包实例。因此可以通过传值或引用的方式捕获其封装作用域中的变量。
lambda 表达式的语法定义:

int x = 10;
int add_num = [&x](int y) -> int { return x + y; };
cout << add_num(5);						// 结果为 15
返回类型 闭包实例 = [捕获变量](传入参数) -> 返回类型 {函数体};

请介绍下【move】?
作用:move 是一种高效的资源转移机制,相当于转移了资源的所有权,这样可以让待销毁对象的资源转移到其他变量,避免不必要的拷贝操作,提高程序的性能。
场景:比如在标准库中,vector 的 push_back 操作,会对参数的对象进行临时复制,有额外的对象创建。然而使用 move 可以强制将一个左值转换为右值引用,不会构建临时对象而是直接交换对象资源,因此可以提高效率。

class MyClass {
public:
	MyClass(int x) : data(new int(x)) {}
	// 移动构造函数,接受右值引用 &&
	MyClass(MyClass&& other) : data(other.data) {
		// 将之前被移动对象的数据成员指针置为空
		other.data = nullptr;
	}
private:
	int* data;
};
// move 使用
Myclass a(1);
Myclass b(move(a));

sort

[** 在【sort】中主要基于什么排序算法 ?**](#C++_Sort_01)

在【sort】中主要基于什么排序算法 ?
sort 中采用的是一种 IntroSort 内省式排序的混合排序算法。
首先判断排序的元素个数是否大于阈值,通常这个阈值为 16,如果元素个数小于阈值则直接使用插入排序。因为当序列中大多元素有序时,采用插入排序会更快。
如果元素个数大于阈值时,判断序列递归的深度会不会超过 2*log(n),如果递归深度没有超过阈值时就使用快速排序。
如果递归深度超过阈值时,那么快排的复杂度就退化了,这时候就采用堆排序,因为堆排序的复杂度是 nlog(n),比较稳定。

计算机网络

HTTP,HTTPS

浏览器输入 url 回车后会涉及哪些技术?【HTTP】
HTTP 长连接和短链接的区别?【HTTP】
HTTP 请求有哪些?【HTTP】
HTTP 请求和响应报文有哪些字段?【HTTP】
HTTP 缓存中的私有和公有字段?【HTTP】
HTTP 如何保证缓存是最新的?【HTTP】
HTTP 的状态码有哪些?【HTTP】
HTTP 和 HTTPS 的区别?【HTTP,HTTPS】
HTTPS 采用的加密方式有哪些?【HTTPS】
HTTP 请求可以同时发送多个吗?【HTTP】

浏览器输入 url 回车后会涉及哪些技术?

  • 解析 URL:浏览器首先会解析输入的 URL,从而生成发送给服务器的请求消息。
  • DNS 解析:查询服务器域名对应的 IP 地址,浏览器先会查询本地缓存,如果没有的话就向 DNS 服务器发送查询请求。
  • 建立连接:根据查询到的 IP 地址,向服务器三次握手建立 TCP 连接与服务器保持通信。
  • 发送请求:浏览器向服务器发送 HTTP 请求,比如 GET 获取用户想要的资源。
  • 处理请求:服务器会对请求的内容进行处理,比如查询数据库获取数据。
  • 服务器响应:服务器处理完成后会发送 HTTP 响应返回给浏览器,其中包含了请求资源的内容。
  • 浏览器渲染:浏览器根据返回的响应,解析内容并渲染页面,最终呈现在屏幕上。

HTTP 长连接和短连接的区别?

  • 在 HTTP/1.0 中默认的是短链接,客户端每发送一次 HTTP 请求,都会经历连接断开的操作。
  • 而从 HTTP/1.1 开始,默认使用长连接,客户端发送多次 HTTP 请求,和服务端始终保持连接的状态,除非有一端提出断开或者长时间未发生活动超时断开。

HTTP 请求有哪些?

  • GET:发送一个请求从服务器中获取资源,最终返回给客户端。可以用在登陆时获取服务器的账号密码进行验证,下载图片,视频等方面。
  • POST:向服务器指定的资源提交数据,请求进行处理。可以用在注册,修改用户信息等方面。
  • HEAD:判断某个资源是否存在,没有返回体。可以用在查询某些容易过期的文件是否还过期。
  • OPTIONS:查询支持的请求方法。
  • PUT:向服务器指定路径的资源提交数据,替换掉该路径的文件,和 POST 类似。
  • DELETE:删除服务器中指定的资源。
  • TRACE:回显服务器收到的请求,主要用于诊断和测试。
  • CONNECT:要求服务器在通信时使用隧道进行 TCP 通信。

HTTP 请求和响应报文有哪些字段?
请求报文:

  • 请求行:包含请求方法,URL。
  • 请求头部:包含 Host,Cookie,Authorization 等信息。
  • 请求体:包含 POST 请求的数据。

响应报文:

  • 状态行:HTTP 版本,状态码。
  • 响应头部:Content-type,Content-length 等信息。
  • 响应体:包含服务器返回的数据。

HTTP 缓存中的私有和公有字段?
HTTP 中的缓存控制通过一系列字段来实现,其中可以分为公有和私有两类。

  • Cache-control 中 private 指令规定将资源作为私有缓存,仅被单用户使用,存放在用户浏览器中。
  • Cache-control 中 public 指令规定将资源作为公有资源,可以被多用户使用,存放在服务器中。

HTTP 如何保证缓存是最新的?
HTTP 通过使用头部字段来控制缓存,确保资源是最新的。

  • Cache-control 中的 max-age 指令可以设置缓存资源的最大存活时间。
  • expires 字段设置了一个绝对时间,过了时间之后资源将会过期。
  • Cache-control 中的 no-store 指令可以强制禁用缓存资源。
  • Cache-control 中的 no-cache 指令要求缓存服务器必须先向源服务器验证缓存资源的有效性,只有资源有效时才使用缓存。

HTTP 的状态码有哪些?

  • 1xx:信息状态码,表示接收的请求正在处理,比如 100 代表一切正常。
  • 2xx:成功状态码,表示请求被正常处理,比如 200 代表成功。
  • 3xx:重定向状态码,表示需要附加的操作来完成请求,比如 301 代表永久重定向,访问的资源已经被转移到其他位置。
  • 4xx:客户端错误码,代表客户端发送的请求中存在错误,比如 404 代表访问资源不存在。
  • 5xx:服务端错误码,代表服务端处理请求时出现错误,比如 503 代表服务器繁忙,无法处理请求。

HTTP 和 HTTPS 的区别?

  • HTTP 协议传输的数据时明文,未加密的,涉及隐私泄漏不安全;HTTPS 协议采用 SSL+HTTP 构建的可加密传输,因此更加安全。
  • HTTP 连接简单,仅需要 TCP 三次握手后就可以进行报文传输;而 HTTPS 在 TCP 三次握手后,还需要经历 SSL/TLS 握手过程,才可以进行加密传输。
  • HTTP 默认端口是 80;HTTPS 默认端口是 443。
  • 使用 HTTPS 协议的服务端需要向 CA 来申请数字证书,来保证服务器的身份是可信的。

HTTPS 采用的加密方式有哪些?
HTTPS 采用混合的加密策略,首先采用非对称密钥,加密用于传输的对称密钥,之后采用对称密钥加密通信,保证通信过程中的效率。

  • 客户端给出协议版本号,客户端生成的随机数,以及客户端支持的加密算法。
  • 服务端确认双方使用的加密算法,给出数字证书,以及服务器生成的随机数。
  • 客户端确认数字证书有效,再次生成一个随机数,并使用数字证书中的公钥,加密这个随机数发送给服务端。
  • 服务端使用自己的私钥解密,获取客户端加密的随机数。
  • 客户端和服务端约定加密方法,使用当前的三个随机数,生成对话密钥,加密之后的整个通信过程。

HTTP 请求可以同时发送多个吗?

  • HTTP/1.1 在单个时刻,同一连接只能处理一个请求,两个请求的声明周期不能重叠。
  • HTTP/2 引入了多路复用,允许在同一个连接上并行处理多个请求。
  • 此外,可以尝试对同一个服务器建立多个 TCP 连接,分别处理请求。

TCP,UDP

TCP 头部格式有哪些?【TCP】
为什么需要 TCP,其工作在哪一层?【TCP】
什么是 TCP?【TCP】
如何唯一确定一个 TCP 连接?【TCP】
TCP 和 UDP 的区别?【TCP,UDP】
TCP 和 UDP 可以使用同一个端口吗?【TCP,UDP】
TCP 三次握手是怎样的?【TCP,握手】
如何在 Linux 中查看 TCP 状态?【TCP】
为什么是三次握手,而不是二或四次?【TCP,握手】
为什么 TCP 要求随机初始化序列号?【TCP】
IP会分片,为什么 TCP 还需要 MSS?【TCP】
什么是 SYN 攻击,如何避免?【TCP,SYN攻击】
TCP 四次挥手是怎样的?【TCP,挥手】
为什么挥手需要四次?【TCP,挥手】
为什么 TIME_WAIT 等待时间为 2MSL?【TCP】
TCP 粘包和拆包是什么?【TCP,粘包拆包】
TCP 拥塞控制有哪些?【TCP,拥塞控制】
TCP 超时重传和快速重传?【TCP,重传】

TCP 头部格式有哪些?

  • 源端口和目的端口(32bit):发送方和接收方的端口号。
  • 序列号(32bit):发送方为每个发送的字节编号,这个编号就是序列号。序列号主要用于解决乱序的问题。
  • 确认号(32bit):接收方在 TCP 头部中指定的期望接收的下一个序列号。同时告诉发送方,当前已接收确认号之前的所有数据。确认号用来解决丢包的问题。
  • 首部长度(4bit):记录 TCP 头部的长度为多少个 4 byte。
  • 标志位(6bit):常用的是 ACK,SYN,FIN,RST,此外还有 PSH 和 URG。
  • 窗口(16bit):告知发送方当前缓存还可以接收多少数据,用于流量控制。
  • 校验和(16bit):接收端检测收到的报文有无损坏。

为什么需要 TCP,其工作在哪一层?

  • 因为 IP 层是不可靠的,它不能保证数据包的交付,不能保证数据包的按序和完整的交付。
  • 为了保障数据包传输的可靠性,需要通过传输层的 TCP 协议来负责。
  • TCP 是一种可靠的数据传输协议,它能够保证接收端收到的数据无损坏,无冗余,按序的到达。

什么是 TCP?

  • TCP 是一种面向连接的,可靠的,基于字节流的传输层通信协议。
  • 其中面向连接表示传输前需要建立连接;可靠代表确保数据能够安全有序到达接收端;基于字节流表示数据被视为一连串的字节流,并且没有消息边界,TCP 负责对这个字节流进行维护。

如何唯一确定一个 TCP 连接?

  • 通过 TCP 四元组可以唯一确定一个连接,分别为:源地址,源端口,目的地址,目的端口。
  • 源地址和目的地址(32bit),存放在 IP 头部,通过 IP 协议发送报文给对方主机。
  • 源端口和目的端口(16bit),存放在 TCP 头部,通过 TCP 协议发送报文给对应端口的进程。

TCP 和 UDP 的区别?

  • 连接:TCP 是面向连接的协议,传输前需要先连接;UDP 不需要连接,可以直接传输。
  • 服务对象:TCP 是一对一的点到点服务;UDP 支持一对一,一对多,多对多。
  • 可靠性:TCP 是可靠的,数据可以无差错,有序,完整的传输;UDP 是尽最大努力交付,并不可靠。
  • 拥塞控制,流量控制:TCP 拥有拥塞控制和流量控制机制;UDP 没有。
  • 首部开销:TCP 首部长,开销较大;UDP 首部只有 8 字节,开销较小。
  • 传输方式:TCP 基于字节流传输,无边界,但保证有序;UDP 是一个包一个包传输,有边界,但可能会丢包乱序。
  • 分片:TCP 数据大小如果超过 MSS,则会在传输层进行分片,目标主机收到后,在传输层进行组装;UDP 数据大小如果大于 MTU,则会在 IP 层进行分片。目标主机收到后,在 IP 层进行重组。
  • 应用场景:TCP 是面向连接且可靠的,因此主要用于像文件传输这样的场景;UDP 是无连接不可靠但快速的,因此可以用于像音视频通话这样的场景。

TCP 和 UDP 可以使用同一个端口吗?

  • 传输层中的端口号主要是为了区分同一台主机上的不同应用程序。
  • 而 TCP 和 UDP 在内核中是两个完全独立的软件模块,当主机收到数据包后,可以根据 IP 包头的协议号区分 TCP/UDP,并交给相应的模块处理。

TCP 三次握手是怎样的?

  • 最开始,客户端和服务端都处于 CLOSE 状态,于是服务端开始监听某个端口,处于 LISTEN 状态。
  • 客户端请求连接,发送 SYN 报文给服务端。这时候客户端进入 SYN_SEND 状态。
  • 服务端收到 SYN 报文后,发送 ACK 和 SYN 报文,表示自己已经收到,也愿意连接。这时候服务端进入 SYN_RECV 状态。
  • 客户端收到报文后,发送最后的 ACK 报文,直接进入 ESTABLISH 状态,服务端收到 ACK 后随即也进入 ESTABLISH 阶段。

如何在 Linux 中查看 TCP 状态?

  • 可以通过 netstat -napt 查看。

为什么是三次握手,而不是二或四次?

  • 避免历史连接:当网络拥塞时,客户端会发送多个 SYN 报文,其中旧的 SYN 可能比新的 SYN 先到,那么通过比对确认号有误,客户端可以在第三次握手时发送 RST 重连请求,避免历史连接。
  • 同步双方初始序列号:客户端发送 ACK 给服务端,告诉客户端发送的多少数据已经被接收,同样的三次握手就是客户端告诉服务端,当前发送的多少数据已被接收。这样一来一回双方才能确保序列号够可靠的同步。
  • 避免资源浪费:如果没有三次握手,服务端不知道客户端是否收到了 ACK,因此只能主动建立一个连接。万一网络冗余,客户端会重复发送多个 SYN,服务端会建立多个冗余的连接,造成资源浪费。
  • 其中二次握手无法防止历史连接的建立,双方资源会浪费,序列号也无法同步;四次握手没有必要,因为第二次握手中的 SYN 和 ACK 包可以同时发送,不需要使用更多的通信次数。

为什么 TCP 要求随机初始化序列号?

  • 为了防止历史报文被下一个相同的四元组接收。
  • 为了安全性,防止黑客伪造相同序列号的报文被对方接收。

IP会分片,为什么 TCP 还需要 MSS?

  • 因为 IP 没有超时重传机制,所以当一个 IP 分片丢失,那么所有的 IP 分片都需要重传,非常低效。
  • 如果 TCP 进行分片,因为有超时重传机制,所以只需要重新传输丢失的 TCP 分片即可。

什么是 SYN 攻击,如何避免?

  • TCP 连接需要三次握手,假设攻击者短时间内伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 就会进入 SYN_RECD 状态,但服务端发出的 SYN+ACK 却始终得不到回应,因此会占满服务器的半连接队列,使服务器不能为正常用户服务。

解决方法:

  • 增大 TCP 半连接队列的容量。
  • 减少 SYN+ACK 的重传次数。
  • 开启syncookie 功能,当半连接队列已满时,使用 cookie 来建立连接。

TCP 四次挥手是怎样的?

  • 最开始,客户端和服务端都处于 ESTABLISH 状态,当客户端想要关闭连接时,会发送一个 FIN 报文给服务端,此时客户端进入 FIN_WAIT_1 状态。
  • 服务端收到 FIN 报文后,向客户端发送 ACK 报文,此时服务端进入 CLOSE_WAIT 状态。
  • 客户端收到 ACK 报文后,进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,服务端向客户端发送 FIN 报文也请求连接,此时进入 LAST_ACK 状态。
  • 客户端收到 FIN 报文后,回应 ACK 报文给服务端,之后进入 TIME_WAIT 状态。
  • 服务端收到 ACK 报文后,直接进入 CLOSE 状态,此时服务端已经关闭连接。
  • 客户端经过 2MSL 时间后,自动进入 CLOSE 状态,此时客户端也已经关闭连接。

为什么挥手需要四次?

  • 因为客户端向服务端发送 FIN 包,表明客户端不再发送数据了,但是此时服务端可能还有数据需要发送。
  • 因此服务端会先回复 ACK 报文告知已收到信息,等服务端数据处理完毕后再发送 FIN 报文。

为什么 TIME_WAIT 等待时间为 2MSL?

  • 这是为了保证客户端发送的最后一个 ACK 报文能够达到服务端。因为最后的 ACK 可能会丢失,使得服务端接收不到 ACK 而重发 FIN+ACK 报文,接着客户端重新发送 ACK 并且计时器重置为 2MSL,这样可以确保双方都进入 CLOSE 状态。
  • MSL 为最大报文生存时间,经过 2MSL 后,在本连接中产生的所有数据包都已经消息,这样可以防止旧的数据包对新的连接产生干扰。

TCP 粘包和拆包是什么?
粘包:如果一次请求发送的数据量过小,没达到缓冲区大小,TCP 会将多个请求合并为同一个请求进行发送,发生了粘包。在缓冲机制,拥塞控制这样的场景下可能会出现。
拆包:如果一次请求发送的数据量过大,超过了缓冲区大小,TCP 会将其拆分多次进行发送,发生了拆包。

  • 发送端将每个包都封装成固定的长度进行传输。
  • 发送端在每个包的末尾添加固定的符号作为分隔符,后续根据分隔符合并。
  • 将消息分为消息头和消息体,其中头部存放整个消息的长度。
  • 通过自定义的协议来对粘包拆包情况进行处理。

TCP 拥塞控制有哪些?

  • 慢启动算法:TCP 在刚建立连接时,首先有个慢启动的过程,就是一点一点的提高发送数据包的数量。每当发送方收到一个 ACK,拥塞窗口就 +1。

    1. 假设初始化拥塞窗口为 1,表示可以传输一个 MSS 大小的数据。
    2. 当收到一个 ACK 后,cwnd+1,此时一次可以传输两个 MSS 大小的数据。
    3. 当再次收到两个 ACK 后,cwnd+2,此时一次可以传输四个 MSS 大小的数据。
    4. 慢启动有一个门限,当 cwnd 小于门限时使用慢启动算法,大于门限时就会使用拥塞避免算法
  • 拥塞避免算法:每当收到一个 ACK,cwnd 会增加 1/cwnd。

    1. 比如慢启动的阈值为 8 时,初始会收到 8 个 ACK,这时 cwnd 会增加 1/8 * 8 的大小,也就是增加 1。
    2. 接着一次可以传输 9 个 MSS 大小的数据,cwnd 呈现线性的增长。
    3. 当线性增长出现拥塞后,会进入拥塞发生算法
  • 拥塞发生算法:产生拥塞时数据包会进行重传,分为超时重传和快速重传。

    1. 当发生超时重传时,阈值会设为当前拥塞窗口的一半,窗口大小设置为 1。
    2. 重新进入慢启动阶段。
    3. 当发生快速重传时,阈值和窗口大小都设为当前窗口值的一半,进入快速恢复算法
  • 快速恢复算法:此时还能收到 3 个 重复的ACK,说明网络不是特别的糟。

    1. 首先将拥塞窗口 +3,重传丢失的数据包。
    2. 如果再收到重复的 ACK,cwnd+1。
    3. 如果收到新的数据包,表示重传的包接收成功,把 cwnd 设置为阈值大小,进入拥塞避免算法

TCP 超时重传和快速重传?
超时重传:在发送数据时,设置一个定时器,当超过指定的时间后,没有收到对方的 ACK 报文,就会重发该数据。一般发生在数据包丢失,ACK 报文丢失的情况。
快速重传:当发送方收到接收方发送的三个相同的 ACK 时,得知有数据丢失,于是重新传输丢失的数据包。

Mysql

关键字

关键字【delete,drop,truncate】的区别?
关键字【char】和【varchar】的区别?

【Delete,drop,Truncate】的区别?

  • Delete:用于删除表中的全部,或一部分数据。执行后,用户需要 commit 或 rollback 来完成删除或撤销操作。
  • Drop:用于从数据库中删除表,所有的数据行,索引都会被删除,且不能回滚。
  • Truncate:用于删除表中的所有数据,但是没有删除索引,本质上相当于删除表后有创建了个新表,这个操作也不能回滚。

【char】和【varchar】的区别?

  • char 是长度不可变的,会用空格填充到指定长度的大小;varchar 的长度是可变的。
  • char 的存取速度比 varchar 更块。
  • char 的存储是对英文字符占 1 字节,而对中文字符占 2 字节;varchar 对中文还是英文字符都占 2 字节。

事务

事务的【特性】?
事务的【隔离级别】?
并发事务的【脏读,不可重复读,幻读】?

事务的【特性】?

  • 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会在中间某个阶段结束。如果中间发生错误,则会 rollback 到事务开启前的状态。
  • 一致性:事务开始前和结束后,数据库的完整性没有被破坏。
  • 隔离性:当多个用户同时访问数据库时,多个事务之间应该相互隔离,互不干扰。
  • 持久性:事务结束后,对数据的修改是永久的,即使故障也不会丢失。

事务的【隔离级别】?

  • 读未提交:一个事务还没有提交时,它做的修改可以被其他事务看到。会出现脏读,不可重复读,幻读现象。
  • 读提交:一个事务提交时,它做的修改才能被其他事务看到。会出现不可重复读,幻读现象。
  • 可重复度:一个事务执行过程中看到的数据,应该始终保持一致。会出现幻读现象。
  • 串行化读:当多个事务对同一记录进行读写时,会为记录加上读写锁,只有前一个事务处理完成后,才可以执行下一个事务。

并发事务的【脏读,不可重复读,幻读】?

  • 胀读:如果一个事务读取到了另一个 [读未提交] 事务修改的数据,则会发生脏读现象。比如账户里现在有 100,A 事务存了 100,此时余额为 200,但发生了未知的错误,需要回滚。B 事务查询余额为 200,发生了脏读。
  • 不可重复读:如果一个事务多次读取同一个数据,结果读取的数据不一样,则发生了不可重复读现象。比如账户里有 100,A 事务查询余额也为 100,之后干别的事情了,同时 B 事务存了 100,A 事务重新查询了余额,发现变成了 200,两次查询结果不一致,发生了不可重复读。
  • 幻读:如果一个事务多次查询符合某条件的记录数量,结果查询的记录数量不一致,则发生了幻读现象。比如 A 事务查询年龄大于 20 岁的用户,查出来是 100 条数据,接着 B 事务新添加了 100 条数据,A 事务再次查询发现符合条件的数据变为了 120,那么就是发生了幻读。

索引

索引为何使用【B+树】?
索引的【优点】?
索引中【聚集索引】和【非聚集索引】的区别?
索引的【注意事项】?

索引为何使用【B+树】?

  • Mysql 中的数据是放在磁盘的,读取数据会有访问磁盘的操作,而访问磁盘的 IO 操作效率很低。
  • 二分查找树可以快速地查找到数据,但是极端情况下,比如依次插入递增的元素,那么查询数据的复杂度会从 O(logn) 退化为 O(n)。
  • 自平衡二叉树是高度平衡的,确保复杂度为 O(log n),但是当数据量过大时,树的高度依然很高,磁盘 IO 次数也更多。
  • 使用 B 树和 B+ 树时,一个节点有多个子节点,因此树的高度会变低,磁盘 IO 次数也会降低,查询效率更高。

B+ 树和 B 树的对比

  1. B+ 树中的叶子节点存放数据,非叶子节点仅存放索引,在相同数据量下,B+ 数的非叶子节点可以存放更多的索引,因此高度比 B 树更低,查询效率也更高。
  2. B+ 树中有着大量的冗余索引,这些冗余索引可以让 B+ 树在插入删除时的效率更高,数的结构也不会发生太复杂的变化;
  3. B+ 树的叶子节点之间用链表连接了起来,有利于范围查询,而 B 树只能通过前序遍历来范围查询。

B+ 树和 hash 表的对比

  1. 采用哈希表的查询复杂度为 O(1),速度确实更快,但数据库中可能查询多条连续数据,由于 B+ 树的有序性,进行范围查询时会比 hash 表更快。
  2. hash 表需要把数据全部加载到内存中,非常消耗内存,而采用 B+ 树可以按节点分段加载,内存消耗更少。

B+ 树和红黑树的对比

  1. 红黑树属于一种平衡二叉树,它没有实现绝对平衡而是选择实现局部平衡,使得查找,删除,插入的复杂度都为 O(log2 n)。但它本质上还是二叉树,当数据量过大时高度还是会很高,磁盘 IO 次数会增加导致效率变低。
  2. 当进行连续数据查询时,红黑树也需要通过前序遍历来范围查询,效率不如 B+ 树。

索引的【优点】?

  • 索引是是一种数据库结构,由数据表中的一列或多列组成,可以快速查询数据表中具有某一特点的记录。
  • 通过创建唯一索引可以保证数据表中每一行数据的唯一性。
  • 使用索引可以大大加快数据的查询速度。
  • 在使用分组和排序子句进行查询时,也可以显著减少查询中分组和排序的时间。

索引中【聚集索引】和【非聚集索引】的区别? ref01

  • 聚集索引记录的数据在物理上是连续的;非聚集索引记录的数据在逻辑上是连续的,但是在物理上是不连续的。
  • 聚集索引的叶子节点就是数据节点;非聚集索引的叶子节点仍然是索引节点,该索引节点包含指向对应数据块的指针。
  • 聚集索引的作用是缩小查询的范围,可以直接查询到需要查找的数据;非聚集索引可以查找到记录对应的主键,根据主键再通过聚集索引查找到数据。
  • 聚类索引一张表只能有一个;非聚类索引一张表可以有多个。

索引的【注意事项】?

  • 不要在列上使用函数,这将会导致索引失效而进行全局扫描
  • 避免在 where 子句中使用 !=,<> 等否定操作符,这也会导致索引失效而进行全表扫描。
  • 多个单列索引不能提高数据的查询性能,应该改成复合索引。
  • 复合索引应该遵循最左前缀原则,即在查询中使用了复合索引的第一个字段,索引才会被使用。
  • 索引的列中不会包含 NULL 值,对于复合索引,只要某列出现 NULL 值,该列对于复合索引就是无效的。
  • 当查询条件左右两端的数据类型不匹配时,会进行隐式类型转换,这可能会导致索引失效而全局扫描。
  • 使用 like 进行模糊查询时,查询只有右边有 % 的数据时支持索引,但是查询左右两边都有 % 的数据时就会导致索引失效进行全表扫描。

mysql 的【行级锁】有哪些?
mysql 的【悲观锁】和【乐观锁】是什么?

mysql 的【行级锁】有哪些?

  • 在读提交隔离级别下,行级锁的种类只有 [记录锁]。
  • 在可重复读隔离级别下,行级锁的种类有 [记录锁,间隙锁,记录锁+间隙锁]。
  • 记录锁:仅仅锁定一条记录。
  • 间隙锁:锁定一个范围,但是不包括记录本身。
  • 记录锁+间隙锁:锁定一个范围,同时也锁定记录本身。

mysql 的【悲观锁】和【乐观锁】是什么?

  • 悲观锁:当执行并发任务且认为数据容易被同时修改时,那么需要在操作前先加上锁,再执行操作。它主要是为了提高数据操作的安全性,但是会降低性能。
  • 实现方式:在对记录进行修改前,先为该记录添加上排他锁,如果加锁失败,那么该记录正在被修改,当前操作进行等待。如果加锁成功,那么就可以对记录进行修改,完成后释放锁。
  • 乐观锁:当执行并发任务时,认为数据被同时修改的概率非常低时,就先直接进行修改不加锁,事后再检查有没发生冲突,如果发生冲突则返回错误信息并进行对应处理。
  • 实现方式:乐观锁通常可以使用 CAS 技术实现。当多个线程尝试使用 CAS 同时更新一个变量时,只有其中一个线程可以成功更新变量,其他的线程都会更新失败,并告知可以重试。同时乐观锁每次在执行数据修改时,都会带上一个版本号,一旦版本号和数据版本号一致就成功修改,并且版本号更新,否则修改失败。

概念

【InnoDB】和【MyISAM】的区别?
【非关系型数据库】是什么?

【InnoDB】和【MyISAM】的区别?

  • 事务支持:InnoDB 支持事务,具有 ACID 属性;MyISAM 不支持事务,适用于读多写少的场景。
  • 锁支持:InnoDB 支持行级锁,仅锁定实际需要的数据;MyISAM 只支持表级锁。
  • 外键支持:InnoDB 支持外键,保证了数据的完整性;MyISAM 不支持外键。
  • 崩溃恢复:InnoDB 在崩溃后可以自动恢复;MyISAM 崩溃后需要手动恢复或使用备份恢复。
  • 备份:InnoDB 支持热备份,即在数据库运行的时候进行备份;MyISAM 不支持热备份,需要保证备份期间没有写操作。

【非关系型数据库】是什么?

  • 非关系型数据库称为 NOSQL,采用键值对的形式来存储数据。
  • 它的读写性能很高,易于扩展,可以分为内存型数据库 redis 和文档型数据库 mongoDB。
  • 适用非关系型数据库的场景为:海量数据存储,多格式数据存储和对查询速度要求快的数据存储。

备份,日志

mysql 的【备份】和【恢复】?
日志中【undo log,buffer pool,redo log,binlog】的作用?

mysql 的【备份】和【恢复】?

  • mysql 备份可以分为完全备份,差异备份和增量备份。
  • 完全备份:每次对数据库进行完整的备份,它的特点是占用磁盘大,恢复慢。
  • 差异备份:备份上一次完全备份后发生变化的数据,与完全备份相比占用空间小,但是随着时间的推移,差异备份也会越来越大。
  • 增量备份:备份上一次完全备份或者是差异备份后发生变化的数据,特点是占用空间较小。

可以使用 mysqldump 工具进行备份

  1. 完全备份时,可以选择备份整个数据库,或者部分数据表,使用 mysqldump 命令将数据保存为 .sql 格式。根据 .sql 文件可以使用 source 命令将其恢复为数据库或表。
  2. mysql 通常使用二进制日志文件 binary log 来间接实现增量备份。首先设置二进制日志记录为开启状态,接着使用 mysqldump 进行一次增量备份,在此之后,对数据库的修改操作在日志文件中都会有所记录。在之后需要恢复时,可以使用 mysqlbinlog 命令根据日志文件中时间戳大于上次备份时间后的记录,可以实现数据库的增量备份恢复。

日志中【undo log,buffer pool,redo log,binlog】的作用?

  • udno log:是回滚日志,负责记录更新前的对应数据。当执行数据增删的操作时,再事务提交前,mysql 会隐式的开启事务来执行这个增删的操作。如果在此期间出现了崩溃,可以通过 undo log 来回滚到之前的数据。
  • buffer pool:是缓冲池,用来提升数据库的读写性能。当读取数据时,如果数据存在于 buffer pool 中,那么直接去内存中读取数据,而不需要去磁盘中读取。当写入数据时,如果数据存在于 buffer pool 中,直接修改数据 buffer pool 中数据所在的页,并将其设置为脏页,由后台选择合适的时机将脏页写入到磁盘。
  • redo_log:是物理日志,记录某个数据页中发生了什么修改,一旦事务提交,redo log 将会被持久化到磁盘。因为像 buffer pool 是基于内存的,而内存是不稳定的,断电时数据会丢失。可以根据 redo log 中的信息将数据恢复到最新的状态。
  • binlog:是二进制日志,是 mysql 的 server 层生成的日志,所有存储引擎都可以使用。在执行任意更新操作后,会生成一个 binlog,当事务提交之后会把所有的 binlog 写入一个 binlog 文件,其中只会记录修改了数据库内容的日志,不会记录查询操作。binlog 主要是用于数据库的备份恢复。

Redis

锁【分布式锁】

redis 实现【分布式锁】?

redis 实现【分布式锁】?

  1. 使用 setnx + expire 实现。首先利用 setnx 将 key 设为 value,当 key 不存在时才能成功,而当 key 存在时什么也不做。因为分布式锁需要超时机制,因此使用 expire 来设置过期时间。但上述方法存在问题,因为 setnx 和 expire 是两步操作,不具有原子性,如果执行第一条语句后发生异常,锁将无法过期。
  2. 可以采用 lua 脚本,将 setnx 和 expire 写在脚本中,由于 lua 脚本在 redis 中的实现是原子性的,因此实现的加锁的原子性。
  3. 使用 set 及 nx 可选参数实现。例如 EX 代表设置过期时间,NX 代表 key 不存在时设置过期时间。其中设置的 value 必须具有唯一性,避免释放锁时验证 value 发生错误。释放锁时,需要根据 value 值判断锁是不是自己的,再进行删除锁。解锁时的步骤也需要 lua 脚本,确保解锁时的原子性。

数据【底层,常用数据结构,redis 介绍】

redis 的【常用数据结构】和【使用场景】?
redis 的【底层数据结构】?
redis 的【介绍】?

redis 的【常用数据结构】和【使用场景】?

  1. string:string 是最基本的 key-value 结构,其中 key 为字符串,但是 value 可以为多种数据类型。string 是基于简单动态字符串 SDS 实现的。string 通常用在常规计数(粉丝数,订阅数)等场景。
  2. list:list 为简单的字符串列表,按照插入顺序排序,可以从列表的头部或尾部添加元素。list 的底层结构由双向链表或压缩列表实现。lisit 通常用在消息队列(关注列表,好友列表)等场景。
  3. hash:hash 是一个 key-value 集合,其中 key 为字符串,value 的形式为多个 field_i 和 value_i 组成的映射表。hash 的底层结构由压缩列表和哈希表实现。hash 通常用于存储对象信息(用户信息,商品信息)等场景。
  4. set:set 是键值集合,元素存储顺序和插入顺序无关,且无法存储重复数据。set 是由哈希表或整数集合实现的。set 可以用于需要交集并集之类的场景(共同好友,共同关注)等场景。
  5. zset:zset 是有序键值集合,相比于 set 多了个排序属性,因此可以实现元素有序排列。zset 是由压缩列表或跳表实现的。zset 可以用于排序(播放排行榜,电商排行榜)等场景。
  6. bitmap:bitmap 是位图,由一串连续的二进制数组组成,通过设置 0/1 值可以表示某个元素的状态。bitmap 是基于 string 实现的。bitmap 可以用在二值状态统计(签到统计,登录状态)等场景。
  7. hyperloglog:hyperloglog 是一种用于统计基数的集合类型,用于统计集合中非重复元素的个数。hyperloglog 可以用在百万级网页 UV 计数等场景。
  8. geo:geo 主要用于存储地理位置信息,并对存储的信息进行操作。geo 基于 zset 类型实现。geo 可以用在查找附近商家,附近网约车等场景。
  9. stream:stream 主要用于实现消息队列,它支持消息的持久化,支持自动生成全局唯一ID,支持消费组模式消费数据,让消息队列更加的稳定可靠。

redis 的【底层数据结构】?

  1. 简单动态字符串:SDS 的结构中包含参数 len,用来记录字符串的长度,因此查询字符串长度的复杂度仅为 O(1),速度快。同时 SDS 还是二进制安全的,它不像 C 语言一样用 ‘\0’ 标识字符串末尾,而是使用 len 来记录,因此可以存放任意二进制数据。参数 alloc 用于计算字符串的剩余空间,在拼接字符串时如果空间不够会自动扩容,避免缓存区溢出,更加安全。参数 flags 可以设置不同的类型的 SDS 结构体,用于节省内存空间。
  2. 链表:链表的节点有一个前向指针和后向指针,因此是双向链表。链表结构中 head 和 tail 可以获取头尾元素节点,dup,free 和 match 可以复制,释放和比较元素节点。参数 len 可以用来获取链表长度,复杂度仅为 0(1)。链表节点由于使用的是 void* 指针,因此可以存放不同类型的数据。链表的缺点是内存不连续,数据查找效率不高,以及创建节点内存开销大。
  3. 压缩列表:压缩列表类似于数组,使用连续的内存块来存放数据。当前元素节点会记录前一节点的长度,因此可以实现双向遍历。压缩列表在存储数据时会根据数据的大小分配合适的内存,可以很好的节省了内存空间。但是当新增一个较大元素时,可能出现空间不够重新分配内存的情况,连锁更新会导致压缩列表性能的下降。
  4. 哈希表:哈希表是一种 key-value 的数据结构,可以使用 O(1) 的复杂度进行快速查询。然而,随着数据的不断增加,可能会发生哈希冲突。redis 采用链式哈希结构来解决哈希冲突,就是将拥有相同哈希值的数据用链表串起,确保数据仍可以被查询到。另一种解决方法就是 rehash,当发生哈希冲突时,新建一个哈希表并扩增哈希桶的大小为两倍,迁移旧哈希表中的数据到新哈希表中。
  5. 整数集合:整数集合是一块连续的内存空间,类似于数组。其中元素的存放类型取决于参数 encoding 的属性,比如 8,16,32 位的 int。此外当插入的新数据类型超过当前最大类型时,会进行类型的升级操作。
  6. 跳表:跳表在链表的基础上进行改进的,实现了一种多层的有序链表,可以快速定位数据。它的查询步骤是从头节点的顶层开始,查找第一个大于指定元素的节点,再退回上一节点,在下一层节点继续查找,使得查找的复杂度为 O(log n)。
  7. quicklist:quicklist 是由双向链表和压缩列表组合实现的,快速链表它的每一个元素都是压缩列表。因为压缩列表的优点是节省内存,缺点是元素数量大时发生连锁更新效率低。因此通过控制每个链表节点中的压缩列表的长度可以对内存占用和访问性能都进行一定的提升。
  8. listpack。listpack 采用压缩列表的结构,采用连续的内存保存数据。此外对于不同的节点,listpack 会采用不同的编码方式保存数据进一步节省内存开销。此外 listpack 只记录当前节点的长度,而不像压缩链表记录前一个节点的长度,当插入新元素的时候,不会影响其他节点的字段变化,因此避免了连锁更新的问题。

redis 的【介绍】?

  • redis 是一种基于内存的数据库,对数据的读写都是在内存中完成的,因此读写速度很快,常用于缓存,消息队列,分布式锁等场景。
  • redis 提供了很多的数据类型来支持不同的业务场景,比如 string,hash,list,set,zset,bitmap,hyperloglog,geo,stream 等,并且对数据的操作是原子性的。
  • 除此之外,redis 还支持事务,持久化,集群方案等。

缓存【数据一致性,缓存雪崩…,过期删除,内存淘汰】

数据库和缓存的【数据一致性】?
缓存的【雪崩,击穿,穿透】?
redis 的【过期删除策略】?
redis 的【内存淘汰策略】?

数据库和缓存的【数据一致性】?

  • 使用缓存就会涉及到数据库和缓存的存储双写,会导致数据不一致的问题。
  • 数据一致性分为:强一致性,即用户写入什么,读取的立刻就是什么,用户体验好但是对系统性能影响大。弱一致性:写入数据后,不保证数据立即一致,而是某个时间级别后能够一致。最终一致性,写入数据后,只保证最终数据会保持一致。
  1. 旁路缓存模式:旁路缓存是最常见的一种缓存技术,适用于请求比较多的场景,服务端会以数据库的结果为准。读请求时,先读取缓存,如果命中的话直接返回数据,未命中的话从数据库中获取数据,并添加到缓存中。写请求时,先更新数据库,再删除缓存。
    • 先更新数据库,再更新缓存 / 先更新缓存,再更新数据库:线程 A 先更新数据库为 x,此事发生网络问题,线程 B 更新数据库为 y,再更新缓存为 y,此时线程 A 再更新缓存为 x,发生数据不一致问题。
    • 先删除缓存,再更新数据库:线程 A 发起了写操作,先删除缓存,接着线程 A 遇到网络问题。线程 B 发起了读操作,发现缓存未命中,于是从数据库中找到数据 x,更新缓存为 x。线程 A 接着执行,更新数据库中的数据为 y,此时导致数据不一致。
    • 先更新数据库,在更新缓存:线程 A 发起了写操作,修改数据库中的值为 y 着线程 A 遇到网络问题。线程 B 发起了读操作,命中原来的缓存 x。线程 A 接着执行,删除缓存。可以确保在之后的读取中,数据库和缓存中的数据是一致的。对于读+写操作,理论上会发生线程 B 写操作删除缓存后,线程 A 读操作更新旧数据缓存的情况,但实际上是不会发生的,因为缓存的写入速度比数据库的写入速度快很多,所以还是可以保持数据的一致性。
  2. 读写穿透模式:读写穿透中服务器将缓存作为主要的存储数据。读请求时,先读取缓存,如果命中的话直接返回数据,未命中的话从数据库中获取数据,并添加到缓存中。当进行写请求时,先检查缓存,缓存未存在则直接更新数据库,如果缓存存在,则先更新缓存,再更新数据库。此外缓存传统在旁路缓存的基础上提供了 cache-provider 层并进行了封装,在旁路缓存中缓存未命中客户端自己负责写入数据到缓存,而读写穿透中是由 cache 服务负责将数据写入缓存的。
  3. 异步缓存写入模式:异步写入和缓存穿透类似,都是通过 cache 服务负责缓存和数据库的读写。不同的是读写穿透同时更新数据库和缓存,而异步缓存写入是先更新缓存数据,之后通过异步批量的方式更新数据库。这种同步方式的缺点是缓存还没有来得及更新到数据库,缓存就已经失效。适合用在对写入修改数据需要高但对一致性要求不高的情况,比如网页的浏览量,点赞量等场景。

缓存的【雪崩,击穿,穿透】?

  1. 缓存雪崩:缓存雪崩是指大量缓存数据在同一时间过期或者是 redis 故障宕机时,如果此时出现大量的用户请求,那么因为 redis 无法处理,所有的请求都直接访问数据库,造成数据库的压力剧增,造成数据库宕机或系统崩溃。对于大量数据同时过期的情况,可以采用(1)给过期时间设置为随机数确保过期时间分布均匀;(2)当数据不在缓存中时,使用互斥锁保证同一时间内只有一个请求来构建缓存,构建后释放锁;(3)业务线程不负责更新缓存,也不设置缓存过期时间,而是让后台线程负责缓存更新工作,如果内存紧张时部分缓存被淘汰,也是后台进程负责从数据库读取数据写入到缓存。对于 redis 宕机的情况,可以采用(1)启用请求限流机制,直接收部分的请求访问数据库,其他请求拒绝其访问;(2)构建 redis 集群,如果主节点发生宕机,那么将从节点切换为新的主机点继续提供缓存服务。
  2. 缓存击穿:缓存击穿是缓存中的某个热点数据过期,此时大量的请求访问该热点数据,但从缓存中获取不到,于是大量请求访问数据库,数据库承受很大的压力。可以采用(1)更新缓存时添加互斥锁,确保同一时间只有一个请求来构建缓存,其他请求等待;(2)不给热点数据设置过期时间,通过后台线程异步更新缓存;或者在热点数据即将过期前,通过后台线程更新缓存并重制过期时间。
  3. 缓存穿透:缓存穿透是用户访问的数据,既不在缓存中,也不在数据库中,导致请求缓存时未命中,从数据库读取不到值无法构建缓存,当有大量请求时数据库承受很大的压力。可以采用(1)接收请求前判断请求参数是否合理,是否含有非法值;(2)当发现缓存穿透发生时,可以为查询数据的缓存设为一个空值返回,这样就不会继续查询数据库;(3)设置布隆过滤器快速判断数据是否存在,而避免通过数据库来查询数据是否存在,因此减轻数据库的压力。

redis 的【过期删除策略】?

常用的删除策略

  1. 定时删除:在设置 key 的过期时间时,创建一个定时事件,时间到达时事件处理器自动执行 key 删除操作。优点是可以尽快的删除 key 释放内存;缺点是过期 key 较多时比较占用 CPU 时间。
  2. 惰性删除:不主动删除过期的 key,当从数据库中访问 key 时,检查 key 是否过期,过期的话就删除。优点是对 CPU 时间友好,缺点是比较占用内存。
  3. 定期删除:每隔一段时间从数据库中随机选取一定数量的 key 进行检查,删除其中过期的数据。它的特点是 CPU 时间和内存占用是上述方法的折中。

redis 删除策略:redis 采用惰性删除 + 定期删除的策略组合使用,以在 CPU 时间和内存占用中达到平衡。

  1. 惰性删除:redis 在访问和修改 key 之前,会调用 expireIfNeeded 函数进行检查。如果过期则删除 key,根据参数设置决定同步或异步删除,返回客户端空值。如果没有过期,不做任何处理正常返回。
  2. 定期删除:每个一段时间从数据库中随机抽取一定数量的 key 进行检查,删除其中过期的 key。默认是每 10 秒进行一次检查,每次随机抽取 20 个 key。如果过期 key 的占比超过 1/4,则继续循环检查。

redis 的【内存淘汰策略】?

  • 当 redis 中的内存数据已经超过了 redis 设置的最大内存后,会使用内存淘汰策略来删除符合条件的 key,保障 redis 的运行效率。
  1. noeviction:就是当运行内存超过最大内存后,不淘汰任何数据,当有新的数据写入时会被禁止写入,只能进行一些查询和删除操作。
  2. volatile-random:随即淘汰设置了过期时间的任何 key。
  3. volatile-ttl:优先淘汰过期时间更早的值。
  4. volatile-lru:淘汰所有设置了过期时间的 key 中,最久没有使用的 key。
  5. volatile-lfu:淘汰所有设置了过期时间的 key 中,最少使用次数最少的 key。
  6. allkeys-random:随即所有 key 中的任意的 key。
  7. allkeys-lru:淘汰所有 key 中,最久没有使用的 key。
  8. allkeys-lfu:淘汰所有 key 中,使用次数最少的 key。

持久化【持久化机制】

redis 的【持久化机制】?

redis 的【持久化机制】?

  • redis 通过持久化机制,将内存中的数据同步到硬盘中来保证数据的持久化。当 redis 重启后,将持久化后的硬盘数据加载到内存中,达到恢复数据的目的。redis 默认使用的就是 RDB 持久化。
  • redis 有两种持久化方式,分别为 RDB 持久化和 AOF 持久化技术。
  1. RDB:redis 可以通过创建快照来获取某个时刻在内存中存储的数据副本,创建的快照是二进制文件。redis 创建快照完成后将快照进行备份,可以发送给其他服务器创建具有相同数据的服务器副本,也可以在重启时用快照恢复自身的服务器数据。
    • 补充:redis 提供了两个命令来生成快照,分别是 save 和 bgsave。其中 save 会在主线程中生成文件,而 bgsave 会创建子线程来生成文件,避免主线程的阻塞。RDB 的备份是全量备份,如果备份频繁的话会影响 redis 的性能,但不频繁可能会丢失较长时间的数据。
  2. AOF:redis 在执行一条写操作命令时,会把命令以追加到形式写入到磁盘中的日志里。当 redis 重启的时候,读取并执行日志中的命令,就可以恢复缓存数据。
    • 写回策略:redis 执行完写操作后,会将命令写入缓冲区,系统调用 write() 函数将缓冲区数据写入 AOF 日志中,等待内核决定何时将日志写入到磁盘。通过设置配置文件参数为 always,可以让内核在每次执行写操作后将日志写入到磁盘,可以最大程度保证数据不丢失,但是影响主进程性能。参数为 everysec,可以让内核每秒将日志写入到磁盘,减少了性能的开销,最多损失一秒的数据。参数为 no,意味着将日志写入磁盘的时机完全由内核决定,具有很大的不确定性。
    • 重写机制:随着写操作的越来越多,AOF 日志文件的大小也会越来越大,这会导致性能的问题。于是 redis 给 AOF 文件大小设置阈值,当超过后会启用 AOF 重写机制。当出现了多条命令修改同一个 key 时,只会记录最后一条更新使用的命令。然后将重写后的 AOF 文件覆盖掉现有的 AOF 文件,AOF 文件的大小也得到了压缩。
    • 后台重写:当触发 AOF 重写时,会读取内存中的键值对并生成新的命令,将新的命令放入新的 AOF 文件中,如果 AOF 文件太大的话,重写是很耗时的,因此重新一般由后台的子进程完成。子进程会获取主进程的数据副本,并读取缓存中的所有数据,最后将键值对对应的命令记录到新的 AOF 日志中完成后台重写,在此期间主进程一直是非阻塞的。
  • redis 也支持 RDB 和 AOF 的混合持久化。使用混合持久化时,AOF 重写日志所创建的子进程会将与主进程共享的内存数据以 RDB 的形式全量写入 AOF 文件,之后会将新增命令以 AOF 的形式增量写入到 AOF 文件,实现混合持久化。

集群【高可用】

redis 的【高可用】?

redis 的【高可用】?

  1. 主从复制:主从复制是 redis 实现高可用的最基本的保障,它将主 redis 服务器的数据同步到多台从 redis 服务器上,且主从服务器之前采用的是读写分离的模式。当主服务器发生写操作时,会将当前的写操作同步给从服务器,从服务器会执行同步的写操作命令。主从服务器之间的命令复制是异步执行的,主服务器不会等待从服务器是否复制完毕,因此无法实现主从服务求数据的强一致性。
  2. 哨兵模式:当 redis 服务器宕机时,使用主从复制需要手动进行 redis 的恢复。如果使用哨兵模式可以监控主从服务器的状态,实现故障转移。
  3. 切片集群模式:当 redis 缓存数据大到一台服务器无法缓存时,就需要 redis 切片集群将数据分散到不同的服务器上,降低系统对单服务器的压力。redis 切片集群采用 hash slot 来处理数据和节点之间的映射关系,每个键值对会根据 key 被映射到一个 hash slot 中,多个 hash slot 被一个 redis 节点管理。

Demo
Demo1
Demo2
Demo3
 1. Demo1
 2. Demo2
 3. Demo3
  Demo1
  Demo2
  Demo3
  1. Demo1
  2. Demo2
  3. Demo3

操作系统

介绍下【锁】?
介绍下【死锁】?
介绍下【公平锁,非公平锁】?
介绍下【悲观锁,乐观锁】?


介绍下【锁】?
在多线程环境中,为了避免多个线程同时修改同一资源,造成资源混乱,因此需要在访问共享资源前加锁。
互斥锁:互斥锁是为了在任意时间内,仅有一个线程可以访问共享资源。当线程A加锁成功时,只要线程A没有释放锁,那么线程B就会加锁失败,并释放CPU让给其他线程。随后线程B进入阻塞睡眠状态,等待锁被释放后,被内核唤醒继续执行业务。
自旋锁:自旋锁也是为了在任意时间内,仅有一个线程可以访问共享资源。但是当自旋锁被持有而导致锁获取失败时,线程不会立即释放CPU,而是一直循环尝试获取锁,直到获取锁成功。因此自旋锁一般用于加锁时间很短的场景,较少了线程上下文切换的开销,效率更高。
读写锁:读写锁由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果需要修改共享资源则需要用「写锁」加锁,它主要用于可以明确区分读操作和写操作的场景,在读多写少的场景下可以发挥出优势。
当「写锁」没有被线程持有时,多个线程可以并发的持有「读锁」,大大提高资源的读取效率。
当「写锁」被线程持有时,线程获取「读锁」的操作会被阻塞,其他线程获取「写锁」的操作也会被阻塞。
读写锁可分为「读优先锁」和「写优先锁」,区别在于当前线程持有的锁被释放后,后续线程优先获取读或写锁。
条件变量:条件变量是一种同步机制,可能使线程在阻塞等待到某个条件发生后,继续执行业务。比如在生产者消费者模型中,希望在生产100个产品后进行庆祝。假设线程A是生产线程,线程B是查询线程。如果使用互斥锁,线程B需要不断地轮询查看是否满足条件,每次查询都需要加锁解锁,非常浪费资源。而使用条件变量可以高效解决,当线程B发现不满足条件时,直接进入阻塞等待状态。接着在线程A中判断是否满足条件,当满足条件时由线程A发送信号唤醒线程B继续查询,避免了无效的加锁解锁操作。

介绍下【死锁】?
死锁:在多线程编程中,线程通常会在访问共享资源前加上互斥锁,只有成功持有锁,才能操作共享资源。当两个线程为了访问两个不同的共享资源时,会使用两个互斥锁,如果这两个互斥锁存在着循环依赖,则可能会同时等待对方释放锁,导致死锁。
产生原因:死锁在同时满足4个条件时才会发生:
互斥条件:线程A已经持有的资源,不能再被线程B持有,如果线程B想要使用,必须等待线程A释放。
持有并等待条件:线程A已经持有资源1,想要持有资源2,那么需要等待其他线程释放资源2,因此会仅如等待状态,但是在等待的过程中,线程A不会释放已经持有的资源1。
不可剥夺条件:线程A已经持有了资源,在使用完毕之前,该资源不能够被其他的线程所获取。
环路等待条件:线程A和线程B在获取资源的顺序上构成了环形链,比如线程A已经获取了资源1,等待资源2,线程B已经获取了资源2,等待资源1,那么双方会一直等待。
避免方案:使用资源有序分配法来破坏环路等待条件,当线程A和线程B都需要获取部分相同资源时,要保证他们获取资源的顺序需要一样,比如线程A先获取资源1,再获取资源2,那么线程B也必须先获取资源1,再获取资源2,这样就不会产生资源获取的循环链。

介绍下【公平锁,非公平锁】?
公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是优先获取锁。
非公平锁:每个线程获取锁的顺序是随机的,不会遵循先来后到的原则,所有的线程会竞争锁。
在实际的应用中,公平锁可以避免发生饥饿现象,但由于需要维护线程队列,因此效率相对较低。而非公平锁并不需要维护线程队列,效率更高,但可能会导致某些线程长时间无法获取锁。

介绍下【悲观锁,乐观锁】?
悲观锁:悲观锁认为多线程访问共享资源时,同时修改共享资源的概率比较高,为了避免发生冲突,会在访问共享资源前为资源上锁。像互斥锁,自旋锁,读写锁等都属于悲观锁。
乐观锁:乐观锁认为多线程访问共享资源时,同时修改共享资源的概率比较低,因此默认不加锁,在线程修改共享资源后,会检查有无发生冲突,如果没有发生冲突,则操作完成。如果发生了冲突,则放弃本次修改操作。乐观锁适用于冲突概率小,加锁成本高的场景,比如在线文档编辑。

进程,线程

介绍下【进程,线程,协程】的区别?
介绍下【进程间通信方式】有哪些?
介绍下【进程调度算法】有哪些?
介绍下【进程切换状态】有哪些?


介绍下【进程,线程,协程】的区别?
定义:进程是资源分配的基本单位。线程是CPU调度的基本单位。协程是轻量级的线程,是线程内部调度的基本单位。
拥有资源:进程拥有 CPU 资源,内存资源,文件资源和句柄等。线程和协程只拥有自己的寄存器和栈等资源;
切换效率:进程的切换内容包含虚拟空间地址,页表,内核栈,硬件上下文等,切换的内容被保存在内核中,会经历用户到内核态用户态的切换,因此切换效率较低。线程的切换资源也保存在内核中,需要经历用户到内核态用户态的切换,但是由于其切换内容仅有部分寄存器资源,因此切换效率中等。协程的切换资源保存在用户态中,切换过程不涉及内核态,因此切换效率高。
切换时机:进程和线程的切换者是操作系统,切换时机是根据操作系统自己来判断的;协程的切换者是用户,切换时机根据用户自己的程序决定。
并发性:多个进程或线程在单核CPU上可以并发运行,在多核CPU上可以并行运行;多个协程之间只能并发执行。

介绍下【进程间通信方式】有哪些?
管道:允许一个进程和另外一个具有共同祖先的进程之间进行通信。但管道通信效率低,不适合进程间进行频繁的数据交换。
命名管道:允许任意两个进程之间进行通信,通过 mkdifo 来创建命名管道。
消息队列:消息队列是保存在内核中的消息链表,有写权限的进程可以向队列中添加消息,有读权限的进程可以从队列中取出消息。消息队列中消息体的大小有长度限制,不能传输过大的数据。在消息队列通信的过程中,存在用户态和内核态之间的数据拷贝开销。
共享内存:共享内存机制把多个进程中的虚拟地址空间映射到相同的物理地址空间中去。这样一个进程写入数据,其他进程立马可以看到,不需要数据的拷贝过程,大大提高了通信效率。但如果多个进程同时修改共享内存,那么可以会发生冲突。
信号量:信号量是一个整形的计数器,主要用于实现进程间的互斥和同步。信号量的值代表资源的数量,控制信号量的方式有 P 操作和 V 操作,分别发生在进入共享资源,离开共享资源时。
P 操作:将信号量 -1,如果信号量 <0,则代表资源已被占用,进程需要阻塞等待。如果信号量 >=0,则代表有资源可以使用,可以正常执行。
V 操作:将信号量 +1,如果信号量 <=0,则代表当前有阻塞的进程,将其唤醒。如果信号量 >0,则代表没有阻塞的进程。
信号:信号是进程间的一种异步通信机制,可以在任何时候发送信号给某一进程。信号的来源有硬件来源(ctrl+C 终止进程)和软件来源(kill 命令)。
socket:socket 可以实现跨网络不同主机间进程的通信,也可以实现同主机间不同进程的通信。

介绍下【进程调度算法】有哪些?
先来先服务算法 FCFS:遵循先来后到的原则,每次从就绪队列中选择最先进入队列的进程,然后一直运行,直到进程退出或者被阻塞,才会轮到下一个进程接着运行。
最短作业优先算法 SJF:优先选择运行时间较短的进程来运行,这样有利于提高系统的吞吐量。
高响应比优先算法 HRRN:在每次进行进程调度时,先计算响应比优先级,然后让响应比优先级最高的进程优先运行。其中优先级计算公式为 (等待时间+要求服务时间) / 要求服务时间。
时间轮调度算法 RR:这种算法最古老,最简单,最公平,每个进程被分配一个相等的时间片,所有进程轮流执行。
最高优先级调度算法 HPF:从就绪队列中选择最高优先级的进程先运行,其中优先级分有固定的静态优先级,和会随进程运行时间调整的动态优先级。
多级反馈队列调度算法 MLFQ:该算法设置了多个队列,每个队列有不同的优先级,其中优先级越高,分配的时间片越短。同一个队列中的进程按先来先服务算法分配时间片,如果当前队列中的进程在时间片内没有完成,则会把该进程放入下一队列的尾部。只有较高优先级的队列为空时,才会调度较低优先级的队列。这种算法可以为长任务提供更多的CPU时间,同时让优先级高的任务可以得到更快的响应。

介绍下【进程切换状态】有哪些?
线程切换是指当一个线程执行完毕后,操作系统将CPU分配给另一个线程的过程。
创建状态:线程正在被创建时的状态。
就绪状态:线程已经进入就绪队列准备执行,但是还没有被分配到 CPU 去执行。
运行状态:线程已经被分配到 CPU 上执行,正在运行。
阻塞状态:线程因为某些原因无法继续执行进行等待,比如等待 IO 请求完成,等待锁释放等。
终止状态:线程已经执行完操作,或被强制终止执行。

你可能感兴趣的:(c++)