问的都是基础知识,主要是三个部分:计网,数据库,java。计网答得挺好,数据答得一般,Java答得一坨。
目录
[4.lunix top 命令看到的是进程还是线程?](#4.lunix top 命令看到的是进程还是线程?)
11.String以及StringBuilder和StringBuffer的区别
14.讲讲concurrentHashMap(JDK1.8之后)
16.结合线程池的几个核心参数讲一个线程提交到线程池中的一个过程
1.TCP/IP协议的5层模型
送分题,TCP/IP是物理层,数据链路层,网络层,传输层,应用层。OSI是7层"物联网输会示用"
2.3次握手和4次挥手
也是送分题,我说的太详细了,讲了中间的所有syn,ack,ACK,seq还有fin等,在讲4次挥手的时候他打断了说太详细了不用讲了。
3次握手:
第一次握手 TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT 同步已发送状态
第二次握手 TCP服务器收到请求报文后,如果同意连接,则会向客户端发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了 SYN-RCVD 同步收到状态
第三次握手 TCP客户端收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED已建立连接状态 触发三次握手
4次挥手:
第一次挥手 客户端发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态
第二次挥手 服务器端接收到连接释放报文后,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT 关闭等待状态
第三次挥手 客户端接收到服务器端的确认请求后,客户端就会进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文,服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态,但此时TCP连接还未终止,必须要经过2MSL后(最长报文寿命),当客户端撤销相应的TCB后,客户端才会进入CLOSED关闭状态,服务器端接收到确认报文后,会立即进入CLOSED关闭状态,到这里TCP连接就断开了,四次挥手完成
3.操作系统中的进程和线程的区别
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
4.lunix top 命令看到的是进程还是线程?
top命令看到的是进程,top -h -pid 看到的pid进程下的线程
5.http协议的post和get有什么区别?
目的和用途:
GET 方法主要用于从指定的资源请求数据。它通常用于获取数据,而不是发送数据。
POST 方法主要用于将数据发送到服务器以创建或更新资源。它通常用于提交表单或上传文件。
数据发送:
GET 请求的数据通常附加在 URL 的查询字符串中,这意味着数据会暴露在 URL 中,长度也有限制(通常是 URL 长度限制,而不是数据本身的限制)。
POST 请求的数据包含在请求体中,这意味着数据不会暴露在 URL 中,更加安全。此外,POST 请求没有长度限制(虽然服务器和客户端可能会有限制)。
幂等性:
GET 请求是幂等的,即多次执行相同的 GET 请求不会产生不同的结果(除非资源本身发生了变化)。
POST 请求通常不是幂等的,因为每次发送 POST 请求都可能会创建新的资源或更新现有资源。
缓存:
GET 请求可以被缓存,这有助于减少网络流量并提高性能。
POST 请求通常不会被缓存,因为它们通常用于修改数据。
安全性:
GET 请求由于数据在 URL 中,所以不适合传输敏感信息,因为它可能被记录在浏览器历史、服务器日志或代理服务器中。
POST 请求通过请求体发送数据,因此更加安全,适合传输敏感信息。但是,这并不意味着 POST 请求本身更加安全,因为还需要其他安全措施(如 HTTPS)来保护数据。
书签和收藏夹:
GET 请求的 URL 可以被书签或收藏夹保存,方便用户以后再次访问。
POST 请求的 URL 通常不包含用户提交的数据,因此无法直接通过书签或收藏夹保存。
6.http协议有哪几块内容?
包括请求行,请求头,请求体。
-
请求行(Request Line):
- 方法:如 GET、POST、PUT、DELETE等,指定要执行的操作。
- 请求 URI(统一资源标识符):请求的资源路径,通常包括主机名、端口号(如果非默认)、路径和查询字符串。
- HTTP 版本:如 HTTP/1.1 或 HTTP/2。
请求行的格式示例:
GET /index.html HTTP/1.1
-
请求头(Request Headers):
- 包含了客户端环境信息、请求体的大小(如果有)、客户端支持的压缩类型等。
- 常见的请求头包括
Host
、User-Agent
、Accept
、Accept-Encoding
、Content-Length
等。
-
请求体(可选):
- 在某些类型的HTTP请求(如 POST 和 PUT)中,请求体包含要发送给服务器的数据。
7.几个状态码对应的含义
200:请求成功 处理方式:获得响应的内容,进行处理
302:请求到的资源在一个不同的URL处临时保存 处理方式:重定向到临时的URL
404:没有找到 处理方式:丢弃
503:服务出错 由于临时的维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。
参考:http 请求包含哪几个部分,分别有何作用?_后端开发 最关注的是请求的哪部分-CSDN博客
8.数据库事务的隔离级别
数据库事务隔离级别是为了解决多个事务并发执行时可能出现的问题,如脏读、不可重复读和幻读等。SQL标准定义了四种隔离级别,它们分别是:
-
Read Uncommitted(读未提交):这是最低的隔离级别,允许事务读取尚未提交的数据变更,可能会导致脏读。
-
Read Committed(读已提交):这个级别保证一个事务只能看见已经提交事务所做的改变,但不可重复读仍可能发生。
-
Repeatable Read(可重复读):这是MySQL的默认事务隔离级别,它确保在同一事务中多次读取相同数据时,结果是一致的,但可能会导致幻读。
-
Serializable(可串行化):这是最高的隔离级别,通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。但这会导致性能问题,因为它可能导致大量的超时和锁竞争。
9.脏读和幻读:
-
脏读 :一个事务读取另一个未提交事务的数据。
-
不可重复读 :在一个事务内,多次读取同一数据集合时,由于其他事务的修改,导致多次读取的结果不一致。
-
幻读:在一个事务内,执行两次查询,第二次查询结果中包含了第一次查询中未出现的数据或者缺少了原有的数据。
![](https://i-blog.csdnimg.cn/direct/7ec63c2c71414420821c18c0ae5d12b3.png)
9.主键索引和非主键索引有什么区别?(聚集索引和二级索引)
存储结构:
主键索引:在 InnoDB 存储引擎中,主键索引采用聚簇索引(Clustered Index)的存储方式。聚簇索引的特点是数据行和索引项是存储在一起的,索引的叶子节点直接包含了完整的数据记录。
非主键索引:非主键索引通常采用非聚簇索引(Non - Clustered Index)的存储方式。非聚簇索引的叶子节点存储的是索引列的值以及对应的主键值,而不是完整的数据记录。
查询效率:
主键索引:对于基于主键的查询,主键索引的查询效率非常高,因为可以直接通过聚簇索引定位到数据行,无需进行二次查找。
非主键索引:对于基于非主键索引列的查询,首先需要在非主键索引中查找对应的主键值,然后再通过主键值在主键索引中查找完整的数据记录,这个过程称为回表。因此,非主键索引的查询效率相对较低,尤其是在需要查询大量数据时,回表操作会增加额外的开销。
唯一性:
主键索引:主键索引具有唯一性约束,即主键列中的值必须是唯一的,且不能为 NULL。这是为了确保每一行记录都能被唯一标识。
非主键索引:非主键索引的创建和维护相对简单,因为它不影响数据行的物理存储顺序。在插入、更新和删除数据时,只需要更新非主键索引的索引项,而不需要移动数据行。但是,过多的非主键索引会占用额外的存储空间,并且在数据更新时需要同时更新多个索引,也会增加一定的维护成本。
10.非主键索引一定要回表吗?
不一定,在大多数情况下,当使用非主键索引进行查询,且查询的列不包含在该非主键索引中时,就需要进行回表操作,当查询的列都包含在非主键索引中时,就不需要进行回表操作,这种查询被称为索引覆盖查询。覆盖索引是查询使用了索引,返回的列,必须在索引中能够全部找到。
11.String以及StringBuilder和StringBuffer的区别
String :是不可变类。这意味着一旦创建了String
对象,它的内容就不能再被修改。每次对字符串的修改操作,实际上都会生成一个新的字符串对象,而不会改变原有的对象。这种不可变性提供了较高的安全性和性能优化,但在频繁操作字符串时会导致大量的临时对象生成,影响性能。
StringBuffer:是一个可变类,用于构建和修改字符串。与String不同,StringBuffer允许在不创建新对象的情况下修改字符串的内容。此外,StringBuffer是线程安全的,这意味着它的所有方法都是同步的,多个线程可以安全地操作同一个StringBuffer对象,而不会发生数据竞争。
StringBuilder:也是一个可变类,允许在不生成新对象的情况下修改字符串。StringBuilder和StringBuffer的主要区别在于,StringBuilder是非线程安全的,它的操作不是同步的,不能保证多线程环境中的安全性。但正因为没有同步开销,StringBuilder在单线程环境中的性能优于StringBuffer。
![](https://i-blog.csdnimg.cn/direct/ab2116878efe425aae572d99a10abc4d.png)
string不可变是因为底层是一个final修饰的char数组,并且没有对外提供修改方法,拼接,替换等方法实际是创建了一个新的数组。不变性可以做到多线程线程安全,而且hashcode不变,使用String作为哈希表的键时能够提高性能。
StringBuilder
和 StringBuffer
都继承自 AbstractStringBuilder
类,它们内部同样使用一个字符数组 value
来存储字符串内容,但这个数组没有被 final
修饰,并且提供了一些方法可以修改数组中的内容,如 append()
、insert()
、delete()
等,这些方法可以直接修改内部字符数组 value
中的内容,而不需要创建新的对象(除非容量不足时进行扩容)。目的是为了在需要频繁修改字符串内容的场景下提高性能。如果使用 String
进行频繁的字符串拼接等操作,会产生大量的临时对象,导致频繁的垃圾回收,影响性能。而 StringBuilder
和 StringBuffer
可以直接在原对象上进行修改,避免了创建大量临时对象的开销。
![](https://i-blog.csdnimg.cn/direct/9f7185c47c69433fa3358f5b5749d2d6.png)
12.HashMap的数据结构
数组加链表,当链表长度大于8,数组长度大于64链表会变成红黑树。
13.hashMap什么时候线程不安全?
1、put的时候导致的多线程数据不一致。
比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
2、在扩容的时候,jdk1.8之前是采用头插法,当两个线程同时检测到hashmap需要扩容,在进行同时扩容的时候有可能会造成链表的循环,主要原因就是,采用头插法,新链表与旧链表的顺序是反的,在1.8后采用尾插法就不会出现这种问题,同时1.8的链表长度如果大于8就会转变成红黑树。
14.讲讲concurrentHashMap(JDK1.8之后)
摒弃了分段锁机制,采用 CAS(Compare - And - Swap)和 synchronized
来保证并发操作的线程安全性。数据结构上采用数组 + 链表 + 红黑树,与 JDK 1.8 的 HashMap
类似。
- CAS:在初始化数组、扩容等操作中使用 CAS 操作。CAS 是一种无锁算法,它通过比较内存中的值和预期值是否相等,如果相等则更新内存中的值,否则重试。例如,在初始化数组时,使用 CAS 操作来确保只有一个线程可以成功初始化数组。
- synchronized:在插入、删除和查找操作中,当需要对某个桶进行操作时,使用
synchronized
关键字对该桶进行加锁。由于只对单个桶加锁,不同的线程可以同时访问不同的桶,从而实现高并发。
jdk1.8之前是在整个put方法上加synthornized,而jdk1.8之后是在方法里面当发生冲突的那一段代码加锁。
put方法:
做插入操作时,首先进入乐观锁,
然后,在乐观锁中判断容器是否初始化,
如果没初始化则初始化容器,
如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。
如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。
如果没有扩容,则进行最后一步,先加锁,然后找到hash值相同的那个节点(hash冲突),
循环判断这个节点上的链表,决定做覆盖操作还是插入操作。
循环结束,插入完毕。
get方法:
看上面这代码,ConcurrentHashMap
的get()
方法是不加锁的,为什么可以不加锁?因为table
有volatile
关键字修饰,保证每次获取值都是最新的。
参考:ConcurrentHashMap原理详解(太细了)-CSDN博客
15.实现多线程的方法
-
继承Thread类
-
实现Runnable接口
-
实现Callable接口( JDK1.5>= )
-
线程池方式创建
16.结合线程池的几个核心参数讲一个线程提交到线程池中的一个过程
1.核心参数
ThreadPoolExecutor
的构造函数如下:
java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize
:核心线程数。线程池在初始化时会创建一定数量的核心线程,这些线程在空闲时也不会被销毁,会一直存活在线程池中等待任务的到来。maximumPoolSize
:最大线程数。线程池允许创建的最大线程数量。当核心线程都在执行任务,且任务队列已满时,线程池会创建新的线程,直到线程数量达到最大线程数。keepAliveTime
:线程空闲时间。当线程池中的线程数量超过核心线程数时,多余的线程在空闲一段时间后会被销毁,这段空闲时间就是keepAliveTime
。unit
:keepAliveTime
的时间单位,例如TimeUnit.SECONDS
表示秒。workQueue
:任务队列。用于存储等待执行的任务。当核心线程都在忙碌时,新提交的任务会被放入这个队列中等待执行。常见的任务队列有ArrayBlockingQueue
、LinkedBlockingQueue
等。threadFactory
:线程工厂,用于创建线程。可以通过自定义线程工厂来设置线程的名称、优先级等属性。handler
:拒绝策略。当任务队列已满,且线程池中的线程数量达到最大线程数时,新提交的任务会触发拒绝策略。常见的拒绝策略有AbortPolicy
(直接抛出异常)、CallerRunsPolicy
(由提交任务的线程自己执行)等。
2.线程任务提交到线程池的过程
假设我们创建了一个线程池,代码如下:
java
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 线程空闲时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(3), // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 提交任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Executing task " + taskId + " by thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
- 检查核心线程数 :当一个新任务提交到线程池时,线程池首先会检查当前线程数量是否小于核心线程数(
corePoolSize
)。如果是,线程池会创建一个新的核心线程来执行该任务。在上述示例中,最初提交的 2 个任务会分别由 2 个新创建的核心线程来执行。 - 放入任务队列 :如果当前线程数量已经达到核心线程数,新任务会被放入任务队列(
workQueue
)中等待执行。在示例中,当提交第 3、4、5 个任务时,由于核心线程数为 2,这 3 个任务会被放入LinkedBlockingQueue
中等待。 - 创建新线程 :如果任务队列已满,且当前线程数量小于最大线程数(
maximumPoolSize
),线程池会创建一个新的非核心线程来执行该任务。在示例中,当提交第 6、7、8 个任务时,由于任务队列已满(队列容量为 3),且最大线程数为 5,线程池会创建 3 个新的非核心线程来执行这 3 个任务。 - 触发拒绝策略 :如果任务队列已满,且当前线程数量已经达到最大线程数,新提交的任务会触发拒绝策略(
handler
)。在示例中,当提交第 9、10 个任务时,由于任务队列已满,且线程数量已达到最大线程数 5,会触发AbortPolicy
拒绝策略,直接抛出RejectedExecutionException
异常。 - 线程空闲处理 :当线程完成任务后,如果线程数量超过核心线程数,且该线程空闲时间超过
keepAliveTime
,这个多余的线程会被销毁,以释放系统资源。
17.如何保证缓存中的数据和数据库中的数据是一致的?
缓存更新策略
**1.先更新数据库,再更新缓存:**这种策略是先更新数据库中的数据,更新成功后再去更新缓存里对应的数据,不过它存在并发问题,多个线程同时更新时,可能会使缓存更新顺序混乱,最终导致缓存数据状态错误。比如两个线程同时更新同一条数据,就可能出现后更新数据库的线程先更新了缓存,造成数据不一致。它比较适用于对缓存更新实时性要求不高,并且写操作较少的场景。
2.**先删除缓存,再更新数据库:**执行数据更新操作时,先把缓存里对应的数据删除,再去更新数据库。后续请求获取数据时,若发现缓存中无数据,就会从数据库读取最新数据并更新到缓存。在高并发场景下会有短暂的数据不一致情况。例如一个线程删除缓存后还没更新数据库,另一个线程读取数据,就会把数据库里的旧数据更新到缓存,等第一个线程更新完数据库,缓存数据就成旧的了。适合读多写少的场景,因为频繁的读操作能及时把最新数据更新到缓存。
重试机制
**1.本地重试:**当更新数据库成功但删除缓存失败时,在本地进行多次重试。可以用循环和计数器实现,设置最大重试次数,达到次数仍失败就记录日志或做其他处理。
2.**消息队列重试:**删除缓存失败时,把删除任务发到消息队列,有专门的消费者处理。若处理失败,任务会重新放回队列,直到成功或达到最大重试次数。能避免本地重试阻塞业务线程,提高系统吞吐量。
异步更新
**数据库变更监听:**通过监听数据库的变更日志,像 MySQL 的 binlog。数据库数据变更时,触发缓存更新或删除操作。比如用 Canal 工具监听 MySQL 的 binlog,把变更信息发到消息队列,由消费者处理缓存。能实现数据库和缓存的异步更新,降低业务代码复杂度,保证数据最终一致性。
缓存过期策略
**合理设置缓存过期时间:**给缓存数据设置合理的过期时间,过期后下次请求会从数据库获取最新数据更新缓存。过期时间要根据业务需求和数据更新频率确定,太短会频繁读数据库影响性能,太长则数据不一致时间会延长。
分布式锁
**读写锁:**进行读写操作时用读写锁,保证同一时间只有一个线程能写,多个线程可以同时读。写操作先获取写锁,更新数据库和缓存后释放;读操作获取读锁,读取数据后释放。能避免并发读写导致的数据不一致,提升系统并发性能。