题目来源:https://www.nowcoder.com/feed/main/detail/d39aabc0debd4dba810b4b9671d54348
本期是【捞捞面经】系列文章的第 2 期,持续更新中…。(更多与往期下方仓库直达)
《捞捞面经》系列正式开始连载啦,据说看了这个系列的朋友都拿到了大厂offer~
注:养成先看真题,自己模拟回答,再看解析参考(别忘随手一键三连哦~)
1.基础题
2.代码题
3.场景题
注意:博主基础题即不过多介绍,只选经典题目分析。
常见的网络IO模型有以下几种:
异步网络模型可以应用于任何需要高并发、高性能、高实时性的场景,以提高系统的性能和可扩展性,提高用户体验。
异步网络模型在社交和购物等场景下也非常常见。比如:
举个具体实际的例子,常常玩的 王者荣耀。(个人看法)它需要处理大量的游戏玩家请求,包括登录、注册、查询游戏数据、游戏操作等。如果使用阻塞式IO模型,每个请求都需要创建一个线程来处理,当并发请求量较大时,线程的创建和销毁会带来很大的开销,导致服务器的性能和吞吐量下降。
而如果使用异步网络模型,可以通过 事件驱动的方式处理请求,当有玩家请求到达时,服务器不需要创建新的线程,而是通 过异步IO操作来处理请求,当IO操作完成后,服务器会回调相应的处理函数进行处理,这样可以大大减少线程的创建和销毁开销,提高服务器的性能和吞吐量。
另外,异步网络模型还可以应用于实时数据处理系统,比如金融交易系统、在线广告系统
等,这些系统需要实时处理大量的数据请求,如果使用阻塞式IO模型,会导致数据处理的延迟时间较长,影响系统的实时性。而使用异步网络模型,可以通过事件驱动的方式实时处理数据请求,提高系统的实时性和性能。
Lambda
表达式来实现异步回调。Future
对象是 Java 中的一种异步编程解决方案,它可以将异步操作封装成一个 Future 对象,然后使用 Future.get() 方法来等待异步操作的完成,从而实现异步操作的同步化编程。CompletableFuture
是Java 8中新增的异步编程解决方案,它可以将异步操作封装成一个 CompletableFuture 对象,然后使用 CompletableFuture的方法来处理异步操作的结果,比如 thenApply()、thenAccept()、thenRun()等方法。在Java中,同步阻塞和同步非阻塞可以通过不同的 IO 模型来实现。
当涉及到高并发、高性能、高可靠性的场景时,选择合适的 IO 模型非常重要。下面结合具体场景来讲解:
大量的并发连接和实时数据交互
,因此同步非阻塞 IO 模型是比较适合的选择。在Java中,可以使用 Java NIO
或者 Netty
等框架来实现同步非阻塞IO模型。除了同步阻塞和同步非阻塞 IO 模型之外,还有一些其他的 IO 模型,比如异步IO模型、多路复用IO模型
等。在实际应用中,应该根据具体的场景和需求来选择合适的 IO 模型。
一次完整的HTTP请求通常包括以下步骤:(如果是从浏览器发起地址请求,还需要地址各种解析哦~)
TCP
协议与服务器建立连接,进行 “三次握手”。客户端发送 SYN 包,服务器回应 SYN+ACK 包,客户端再回应 ACK 包,完成连接建立。四次挥手
”。客户端发送 FIN 包,服务器回应 ACK 包,然后服务器发送 FIN 包,客户端回应ACK 包,完成连接关闭。总之,一次完整的 HTTP 请求包括建立 TCP 连接、发送 HTTP 请求、服务器处理请求、服务器返回 HTTP 响应和关闭 TCP 连接等步骤。在实际应用中,还需要考虑 HTTP 缓存、Cookie、会话管理等问题。
常见的长连接实现方式包括:
HTTP/1.1
协议支持长连接,客户端和服务器之间可以保持连接状态,可以在一定时间内进行多次请求和响应。在 HTTP 长连接中,客户端发送请求后,服务器会保持连接状态,直到客户端发送关闭连接的请求或者超时时间到达。WebSocket
是一种基于 HTTP 协议的长连接技术,它可以在客户端和服务器之间建立双向通信的连接,实现实时通信和推送服务。WebSocket 协议通过 HTTP协议的升级实现,客户端和服务器之间可以发送和接收数据帧,而不必重新建立连接。TCP
协议支持长连接,客户端和服务器之间可以保持连接状态,可以在一定时间内进行多次请求和响应。在TCP长连接中,客户端和服务器之间建立连接后,可以保持连接状态,直到客户端或服务器发送关闭连接的请求或者网络异常断开连接。长连接可以提高网络传输效率,常用于实时通信、推送服务等场景
我随便设计的一个简单的 Hashset
(仅供参考):
下面是一个简单的Java代码实现:
public class MyHashSet {
private static final int DEFAULT_CAPACITY = 16;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private Node[] table;
private int size;
private int threshold;
private float loadFactor;
public MyHashSet() {
this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public MyHashSet(int initialCapacity, float loadFactor) {
table = new Node[initialCapacity];
this.loadFactor = loadFactor;
threshold = (int) (initialCapacity * loadFactor);
}
public boolean add(T value) {
int hash = hash(value);
int index = indexFor(hash, table.length);
Node node = table[index];
while (node != null) {
if (node.value.equals(value)) {
return false;
}
node = node.next;
}
Node newNode = new Node<>(value, table[index]);
table[index] = newNode;
size++;
if (size > threshold) {
resize(table.length * 2);
}
return true;
}
public boolean remove(T value) {
int hash = hash(value);
int index = indexFor(hash, table.length);
Node node = table[index];
Node prev = null;
while (node != null) {
if (node.value.equals(value)) {
if (prev == null) {
table[index] = node.next;
} else {
prev.next = node.next;
}
size--;
return true;
}
prev = node;
node = node.next;
}
return false;
}
public boolean contains(T value) {
int hash = hash(value);
int index = indexFor(hash, table.length);
Node node = table[index];
while (node != null) {
if (node.value.equals(value)) {
return true;
}
node = node.next;
}
return false;
}
public int size() {
return size;
}
public void clear() {
Arrays.fill(table, null);
size = 0;
}
private int hash(T value) {
return value.hashCode();
}
private int indexFor(int hash, int length) {
return hash & (length - 1);
}
private void resize(int newCapacity) {
Node[] newTable = new Node[newCapacity];
for (Node node : table) {
while (node != null) {
Node next = node.next;
int index = indexFor(hash(node.value), newCapacity);
node.next = newTable[index];
newTable[index] = node;
node = next;
}
}
table = newTable;
threshold = (int) (newCapacity * loadFactor);
}
private static class Node {
T value;
Node next;
public Node(T value, Node next) {
this.value = value;
this.next = next;
}
}
}
将 1T
的数据加载到 200M
的内存中是不可能的,因为1T 的数据远远超过了 200M 的内存大小。因此,需要采用一些特殊的算法和技术来解决这个问题。
一种解决方案是使用 外部排序算法,将1T的数据分成多个小文件,每个小文件可以加载到内存中进行排序。然后,使用归并排序的思想将这些小文件合并成一个大文件,并在合并的过程中找到两行一样的数据。
具体步骤如下(参考):
而在实际操作中,还需要考虑磁盘读写速度、文件的读写方式等因素,以提高算法的效率和准确性。
在 Java 中打开 1T 的文件,第一步应该是确定文件的读取方式和读取范围。
其底层区别主要在于操作系统和文件系统的交互方式。
用鼠标打开文件是通过操作系统提供的图形用户界面(GUI)来实现的,用户点击图标,但实际操作系统会根据用户的操作来调用相应的API,从而实现文件的打开、读取、写入等操作。而这些 API 实际通常是操作系统提供的底层文件系统接口,例如 Windows 的 Win32 API
、Linux 的 POSIX API
等。
而用代码打开文件则是 **通过编程语言提供的文件操作API **来实现的,这些API通常是对操作系统底层文件系统接口的封装和抽象。通常可以使用 File、FileInputStream、FileOutputStream
等类来实现文件的打开、读取、写入等操作,这些类会调用底层的操作系统文件系统接口来实现相应的功能。
因此,从底层的角度来看,用代码打开文件和用鼠标打开文件的区别在于调用的API不同,但底层的文件系统接口是相同的。
GET、POST、PUT、DELETE
等。User-Agent、Content-Type、Cookie
等。头部信息提供了关于请求的附加信息,用于服务器处理请求。GET
请求,请求体通常为空。对于 POST 请求等需要传递数据的请求,请求体包含了要发送给服务器的数据。原文收录:https://github.com/xlcoding/NowcoderTop