计算机基础问答-面试总结
Table 1
数据类型 | 大小 | 范围/描述 | 默认值 | 示例 |
byte | 8 位 | -128 到 127 | 0 | byte b = 100; |
short | 16 位 | -32,768 到 32,767 | 0 | short s = 1000; |
int | 32 位 | -2^31 到 2^31-1 | 0 | int i = 100000; |
long | 64 位 | -2^63 到 2^63-1 | 0L | long l = 10000000000L; |
float | 32 位 | 大约 ±3.40282347E+38F | 0.0f | float f = 3.14f; |
double | 64 位 | 大约 ±1.79769313486231570E+308 | 0.0d | double d = 3.14; |
char | 16 位 | 0 到 65,535(Unicode 字符) | '\u0000' | char c = 'A'; |
boolean | 未明确 | true 或 false | false | boolean flag = true; |
Python 中有三个逻辑运算符:and、or、not
Python 中的成员运算符in 和 not in
Python 中的身份运算符is 和 not is 运算符
面向对象编程(Object-Oriented Programming,OOP)是一种编程范式,它以对象为核心,将程序中的数据(属性)和行为(方法)封装在对象中,通过对象之间的交互来实现程序的功能。面向对象的核心思想是抽象、封装、继承和多态。
封装是指将数据(属性)和行为(方法)绑定在一起,并隐藏内部实现细节,只暴露必要的接口
- 继承是指一个类可以基于另一个类来创建,继承父类的属性和方法,同时可以扩展或重写父类的功能。
- 多态是指同一个方法在不同的对象中有不同的实现方式。多态分为编译时多态(方法重载)和运行时多态(方法重写)。
Student 类可以继承 Person 类,获得 Person 的属性和方法,同时可以添加自己的特性(如学号)。
以过程(函数)为核心,强调步骤和逻辑。
通过函数调用实现代码复用。
针对特定问题和小型系统可以使用,比如我写个python的脚本文件实现word转pdf,已实现问题为主,不需要复杂的封装和对象,就不用复杂的系统
重载是指在同一个类中,允许定义多个方法名相同但参数列表不同的方法。重写是指子类重新定义父类中已有的方法。重写的方法必须与父类方法具有相同的方法名、参数列表和返回值类型。
抽象类是用 abstract 关键字修饰的类,它可以包含抽象方法(没有方法体的方法)和具体方法(有方法体的方法)。抽象类不能被实例化,只能被继承。
HashMap:非线程安全。在多线程环境下,如果没有额外的同步措施,可能会导致数据不一致。
HashTable:线程安全。它的方法都是同步的,适合多线程环境,但性能可能较低。
Spring MVC如何匹配请求路径
@RequestMapping是用来映射请求的,比如get请求、post请求、或者REST风格与非REST风格的。该注解可以用在类上或方法上,如果用在类上,表示是该类中所有方法的父路径。
什么是控制反转(IOC)?什么是依赖注入?
借助Spring实现具有依赖关系的对象之间的解耦。自动负责实例化、配置和组装对象
自动管理对象的生命周期和依赖关系
对象A运行需要对象B,由主动创建变为IOC容器注入,这便是控制反转。
获得依赖对象的过程被反转了,获取依赖对象的过程由自身创建变为由IOC容器注入,这便是依赖注入。
Spring 框架通过其 IoC(Inversion of Control,控制反转)容器 自动管理对象的生命周期和依赖关系,开发者不需要手动创建和管理容器。Spring 的 IoC 容器是 Spring 框架的核心,它负责实例化、配置和组装对象(即 Bean),并管理它们的依赖关系。
get 和 post 请求有哪些区别?
get请求参数是连接在url后面的,而post请求参数是存放在requestbody内的;
get请求因为浏览器对url长度有限制,所以参数个数有限制,而post请求参数个数没有限制;
因为get请求参数暴露在url上,所以安全方面post比get更加安全;
get请求只能进行url编码,而post请求可以支持多种编码方式;
get请求参数会保存在浏览器历史记录内,post请求并不会;
get请求浏览器会主动cache,post并不会,除非主动设置;
get请求产生1个tcp数据包,post请求产生2个tcp数据包;
在浏览器进行回退操作时,get请求是无害的,而post请求则会重新请求一次;
浏览器在发送get请求时会将header和data一起发送给服务器,服务器返回200状态码,而在发送post请求时,会先将header发送给服务器,服务器返回100,之后再将data发送给服务器,服务器返回200 OK;
Open Systems Interconnection
堆和栈的区别是什么?
- 栈:由编译器自动管理,内存分配和释放遵循后进先出(LIFO)原则。
- 堆:由程序员手动管理(如C/C++)或由垃圾回收机制(如Java、C#)自动管理,内存分配和释放较为灵活。
堆是一个二叉树的数组对象
- 依赖注入(DI):Spring 通过依赖注入管理对象之间的依赖关系,减少了硬编码和耦合,使代码更易于维护和测试。
- 面向切面编程(AOP):Spring 支持 AOP,可以将横切关注点(如日志、事务管理、安全性)与业务逻辑分离,提高代码的模块化。
- 模板化设计:Spring 提供了许多模板类(如 JdbcTemplate、RestTemplate),简化了数据库操作、REST 调用等常见任务。
数据库的三范式(Normalization)是关系数据库设计中的规范化原则,旨在减少数据冗余、提高数据一致性。三范式包括: 第一范式(1NF)
- 定义:确保每列的原子性,即每一列都不可再分。
- 要求:
- 每列都是不可分割的最小数据单元。
- 表中没有重复的列。
第二范式(2NF)
- 定义:在1NF的基础上,消除部分函数依赖。
- 要求:
- 表必须符合1NF。
- 所有非主键列必须完全依赖于主键,而不是部分依赖。
第三范式(3NF)
- 定义:在2NF的基础上,消除传递函数依赖。
- 要求:
- 表必须符合2NF。
- 非主键列之间不能有传递依赖,即非主键列必须直接依赖于主键。
- 1NF:确保列的原子性。
- 2NF:消除部分依赖。
- 3NF:消除传递依赖。
引用数据类型(对象类型):对象(Object)、数组(Array)、函数(Function),
值类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol。
在Spring MVC中,通常的分层架构如下:
- Controller层:负责处理HTTP请求,调用Service层获取数据,并返回响应。
- Service层:包含业务逻辑,调用Model层或DAO层进行数据处理。
- Model层:通常指实体类(Entity),代表反应数据库中的表结构。
- DAO层(Data Access Object):负责与数据库交互,执行CRUD操作
调用关系
- Controller层调用Service层。
- Service层调用DAO层。
- DAO层直接操作数据库,通常不通过Model层调用。
MyBatis Plus 可以根据Model层(实体类)的属性和注解自动生成DAO层的代码(如Mapper接口和XML文件)。具体步骤:
- 定义实体类:使用注解(如@TableName、@TableId等)标注实体类及其属性。
- 使用MyBatis Plus代码生成器:配置生成器,指定实体类、Mapper接口、Service层等的生成路径,运行生成器即可自动生成DAO层代码。
websocket应用的是哪个协议
WebSocket是一个允许Web应用程序(通常指浏览器)与服务器进行双向通信的协议。HTML5的WebSocket API主要是为浏览器端提供了一个基于TCP协议实现全双工通信的方法。
WebSocket优势: 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,双方可以在任意时刻,相互推送信息。同时,服务器与客户端之间交换的头信息很小。
单例模式
作用:保证类只有一个实例。
JDK中体现:Runtime类。
2、静态工厂模式
作用:代替构造函数创建对象,方法名比构造函数清晰。
JDK中体现:Integer.valueOf、Class.forName
3、抽象工厂
作用:创建某一种类的对象。
JDK中体现:Java.sql包。
4、原型模式
clone();
原型模式的本质是拷贝原型来创建新的对象,拷贝是比new更快的创建对象的方法,当需要大批量创建新对象而且都是同一个类的对象的时候考虑使用原型模式。
一般的克隆只是浅拷贝(对象的hash值不一样,但是对象里面的成员变量的hash值是一样的)。
有些场景需要深拷贝,这时我们就要重写clone方法,以ArrayList为例:
clone();
原型模式的本质是拷贝原型来创建新的对象,拷贝是比new更快的创建对象的方法,当需要大批量创建新对象而且都是同一个类的对象的时候考虑使用原型模式。
一般的克隆只是浅拷贝(对象的hash值不一样,但是对象里面的成员变量的hash值是一样的)。
有些场景需要深拷贝,这时我们就要重写clone方法,以ArrayList为例:
DTO(Data Transfer Object)层主要用于在不同层之间传输数据,尤其是在Controller层和Service层之间,或者在与外部系统交互时。DTO 的核心目的是解耦和优化数据传输,避免直接暴露领域模型(如实体类)或数据库结构。
DTO 的使用场景
- Controller 层与 Service 层之间
- Controller 接收客户端请求时,将请求数据封装为 DTO,传递给 Service 层。
- Service 层处理完业务逻辑后,将结果封装为 DTO,返回给 Controller。
- 与外部系统交互
- 当与第三方系统(如 API 接口)交互时,DTO 可以用于封装请求和响应数据。
- 前后端分离架构
- 前端需要的数据格式可能与后端实体类不一致,DTO 可以用于适配前端需求。
性能和安全不可兼得
Get请求性能好,安全性低,存在缓存里
Post请求安全,性能没有get请求好,不存在缓存了
Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
- 保证程序只有一个对象的实例,叫做单例模式;
- 内部类的方式实现单例模式,是线程安全的;
- 双重验证方式实现单例模式也是线程安全的;
使用工厂模式最主要的好处是什么?在哪里使用?
1、工厂模式好处
- 良好的封装性、代码结构清晰;
- 扩展性好,如果想增加一个产品,只需扩展一个工厂类即可;
- 典型的解耦框架;
2、在哪里使用?
- 需要生成对象的地方;
- 不同数据库的访问;
Java的泛型(如List<T>
)不支持基本数据类型,只能使用对象类型。例如,List<int>
是非法的,而List<Integer>
是合法的。因此,当需要在集合中使用基本数据类型时,必须将其装箱为对应的包装类。
泛型允许编写通用的代码,适用于多种类型。例如,可以定义一个通用的List<T>,用于存储任意类型的对象。
Java的泛型(Generics)是一种允许在定义类、接口或方法时使用类型参数的机制。
MySQL 的架构主要包括以下几个核心组件:
- 连接器(Connector):
- 负责与客户端建立连接,验证用户身份(用户名、密码、权限等)。
- 连接成功后,继续处理客户端请求。
- 查询缓存(Query Cache):
- 缓存 SELECT 查询结果,若后续有相同查询,直接返回缓存结果。
- 由于缓存失效频繁,MySQL 8.0 已移除该功能。
- 分析器(Parser):
- 对 SQL 语句进行词法和语法分析,确保语句正确。
- 生成解析树,供优化器使用。
- 优化器(Optimizer):
- 根据解析树生成多种执行计划,选择最优方案。
- 优化器基于成本模型,考虑索引、表大小等因素。
- 执行器(Executor):
- 调用存储引擎接口执行优化后的查询计划。
- 执行器负责与存储引擎交互,完成数据读写。
- 存储引擎(Storage Engine):
- 负责数据的存储和检索,支持多种引擎,如 InnoDB、MyISAM 等。
- InnoDB 是默认引擎,支持事务、行级锁和外键。
- 日志模块:
- 二进制日志(Binlog):记录所有更改数据的操作,用于主从复制和数据恢复。
- 重做日志(Redo Log):确保事务的持久性,记录事务对数据的修改。
- 回滚日志(Undo Log):用于事务回滚和 MVCC 实现。
- 缓冲池(Buffer Pool):
- InnoDB 使用缓冲池缓存数据和索引,减少磁盘 I/O,提升性能。
- 锁管理器(Lock Manager):
- 管理事务中的锁,确保并发事务的隔离性。
- InnoDB 支持行级锁,MyISAM 仅支持表级锁。
- 主从复制(Replication):
- 主库将 Binlog 发送给从库,从库重放日志以保持数据同步。
- 用于读写分离、数据备份和高可用。
这些组件协同工作,使 MySQL 能够高效处理数据存储和查询任务。
本文介绍了Web开发中的常见架构模式,包括单体架构、前后端分离、MVC设计模式以及微服务。 单体架构将所有功能耦合在一个应用中,而前后端分离则强调通过API实现解耦。 MVC是一种经典设计模式,既可视为单体架构的一部分,也可在改进后实现前后端分离。 微服务则是将大型程序拆分为多个独立部署的服务,每个服务专注于单一功能,实现高内聚低耦合。 对于开发者来说,理解这些概念有助于提升开发效率和软件质量。业务逻辑越来越多,功能模块越来越多,就需要微服务了,大型项目需要,小项目没必要用微服务,效率低,占用服务器内存大。都是为了高内聚低耦合。
MySQL 和 Redis 是两种不同类型的数据库,各自适用于不同的场景。以下是它们的主要区别:
1. 类型
- MySQL:
- 关系型数据库(RDBMS),基于表格结构,支持复杂的 SQL 查询和事务处理。
- 数据以行和列的形式存储在表中,表之间可以通过外键建立关系。
- Redis:
- 非关系型数据库(NoSQL),属于键值存储系统。
- 数据以键值对的形式存储,支持多种数据结构(如字符串、列表、集合、哈希、有序集合等)。
2. 数据存储
- MySQL:
- 数据持久化存储在磁盘上,适合需要长期保存和复杂查询的场景。
- 支持 ACID 事务,确保数据的一致性和完整性。
- Redis:
- 数据主要存储在内存中,读写速度极快,但容量受内存限制。
- 支持数据持久化(如 RDB 和 AOF),但主要设计用于缓存和临时数据存储。
3. 性能
- MySQL:
- 适合处理复杂的查询和事务,但在高并发场景下性能可能受限。
- 性能依赖于磁盘 I/O 和索引优化。
- Redis:
- 由于数据存储在内存中,读写速度非常快,适合高并发、低延迟的场景。
- 通常用作缓存、消息队列或实时数据处理。
4. 使用场景
- MySQL:
- 适合需要复杂查询、事务支持和数据一致性的应用,如电商、金融系统等。
- 适用于需要长期存储和复杂数据关系的场景。
- Redis:
- 适合需要高速读写的场景,如缓存、会话存储、排行榜、消息队列等。
- 适用于需要快速访问临时数据或实时数据的场景。
5. 扩展性
- MySQL:
- 支持垂直扩展(增加服务器资源)和水平扩展(分库分表),但扩展性相对复杂。
- 主从复制和集群方案可以提高可用性和读取性能。
- Redis:
- 支持主从复制和集群模式,扩展性较好。
- Redis Cluster 支持自动分片和高可用性。
6. 数据一致性
- MySQL:
- 支持 ACID 事务,确保数据的一致性和完整性。
- 适合需要强一致性的应用。
- Redis:
- 默认情况下不保证强一致性,但在某些模式下(如 AOF 持久化)可以提供一定的一致性保证。
- 适合对一致性要求不高的场景。
7. 查询语言
- MySQL:
- 使用 SQL(结构化查询语言)进行数据操作和查询。
- 支持复杂的查询、连接、聚合等操作。
- Redis:
- 使用简单的命令进行操作,如 GET、SET、LPUSH 等。
- 不支持复杂的查询,通常通过键直接访问数据。
总结
- MySQL 适合需要复杂查询、事务支持和数据一致性的场景。
- Redis 适合需要高速读写、低延迟和临时数据存储的场景。
在实际应用中,MySQL 和 Redis 常常结合使用,利用 Redis 作为缓存层来提升系统性能,同时使用 MySQL 作为持久化存储层来保证数据的完整性和一致性。
什么是索引?
索引是存储引擎用于提高数据库表的访问速度的一种数据结构。
索引的优缺点?
优点:
- 加快数据查找的速度
- 为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度
- 加快表与表之间的连接
缺点:
- 建立索引需要占用物理空间
- 会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长
什么情况下需要建索引?
- 经常用于查询的字段
- 经常用于连接的字段建立索引,可以加快连接的速度
- 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度
- where条件中用不到的字段不适合建立索引
- 表记录较少
- 需要经常增删改
- 参与列计算的列不适合建索引
- 区分度不高的字段不适合建立索引,如性别等
索引的作用?
数据是存储在磁盘上的,查询数据时,如果没有索引,会加载所有的数据到内存,依次进行检索,读取磁盘次数较多。有了索引,就不需要加载所有数据,因为B+树的高度一般在2-4层,最多只需要读取2-4次磁盘,查询速度大大提升。
索引就像字典的目录,查找某个字时,如果没有索引,就相当于把字典的所有页都翻一遍才能找到对应的字和释义,但是效率太过低下,通过目录可以快速找到对应数据。
聚集索引:
拿没有目录的字典来说,我们只有abc字母来查询,当我们要查询 ‘’安’‘ 字时,得先根据读音 an 的a先开始找,我们要翻到a的部分,然后在根据a区里面的n区查找(遵循了最左前缀的匹配原则:即不是从最左开始查询,就无法找到数据) ,找到后我们能看到 ’‘安‘’ 字的所有信息。
即:
--聚集索引的索引是有顺序的
--聚集索引的索引和数据是在一起的
--所以找到文件后不需要再根据索引去回表查询
B+树的核心特点
- 多路平衡搜索树:
- 每个节点可以有多个子节点(通常远多于 2 个),这使得树的高度较低,查询效率更高。
- 树是平衡的,所有叶子节点到根节点的路径长度相同。
- 数据存储在叶子节点:
- 内部节点(非叶子节点)只存储键(key),用于路由。
- 所有实际数据(或指向数据的指针)都存储在叶子节点。
- 叶子节点通过指针连接:
- 叶子节点之间通过指针连接成一个有序链表,支持高效的范围查询和顺序访问。
所以索引相当于字典的目录是吗,目录上有abc...z 对应相应的分区数据?
字典目录
- 目录:字典的目录通常按字母顺序排列(A-Z),每个字母对应一个范围。
- 查找过程:
- 如果你想查一个单词,比如 "apple",你会先找到目录中的 "A" 部分。
- 然后在 "A" 部分中找到具体的页码,翻到那一页查看单词的详细解释。
(2)数据库索引
- 索引:数据库索引(如 B+树)也是一种“目录”,但它比字典目录更复杂和高效。
- 查找过程:
- 如果你想查一条数据,比如 id = 10,数据库会先查找索引。
- 索引通过 B+树的结构快速定位到 id = 10 对应的数据存储位置(类似于字典的页码)。
- 然后数据库直接读取该位置的数据。
B+树索引的“目录”结构
B+树的索引结构可以理解为一种 多层目录,每一层都进一步缩小查找范围,直到找到目标数据。以下是一个具体的类比:
(1)根节点:顶级目录
- 类似于字典的字母分组(A-Z)。
- 根节点存储多个键值,用于决定下一步查找的方向。
(2)内部节点:子目录
- 类似于字典中每个字母下的细分(如 "A" 下的 "Aa"、"Ab" 等)。
- 内部节点进一步缩小查找范围,将查询路由到更具体的子节点。
(3)叶子节点:具体数据
- 类似于字典中某一页的具体单词和解释。
- 叶子节点存储实际的数据(或指向数据的指针),并且通过指针连接成链表,支持范围查询。
3. B+树索引的优势
相比于简单的字典目录,B+树索引有以下优势:
- 动态平衡:
- 字典的目录是静态的,而 B+树可以动态调整(插入、删除数据时自动平衡)。
- 多层结构:
- 字典目录通常只有一层(A-Z),而 B+树是多层结构,适合处理大量数据。
- 范围查询:
- 字典目录只能通过字母查找,而 B+树的叶子节点通过指针连接,支持高效的范围查询(如 id BETWEEN 10 AND 20)。
4. 示例
假设有一个表 users,其中 id 列建立了 B+树索引:
id | name |
1 | Alice |
5 | Bob |
10 | Charlie |
15 | David |
20 | Eve |
(1)索引结构
- 根节点:[10]
- 左子树:[1, 5]
- 右子树:[15, 20]
(2)查找过程
- 查找 id = 15:
- 从根节点 [10] 开始,15 > 10,进入右子树。
- 在右子树 [15, 20] 中找到 15,返回对应的数据。
5. 总结
- 索引确实类似于字典的目录,但它更高效、更智能。
- B+树索引通过多层结构(根节点、内部节点、叶子节点)快速定位数据。
- 叶子节点通过指针连接,支持高效的范围查询和顺序访问。
TCP 和UDP的区别?
死锁?
B+树的核心特点
多路平衡搜索树:
每个节点可以有多个子节点(通常远多于 2 个),这使得树的高度较低,查询效率更高。
树是平衡的,所有叶子节点到根节点的路径长度相同。
数据存储在叶子节点:
内部节点(非叶子节点)只存储键(key),用于路由。
所有实际数据(或指向数据的指针)都存储在叶子节点。
叶子节点通过指针连接:
叶子节点之间通过指针连接成一个有序链表,支持高效的范围查询和顺序访问。
谈一谈 TCP 与 UDP 的区别。
TCP与UDP都是传输层的协议,且都用端口号标识数据所达的进程。
TCP提供的是面向连接服务,提供可靠交付。且具有流量控制和拥塞控制。可用于可靠要求高的场合如:SMTP,FTP,HTTP等
UDP提供的是无连接服务,提供不可靠交付,且无确认机制。主要用于即时强的场合如:视频聊天,语音电话等。
TCP通过连接管理、确认机制、序列号、流量控制、拥塞控制和校验和等机制,确保数据可靠传输,适用于对可靠性要求高的场景,如文件传输、网页浏览等。
序列号与确认号
- 序列号:每个数据段都有唯一序列号,接收方按序重组数据。
什么是事务?
多条SQL一起执行,要么同时成功,要么同时失败
一个SQL慢,怎么优化?
--限制行数
有些查询数据量极大,如果查全部的话,很久都不出来,我们可以查询部分数据来验证SQL是否正确
检查SQL语法
select * 操作改成具体的列,减少性能开销
减少使用子查询,减少使用!= 和 <>操作符,否则就将放弃索引进行全表扫描
避免对null值进行判断
为什么要有线程?
充分利用多核CPU资源:现代计算机大多配备多核CPU,如果程序只运行在一个线程上,就只能使用一个CPU核心,其他核心则处于闲置状态。通过创建多个线程,可以让程序的各个部分在不同的CPU核心上同时运行,从而充分利用多核CPU的计算能力,提高程序的整体执行效率。
实现并发操作:在一些需要同时进行多种操作的场景中,线程可以发挥重要作用。比如在图形用户界面(GUI)程序中,主线程负责界面的绘制和用户交互,而其他线程可以负责数据的读取、计算等后台任务。
进程也可以完成,为什么还要线程?
- 进程:进程是系统进行资源分配和调度的基本单位,每个进程都有自己独立的内存空间、代码段、数据段等资源。创建和销毁进程需要分配和回收这些资源,系统开销较大。例如,创建一个新进程时,操作系统需要为其分配独立的内存空间,复制父进程的代码段和数据段等,这个过程涉及到大量的资源分配和初始化操作。
- 线程:线程是进程中的一个执行单元,多个线程共享所属进程的资源,如内存空间、文件描述符等。创建和销毁线程的开销相对较小,因为不需要像进程那样分配大量的独立资源。例如,在一个已有的进程中创建一个新线程,只需要分配少量的线程栈空间和一些线程控制块等资源,相比进程的创建和销毁,系统开销要小得多。
共享线程空间
线程之间通信更简单吗?
- 共享内存直接访问:线程共享所属进程的内存空间,这意味着线程可以直接通过内存地址访问和修改共享数据。无需像进程间通信那样,通过复杂的IPC机制来传递数据。例如,多个线程可以共同操作一个全局变量或共享的数据结构,读写操作就像在单线程程序中一样简单直接,大大降低了通信的复杂度和开销。
- 简化同步机制:虽然线程之间的通信需要考虑同步问题,但同步机制相对容易实现。常见的同步方式如互斥锁、条件变量等,都可以在进程内方便地使用。例如,多个线程需要访问同一个资源时,可以通过互斥锁来保证同一时刻只有一个线程能够访问该资源,避免了数据竞争和不一致的问题,而这些同步机制的使用在进程内比在进程间要简单得多。
- 数据竞争与一致性问题:由于线程共享内存,多个线程可能会同时对共享数据进行读写操作,这就容易引发数据竞争和一致性问题。如果没有妥善处理同步机制,可能会导致数据的错误读写,产生不可预料的结果。例如,两个线程同时对一个计数器进行递增操作,如果没有互斥锁保护,可能会出现计数器值不正确的情况。
- 两个进程通常不能直接访问同一个内存地址?
虚拟内存系统:现代操作系统普遍采用虚拟内存技术来管理内存。每个进程都有自己独立的虚拟地址空间,操作系统通过页表等数据结构将虚拟地址映射到物理内存地址。不同进程的虚拟地址空间是相互隔离的,即使两个进程使用相同的虚拟地址,这些地址也会被映射到不同的物理内存区域,从而实现了进程间内存的隔离。例如,进程A的虚拟地址0x1000和进程B的虚拟地址0x1000在物理内存中对应的是不同的位置,它们互不干扰。 - 内存保护机制:操作系统还提供了内存保护机制,防止一个进程访问另一个进程的内存空间。当一个进程试图访问不属于它的内存地址时,操作系统会检测到这种非法访问行为,并通常会终止该进程的运行,以保护系统的稳定性和其他进程的数据安全。例如,如果进程A试图读取进程B的内存数据,操作系统会触发一个异常,阻止进程A的非法操作。
进程A和进程B都可以通过系统调用请求一块共享内存,操作系统会为它们分配同一块物理内存,并在各自的虚拟地址空间中映射这块内存,这样两个进程就可以通过各自的虚拟地址访问共享内存中的数据。
- 虚拟地址空间划分:每个进程都有自己的虚拟地址空间,这个虚拟地址空间被划分为多个大小相等的页面(通常页面大小为4KB、8KB等)。虚拟地址由两部分组成:页目录索引和页表索引。例如,对于一个32位的虚拟地址,可以将其分为高10位作为页目录索引,中间10位作为页表索引,低12位作为页内偏移。
- 页目录与页表:操作系统为每个进程分配一个页目录,页目录中包含多个页目录项,每个页目录项指向一个页表。页表中包含多个页表项,每个页表项记录了一个虚拟页面与物理页面的映射关系,包括物理页面的基地址、访问权限等信息。
MMU的作用
- 地址转换:当进程执行时,CPU生成虚拟地址,内存管理单元(MMU)根据进程的页目录和页表将虚拟地址转换为物理地址。
- 隔离与保护:MMU还负责内存的隔离和保护。它会检查访问权限,确保进程只能访问自己有权访问的内存区域。如果进程试图访问非法的内存地址,MMU会触发一个异常,操作系统会根据异常情况进行处理,如终止进程、记录错误日志等。例如,如果进程A试图访问进程B的内存地址,MMU会检测到这种非法访问行为,并通知操作系统进行处理。
线程t1先获取了lock1,然后等待lock2;线程t2先获取了lock2,然后等待lock1。这就形成了一个循环等待链,导致两个线程都无法继续执行,从而发生了死锁。 lock1和lock2是两个锁,为什么会互相等待?
- 锁的获取顺序:
- 确保所有线程以相同的顺序获取锁。如果不同的线程以不同的顺序获取多个锁,很容易形成死锁。例如,线程T1先获取lock1再获取lock2,而线程T2先获取lock2再获取lock1,就可能形成死锁。
死锁发生在线程加多个锁的情况下:
获取锁的顺序不同
线程t1先获取了lock1,然后等待lock2;线程t2先获取了lock2,然后等待lock1。就是这俩线程都没释放锁,然后继续获取锁,由于顺序不同,导致了死锁是吧?
就比如线程要对两个文档进行修改读写,文档1需要加一个锁1,修改完了文档1没释放锁继续修改文档2,文档2也需要加一个锁2,但是这时另外一个线程也要对这两个文档进行修改读写,访问顺序还不一样,导致了死锁
这个场景中,两个线程分别以不同的顺序获取两个锁,导致了死锁。具体来说,线程T1和线程T2分别持有了一个锁,然后等待对方持有的另一个锁,由于它们都没有释放已持有的锁,导致了死锁。