1. 说说计算机存储结构
计算机存储结构通常包括这几个层次:
主存储器(Main Memory):也称为内存(RAM,Random Access Memory),主要用于存储当前正在执行的程序和数据。它是计算机中最快速但容量有限的存储设备。数据可以随机读取和写入,但在断电后数据会丢失。
辅助存储器(Secondary Storage):辅助存储器用于持久性存储数据和程序,如硬盘驱动器(HDD)和固态驱动器(SSD)。与主存储器相比,它们容量更大,但速度较慢。数据在断电后仍然保持。
缓存存储器(Cache Memory):缓存存储器是位于主存储器和中央处理器(CPU)之间的高速缓存,用于提高数据访问速度。它存储最常用的数据和指令,以减少对主存储器的访问次数。
寄存器(Registers):寄存器是位于CPU内部的最快速的存储设备。它们用于存储CPU当前执行的指令和操作数,以及中间计算结果。寄存器的数量有限,通常以位数来衡量(如32位或64位寄存器)。
光盘和磁盘:光盘(如CD和DVD)和磁盘(如软盘)是可移动的辅助存储设备,用于存储大量数据,如音频、视频和文件。它们的访问速度比主存储器和缓存慢,但容量较大。
网络存储:网络存储包括各种云存储和网络附加存储设备,允许用户通过互联网访问和存储数据。这种存储方式越来越常见,允许数据在多个设备之间共享和同步。
外部设备:外部设备如USB闪存驱动器、外部硬盘和网络附加存储设备可以连接到计算机,用于数据备份、传输和存储。
2 操作系统怎样管理内存
内存分配和回收:操作系统负责管理系统中可用的物理内存。当一个程序需要内存时,操作系统会分配一块适当大小的内存空间给该程序,并记录已分配内存的相关信息。当程序不再需要内存时,操作系统会将这些内存释放出来,以便其他程序使用。这个过程通常包括内存分配表、内存回收算法等。
内存保护:操作系统确保不同程序的内存空间相互隔离,以防止一个程序意外地访问或修改其他程序的内存数据。这通常通过硬件机制(如CPU的特权级别和内存保护位)和操作系统的权限管理来实现。
虚拟内存:虚拟内存是一种操作系统提供的抽象层,它将物理内存和磁盘上的存储结合在一起,使得系统似乎具有比物理内存更大的地址空间。操作系统根据需要将数据从磁盘交换到物理内存中,以便程序能够访问。这有助于提高系统的性能和多任务处理能力。
内存分页和分段:操作系统通常将物理内存划分为页面或段,以便更有效地管理内存。分页系统将内存划分为固定大小的页面,而分段系统将内存划分为不同大小的段。这些机制有助于操作系统有效地管理内存的分配和回收。
内存交换:当物理内存不足时,操作系统可以将不活动的程序或数据移动到磁盘上,以释放出更多的物理内存供其他程序使用。这个过程称为内存交换,它可以帮助系统继续运行,尽管物理内存有限。
内存管理单元(MMU):硬件中的MMU是一个关键组件,它协助操作系统实现虚拟内存和内存保护。MMU将逻辑地址(由程序生成)映射到物理地址(在物理内存中)。这有助于隔离不同程序的内存空间,同时使虚拟内存和物理内存之间的映射更加高效。
3. cpu 的组成
CPU的组成主要包括这几部分:控制单元、算术逻辑单元、寄存器、缓存、时钟
控制单元: 控制单元(Control Unit):控制单元是CPU的核心部分之一,负责协调和控制CPU的各个部件的操作。它从内存中读取指令,解码这些指令,然后执行它们。控制单元还负责处理异常情况和中断。
算术逻辑单元(Arithmetic Logic Unit,ALU):ALU执行各种算术和逻辑运算,包括加法、减法、乘法、除法、逻辑与、逻辑或等。它接收来自内存和寄存器的数据,并根据控制单元发出的指令执行相应的操作。
寄存器(Registers):寄存器是CPU内部的高速存储器单元,用于存储临时数据和指令操作数。寄存器非常快速,可以直接与ALU交互,因此在CPU的运算过程中起到关键作用。其中包括:程序计数器、指令寄存器、通用寄存器
缓存(Cache):缓存是一种高速存储器,用于存储频繁访问的数据和指令,以提高访问速度。现代CPU通常包括多级缓存,其中L1缓存最接近CPU核心,L2缓存较大但速度较慢,L3缓存更大但速度更慢。缓存有助于减少CPU与主内存之间的数据传输延迟。
时钟:时钟是CPU的关键部分,它以固定的速度发出脉冲信号,用于同步CPU内部操作。时钟速度通常以赫兹(Hz)为单位表示,例如1GHz表示每秒发出10^9次脉冲信号。
4. 死锁产生条件
死锁是指多个进程或线程在竞争有限资源时可能遇到的一种互相等待的情况,导致它们都无法继续执行下去。死锁产生的充分条件通常包括以下四个条件,它们必须同时满足才能引发死锁:
互斥条件(Mutual Exclusion):至少有一个资源必须是独占性的,即一次只能被一个进程或线程占用。这意味着当一个进程占用了该资源时,其他进程无法同时占用,必须等待释放。
占有和等待条件(Hold and Wait):进程必须持有至少一个资源,并且等待获取其他资源。这表示进程在等待其他资源时不会释放已经占有的资源,导致其他进程无法使用这些资源。
不可抢占条件(No Preemption):资源不能被强制从一个进程手中抢占,只能由持有资源的进程自愿释放。这意味着其他进程不能强制占用已被其他进程占有的资源。
循环等待条件(Circular Wait):存在一个进程等待链,每个进程都在等待下一个进程所占有的资源。这导致一个循环,使得每个进程都无法继续执行
5. 什么情况下栈溢出
在Java中,栈溢出通常是指方法调用栈(Method Call Stack)溢出,也就是由于方法调用的递归深度太大而导致栈空间不足。
栈溢出通常发生在以下情况下:
递归深度过大:递归函数调用自身或其他函数时,每次调用都会在栈上分配一段内存,如果递归深度很大,栈空间可能会耗尽。
无限循环递归:一个无限循环中,如果递归调用导致栈不断增长,最终可能导致栈溢出。
为了防止Java中的栈溢出,您可以采取以下措施:
限制递归深度:确保递归函数的递归深度有限,或使用迭代替代递归。
优化递归算法:在递归算法中,可以尝试减少方法调用的次数,从而减少栈的使用。
增大栈大小:在某些情况下,可以通过设置JVM参数来增加栈的大小,但这不是一种推荐的解决方案,因为栈大小的增加可能导致其他问题。
6. Arraylist与LinkedList区别
可以从它们的底层数据结构、效率、开销进行阐述
ArrayList是数组的数据结构,LinkedList是链表的数据结构。
随机访问的时候,ArrayList的效率比较高,因为LinkedList要移动指针,而ArrayList是基于索引(index)的数据结构,可以直接映射到。
插入、删除数据时,LinkedList的效率比较高,因为ArrayList要移动数据。
LinkedList比ArrayList开销更大,因为LinkedList的节点除了存储数据,还需要存储引用。
7. 红黑树的特点和使用场景
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它具有以下特点:
自平衡性:红黑树是一种自平衡的二叉搜索树,它通过一系列的插入和删除操作来维持树的平衡。这确保了树的高度保持在对数范围内,使得基本操作(如查找、插入和删除)的时间复杂度保持在O(log n)。
节点颜色:每个节点都被标记为红色或黑色。这些颜色标记有助于维护树的平衡。
每个节点要么是红色,要么是黑色。
根节点必须是黑色。
所有叶子节点(NIL节点)都是黑色。
如果一个节点是红色,那么它的两个子节点都必须是黑色(没有两个相连的红色节点)。
从任何节点到其每个叶子的简单路径上,黑色节点的数目必须相等。
插入和删除操作的平衡调整:当插入或删除节点时,红黑树会执行一系列平衡调整操作,以确保树的性质不会被破坏。这些调整通常包括颜色变换和旋转操作。这些操作确保了树保持平衡,不会出现偏斜。
快速查找和插入:红黑树具有较快的查找和插入操作。由于树的平衡性质,平均和最坏情况下的时间复杂度都是O(log n)。
使用场景
Java的TreeMap和TreeSet :也使用红黑树来实现有序的键值对存储和查找。
文件系统 :某些文件系统使用红黑树来管理文件和目录的索引。这有助于快速查找和访问文件。
操作系统进程调度 :一些操作系统的进程调度器使用红黑树来管理进程的调度顺序。红黑树确保了公平性和高效性。
内存分配器:一些内存分配器使用红黑树来管理内存块的分配和释放,以提高内存分配的性能。
8. HashMap 和 Concurrentmap 区别
HashMap
底层由链表+数组+红黑树实现
可以存储null键和null值
线性不安全
初始容量为16,扩容每次都是2的n次幂
加载因子为0.75,当Map中元素总数超过Entry数组的0.75,触发扩容操作.
并发情况下,HashMap进行put操作会引起死循环,导致CPU利用率接近100%
HashMap是对Map接口的实现
ConcurrentHashMap
HashMap 和 ConcurrentMap 的底层实现都使用了数组和链表,以及在需要时使用红黑树来提高性能。
不能存储null键和值
ConcurrentHashMap是线程安全的
JDK 8之前,ConcurrentHashMap使用锁分段技术确保线性安全
JDK8为何又放弃分段锁,是因为多个分段锁浪费内存空间,竞争同一个锁的概率非常小,分段锁反而会造成效率低。
JDK 8 引入了一种新的 ConcurrentHashMap 实现,**称为 "CAS + Synchronized",而不再使用分段锁。**这是因为分段锁虽然在某些情况下可以提供良好的并发性能,但它们确实存在一些缺点,包括内存开销和可能的竞争条件。
内存开销 :每个分段都需要维护一个独立的锁,这会导致内存开销增加,特别是当你有大量的分段时。这可能会在某些情况下占用大量内存,不利于性能和资源使用。
竞争条件:虽然分段锁减少了竞争的可能性,但当多个线程试图修改同一分段内的数据时,仍然可能发生竞争条件。这种情况下,需要线程等待并争夺分段级别的锁,可能导致性能下降。
9. 服务器接收 http 请求怎样区别哪个进程
端口号:每个进程可以监听不同的端口号。当客户端发送HTTP请求时,请求中包含目标端口号。服务器通过请求的目标端口号来确定将请求路由到哪个进程。不同的进程通常监听不同的端口。例如,常见的HTTP服务器(如Apache和Nginx)通常监听默认的HTTP端口80或HTTPS端口443,不同进程监听不同的端口。
域名:对于同一IP地址上的多个虚拟主机,服务器可以通过HTTP请求的Host头部来区分它们。根据请求中的域名信息,服务器将请求路由到不同的进程或应用程序。
URL路径:服务器可以根据HTTP请求的URL路径来区分不同的应用程序或处理程序。不同的路径可以映射到不同的进程或应用程序。例如,一个服务器可以将/app1的请求路由到一个进程,将/app2的请求路由到另一个进程,以此类推。
反向代理:有时,一个前端服务器(通常是反向代理服务器,如Nginx或Apache HTTP Server)会接收所有HTTP请求,然后将请求路由到后端服务器或进程。反向代理服务器可以根据请求的不同特征(如域名、路径、端口等)来决定将请求转发给哪个后端进程。
会话标识符:对于基于会话的应用程序,服务器通常使用会话标识符来区分不同的用户会话。会话标识符可以存储在Cookie中或通过URL参数传递,服务器使用它来将请求路由到正确的用户会话。
自定义头部:有些服务器和应用程序可能使用自定义的HTTP头部来区分请求的目标进程。这需要服务器和应用程序之间的协商和定制。
负载均衡器:在大型应用程序和高流量环境中,通常使用负载均衡器来分发HTTP请求到多个后端进程或服务器实例。负载均衡器可以根据不同的算法和规则来决定请求的路由。
10.服务并发量高时,流量怎样负载均衡
一些常见的负载均衡策略和方法
轮询(Round Robin):这是一种最简单的负载均衡策略,其中负载均衡器将每个新的请求按照轮询的方式分发给后端服务器。每个服务器依次接收请求,然后再次从头开始。这样可以确保请求均匀分布到所有服务器上。
加权轮询(Weighted Round Robin):在加权轮询中,每个后端服务器都分配一个权重,权重高的服务器会获得更多的请求。这种方式允许根据服务器的性能和资源配置来分发负载。
最少连接(Least Connections):负载均衡器将请求发送到当前具有最少连接数的服务器。这可以确保连接较少的服务器不会被过载,从而提高性能。
IP散列(IP Hashing):根据客户端的IP地址计算散列值,并将请求发送到对应散列值的服务器。这种方法可以确保相同IP地址的客户端始终访问同一台服务器,适用于需要维护会话一致性的应用。
最短响应时间(Least Response Time):负载均衡器会根据服务器的响应时间来选择下一个服务器。这有助于将请求发送到响应时间最短的服务器,提高用户体验。
随机(Random):负载均衡器随机选择一个后端服务器来处理请求。虽然这种方法不会平衡负载,但在某些情况下可能有用。
内容感知负载均衡:根据请求的内容类型,将请求路由到不同类型的后端服务器。例如,可以将图像请求路由到图像服务器,将视频请求路由到视频服务器,以提高性能
11.B+树 B-树的区别,为什么不用红黑树做索引
在B-树树中,键和值即存放在内部节点又存放在叶子节点;在 B+树中,内部节点只 存键,叶子节点则同时存放键和值。
B+树的叶子节点有一条链相连,而 B 树的叶子节点各自独立的。
B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的,链表 连着的。那么 B+树使得范围查找,排序查找,分组查找以及去重查找变得异 常简单。.
B+树非叶子节点上是不存储数据的,仅存储键值,而 B-树节点中不仅存储键 值,也会存储数据。innodb 中页的默认大小是 16KB,如果不存储数据,那么 就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会 更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数有会再次减少,数据查询 的效率也会更快.
为什么不用红黑树做索引?
红黑树是一种自平衡的二叉搜索树,它在平衡性和查找效率上是非常好的。然而,红黑树在磁盘存储和数据库索引场景下可能不如B+树效率高效。这是由于以下几个原因:
磁盘IO效率 :B+树采用了一种层次化的索引结构,它的非叶子节点只包含索引信息,而数据存储在叶子节点上。这种结构使得每次磁盘IO能够获取更多的数据。而红黑树在每个节点都存储数据,这样会增加磁盘IO次数,降低IO效率。
顺序访问性能 :B+树的叶子节点形成有序链表,这使得范围查询变得更高效,可以很方便地进行范围遍历。而红黑树不具备这种特性。
内存占用:红黑树在每个节点都需要存储数据,而B+树的非叶子节点只需要存储索引信息,这样在内存占用上,B+树相对更节省。
12. Redis的使用场景
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。 Redis 五种数据类型的应用场景:
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
Hash 类型:缓存对象、购物车等。
Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
Redis 后续版本又支持四种数据类型,它们的应用场景如下:
BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
13. redis 为什么快
Redis 的大部分操作都在内存中完成 ,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
Redis 采用单线程模型可以避免了多线程之间的竞争 ,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
14.redis 分布式锁如何实现的?可能会有哪些坑
可以参考我之前的文章
zookeeper实现分布式锁
redis实现分布式锁
15 算法题
怎样判断链表是否有环
java
快慢指针实现
public class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
this.next = null;
}
}
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
// 如果链表为空或只有一个节点,肯定没有环
return false;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针移动一步
fast = fast.next.next; // 快指针移动两步
// 如果快指针追上了慢指针,说明链表中有环
if (slow == fast) {
return true;
}
}
// 如果快指针到达链表末尾,说明没有环
return false;
}
删除倒数第 n 个节点
java
public ListNode removeNthFromEnd(ListNode head, int n) {
// 创建一个虚拟头节点,以便于处理删除头节点的情况
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode first = dummy;
ListNode second = dummy;
// 将first指针向前移动n+1步
for (int i = 0; i <= n; i++) {
first = first.next;
}
// 同时移动first和second,直到first到达链表末尾
while (first != null) {
first = first.next;
second = second.next;
}
// 删除倒数第n个节点
second.next = second.next.next;
return dummy.next; // 返回新的头节点
}