Java的NIO工作机制

文章目录

    • 1. 问题引入
    • 2. NIO的工作方式
    • 3. Buffer的工作方式
    • 4. NIO数据访问方式

1. 问题引入

在网络通信中,当连接已经建立成功,服务端和客户端都会拥有一个Socket实例,每个Socket实例都有一个InputStream和OutputStream,并通过这两个对象来交换数据。同时我们也知道网络I/O都是以字节流传输的,当创建Socket对象时,操作系统会为InputStream和OutputStream分别分配一定大小的缓存区,数据的写入都是通过这个缓存区完成的。写入端将数据写到SendQ队列中,当队列填满时,数据将被转移到另一端的InputStream的RecvQ队列中,如果这时RecvQ已经满了,那么OutputStream的write方法将会阻塞直到RecvQ队列有足够的空间容纳SendQ发送的数据。值的注意的是,这个缓存区的大小及写入端的速度和读取端的速度非常影响这个连接的数据传输效率,由于可能发生阻塞,所以网络I/O与磁盘I/O不同的是数据的写入和读取还要有一个协调的过程,如果两边同时传送数据可能会产生死锁。使用NIO可以解决该问题。

死锁产生的根本原因是 客户端和服务端都需要对方释放资源,例如在缓存队列中写入数据。当缓存队列满时,写操作会被阻塞,但同时双方都可能需要对方的缓存队列空间,这样就形成了资源争用的情况

2. NIO的工作方式

  • BIO带来的挑战

BIO即阻塞I/O,不管是磁盘I/O还是网络I/O,数据载写入OutputStream或者从InputStream读取时都可能被阻塞,一旦被阻塞,线程将会失去CPU的使用权,这在当前大规模访问量和有性能要求的情况下时不能被接受的。虽然当前的网络I/O有一些解决方案,如一个客户端一个处理线程,出现阻塞时只能是一个线程阻塞而不会影响其他线程工作,还有为了减少系统线程的开销,采用线程池的办法来减少线程创建和回收的成本。单如果当前需要大量的HTTP长连接的情况,线程池可能无法创建那么多线程来保持连接。所以此时我们需要一种新的I/O操作方式。

磁盘IO场景
在磁盘 I/O 中,阻塞通常发生在数据的读取和写入过程中。假设一个应用程序需要从磁盘读取大量数据:

  1. 读取阻塞: 当应用程序发起读取请求时,系统会将请求发送给磁盘驱动器,然后等待磁盘驱动器将数据加载到内存中。在这个过程中,应用程序的线程会被阻塞,直到读取操作完成。如果读取的数据量较大,阻塞时间可能会显著增加。
  2. 写入阻塞: 类似地,当应用程序发起写入请求时,系统会将数据传输到磁盘,然后等待写入操作完成。写入操作的阻塞时间取决于数据的大小和磁盘的性能。如果写入的数据量很大,应用程序可能会长时间地被阻塞。

网络IO场景
在网络 I/O 中,BIO 模型同样存在阻塞问题。考虑一个基于阻塞 I/O 的服务器应用程序,它接受客户端连接并处理数据:

  1. 接受连接阻塞: 当服务器调用accept函数等待客户端连接时,如果没有客户端连接进来,该调用会一直阻塞。在这段时间内,服务器的线程无法执行其他任务,造成资源浪费。
  2. 读取数据阻塞: 在已经建立连接的情况下,当服务器调用read函数等待接收客户端发送的数据时,如果没有数据到达,该调用会一直阻塞。服务器线程被迫等待,可能会导致性能下降。
  3. 写入数据阻塞: 类似地,当服务器调用write函数将数据发送到客户端时,如果客户端接收缓冲区已满,写入操作也会阻塞。这可能使得服务器线程长时间处于等待状态。
  • NIO工作机制
    Java IO和NIO的主要区别在于两者的处理方式不同。Java IO是面向流(Stream)的,它将输入输出数据直接传输到目标设备或文件中,以流的形式进行读写;而NIO则是面向缓冲区(Buffer)的,它将会使用缓存去管理数据,使得读写操作更加快速和灵活。
    在这里插入图片描述

Buffer类

在 Java NIO 中,Buffer 类是一个抽象类,表示一个数据缓冲区,用于在通道(Channel)和原始数据之间进行数据传输。主要的 Buffer 子类包括 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer,它们分别对应不同的基本数据类型。

public abstract class Buffer { //mark 属性和 reset() 方法一起使用,用于在缓冲区中设置一个标记(mark)位置,并在之后通过 reset() 方法将当前位置重置为这个标记位置。mark 属性的作用是用于记录一个位置,以便后续能够回到该位置,方便重新处理或采取其他措施。private int mark = -1;//当前位置的索引private int position = 0;//限制位置的索引,即缓冲区中数据的有效长度private int limit;//缓存区的容量private int capacity;//内存段的代理对象final MemorySegmentProxy segment;//该方法用于设置新的索引位置public Buffer position(int newPosition) {//如果新的索引位置大于数据的有效长度,或小于0,抛出异常if (newPosition > limit | newPosition < 0)throw createPositionException(newPosition);//如果新的索引位置在mark标记之后,那么标记置为-1if (mark > newPosition) mark = -1;//然后将当前索引位置设置为新的索引位置position = newPosition;return this;}//设置新的有效数据长度public Buffer limit(int newLimit) {//如果新的有效数据长度大于缓冲区最大容量或小于0,抛出异常if (newLimit > capacity | newLimit < 0)throw createLimitException(newLimit);//将有效数据长度设置为新的有效数据长度limit = newLimit;//如果当前位置所以大于新的有效数据长度,那么当前位置索引同样设置为有效数据长度if (position > newLimit) position = newLimit;//mark如果大于新的有效数据长度,则直接设置为无效if (mark > newLimit) mark = -1;return this;}//用于标记当前索引位置public Buffer mark() {mark = position;return this;}//将当前索引位置重置为mark标记所在的位置public Buffer reset() {int m = mark;if (m < 0)throw new InvalidMarkException();position = m;return this;}//清空缓存区数据,实际上就是重置了mark、position和limitpublic Buffer clear() {position = 0;limit = capacity;mark = -1;return this;}//flip() 方法可以将缓冲区从写模式切换到读模式。调用 flip() 后,位置被设置为0,限制被设置为之前的位置,用于准备读取缓冲区中的数据。public Buffer flip() {//limit有效数据长度为当前索引所在位置limit = position;//postion设置为0,表示从0开始读取position = 0;mark = -1;return this;}//将缓冲区的位置设置为 0,限制保持不变,用于重新读取缓冲区中的数据,类似于 flip() 但不改变限制。public Buffer rewind() {position = 0;mark = -1;return this;}//获取buffer中剩余的有效容量public final int remaining() {int rem = limit - position;return rem > 0 ? rem : 0;}}

Channel类
Java NIO 中,Channel 是一个接口,它提供了用于读取和写入数据的统一的 API。Channel 接口是 NIO 用于与 I/O 设备(如文件、套接字、选择器等)交互的核心部分。Channel 接口的实现类可以包括文件通道、套接字通道等。

public interface Channel extends Closeable {
//判断通道是否打开public boolean isOpen();//关闭通道public void close() throws IOException;
}

Seletionkey类
SelectionKey 类是 Java NIO 中的关键类,它用于表示注册到 Selector 上的通道和对应的事件。SelectionKey 对象是 Selector 与通道之间的桥梁,它包含了与通道相关的一些信息,以及通道所感兴趣的事件,以便在选择器上进行有效的事件选择。

选择键则是一种将通道和选择器进行关联的机制。

public abstract class SelectionKey {
//SelectableChannel 是 Java NIO 中表示支持非阻塞模式的通道的抽象类,该返回返回一个通道public abstract SelectableChannel channel();//该方法返回一个选择器public abstract Selector selector();//获取通道所感兴趣的操作集合(事件集合),返回一个位掩码,用于表示关注的事件public abstract int interestOps();//读操作的位掩码,用于表示通道已经准备好进行读操作。public static final int OP_READ = 1 << 0;//写操作的位掩码,用于表示通道已经准备好进行写操作。public static final int OP_WRITE = 1 << 2;//连接操作的位掩码, 用于表示连接已经建立public static final int OP_CONNECT = 1 << 3;//accept操作的位掩码, 用于表示通道已经准备好接受新的连接。public static final int OP_ACCEPT = 1 << 4;//返回通道现在是否可读public final boolean isReadable() {return (readyOps() & OP_READ) != 0;}//通道现在是否可写public final boolean isWritable() {return (readyOps() & OP_WRITE) != 0;}//通道是否已经连接就绪public final boolean isConnectable() {return (readyOps() & OP_CONNECT) != 0;}//通道是否可以接受新的连接public final boolean isAcceptable() {return (readyOps() & OP_ACCEPT) != 0;}//将指定的对象附加到此键public final Object attach(Object ob) {return ATTACHMENT.getAndSet(this, ob);}}

Selector类

Selector 是 Java NIO 中的一个关键类,用于实现非阻塞 I/O 操作的多路复用。通过 Selector,可以在单个线程上同时监控多个通道的事件,从而实现高效的事件驱动编程模型。

public abstract class Selector implements Closeable {//返回新创建的选择器实例public static Selector open() throws IOException {return SelectorProvider.provider().openSelector();}//判断selector是否已经打开public abstract boolean isOpen();//SelectorProvider 是一个抽象类,用于提供 Selector 和 Channel 的创建。每个 Selector 都与一个 SelectorProvider 实例关联,而SelectorProvider 实例的具体实现是由具体的操作系统提供的。public abstract SelectorProvider provider();//获取选择器上所有的键集public abstract Set<SelectionKey> keys();//关闭选择器public abstract void close() throws IOException;// 阻塞,直到至少有一个通道在选择器上准备好进行 I/O 操作,或者调用线程被中断。返回已经准备就绪的通道的数量。public abstract int select() throws IOException;//非阻塞地检查是否有通道准备好进行 I/O 操作。返回已经准备就绪的通道的数量。public abstract int selectNow() throws IOException;//唤醒因为调用 select 或 selectNow 方法而处于阻塞状态的线程。返回调用 wakeup 方法的选择器。public abstract Selector wakeup();
}

关键类就是Channel和Selector,它们是NIO中的两个核心概念。Channel 通过 register 方法可以注册到 Selector 中,以实现非阻塞 I/O 操作。Channel 和 Buffer 之间通过 read 和 write 方法进行数据的传输。数据首先被写入到缓冲区,然后从缓冲区读取到通道或从通道读取到缓冲区。Channel 可以通过 register 方法注册到 Selector 上,注册时需要指定感兴趣的事件,例如读、写等。SelectionKey 对象表示了一个通道在一个选择器上的注册信息,它与通道和选择器之间建立了关联。SelectionKey 包含了通道、选择器、感兴趣的操作集合(事件集合)、附件等信息。
在这里插入图片描述
下面看看NIO是如何工作的:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;public class NioServer {public static void main(String[] args) {try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open() //创建一个通道) {//将通道绑定到一个指定的端口,ServerSocketChannel使用选择器(Selector)来管理多个通道,可以在一个线程中处理多个通道的连接请求。serverSocketChannel.bind(new InetSocketAddress(8080));
//将通道设置为非阻塞模式serverSocketChannel.configureBlocking(false);//创建一个选择器Selector selector = Selector.open();//将通道注册到选择器中,状态是等待接受客户端连接serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("NIO Server started on port 8080");while (true) {//自旋获取准备好的通道int readyChannels = selector.select();if (readyChannels == 0) {continue;}//获取所有的键集Set<SelectionKey> selectedKeys = selector.selectedKeys();//迭代键集Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();//处理相应事件if (key.isAcceptable()) {handleAcceptEvent(key, selector);} else if (key.isReadable()) {handleReadEvent(key);}keyIterator.remove();}}} catch (IOException e) {e.printStackTrace();}}private static void handleAcceptEvent(SelectionKey key, Selector selector) throws IOException {//获取键所关联的通道ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();//准备接受连接SocketChannel clientChannel = serverSocketChannel.accept();//设置通道模式为非阻塞clientChannel.configureBlocking(false);//通道注册到选择器上,状态为可以读取数据clientChannel.register(selector, SelectionKey.OP_READ);System.out.println("Accepted connection from " + clientChannel);}private static void handleReadEvent(SelectionKey key) throws IOException {SocketChannel clientChannel = (SocketChannel) key.channel();//分配缓存区ByteBuffer buffer = ByteBuffer.allocate(1024);//通道读取int bytesRead = clientChannel.read(buffer);if (bytesRead == -1) {System.out.println("Connection closed by client: " + clientChannel);clientChannel.close();} else if (bytesRead > 0) {buffer.flip();clientChannel.write(buffer);}}
}

上面代码用NIO实现了一个简单的服务器

调用Selector的静态工厂方法创建一个选择器(这个静态工厂是操作系统底层实现的)。然后创建一个服务端Channel,绑定到一个Socket对象,并把这个通道注册到选择器上,把这个通道设置为非阻塞模式,然后就可以调用Selector的selectedKeys方法检查已经注册在这个选择器上的所有通信信道是否有需要事件发生,从而可以读取通信的数据,而这里读取的数据是Buffer,这个Buffer是我们可以控制的缓冲区。在上面这段程序中,将server的监听连接请求和事件吹了放在一个线程中,但是在事件应用中,我们通常会放在两个线程中,一个线程专门监听客户端的连接请求,而且是以阻塞的方式执行的;另一个线程专门负责处理请求,这个专门处理请求的线程才会真正采用NIO的方式。

下图展示量NIO的工作方式,Selector可以监听一组通信信道上的I/O状态,前提是这个Selector已经注册到这些通信通道中,选择器Selector可以调用select()方法检查已经注册的通信信道上I/O是否已经准备好,如果没有至少一个信道I/O状态发生变化,那么select方法会阻塞等待或在超时时间后返回0。如果有多个信道有数据,那么将会把这些数据分配到对应的数据Buffer中。所以关键的地方是,有一个线程来处理所有连接的数据交互,每个链接的数据交互不是阻塞方式的,所以可以同时处理大量的连接请求。
在这里插入图片描述

  • 总结

NIO模式下客户端和服务端通信流程对比如下:

1.服务端初始化:

服务端创建一个 ServerSocketChannel,并绑定到一个特定的端口。将 ServerSocketChannel 设置为非阻塞模式,并注册到一个 Selector 上,以监听连接事件。

2.客户端初始化

客户端创建一个 SocketChannel,连接到服务端的地址。将 SocketChannel 设置为非阻塞模式,并注册到一个 Selector 上,以监听连接事件。

3.事件循环

服务端和客户端都进入一个事件循环,不断地检查发生的事件。在服务端,可能会检查 OP_ACCEPT 事件,表示有新的连接请求。在客户端,可能会检查 OP_CONNECT 事件,表示连接已建立。(这里如果一直没有客户端访问程序陷入空转并不会阻塞,这样也就避免了内核切换)

4.处理连接事件

在服务端,当有新的连接请求到达时,通过 ServerSocketChannel.accept() 接受连接,并将新的 SocketChannel 注册到 Selector 上监听读事件。

5.客户端处理连接事件

在客户端,当连接建立完成时,通过 SocketChannel.finishConnect() 完成连接,然后注册到 Selector 上监听读事件

6.处理读事件

在服务端和客户端,当通道可读时,从通道中读取数据

这样,通过 NIO,服务端和客户端可以通过非阻塞的方式处理多个连接,并在一个事件循环中实现数据的读取和写入。在实际应用中,可能需要结合多线程、线程池等机制,以更好地处理多个连接的并发处理。

3. Buffer的工作方式

通过前面分析源码,我们也大致知道了Buffer的工作方式,Buffer可以简单理解为一组基本数据类型的元素列表,它通过介个变量来保持这个数据当前的位置状态,也就是有四个索引:

在这里插入图片描述

我们通过ByteBuffer.allocate(11)方法创建了一个11个byte的数组缓冲区,初始状态如下所示,position为0,capacity和limit都是数组默认长度。
在这里插入图片描述
当我们写入五个数据后位置如下所示。

在这里插入图片描述
调用flip方法,数组切换状态为读状态:
在这里插入图片描述
此时底层操作系统可以从缓存区中正确读取这5个字节数据并发送出去了,在下一次写数据之前我们再调用一下clear方法,缓冲区又回回到默认位置。mark标记就是记录当前position的前一个位置,我们调用reset时,position会恢复到mark位置。

通过Channel获取的I/O数据首先要经过操作系统的Socket缓冲区缓冲区再讲数据复制到Buffer中,从操作系统缓冲区到用户缓冲区比较消耗性能,Buffer提供了另外一种直接操作操作系统缓冲区的方法,即ByteBuffer.allocateDirector(size);,该方法返回与底层存储空间关联的缓冲区,它通过Native代码操作非JVM堆的内存空间,每次创建和释放的时候都会调用一次System.gc()。

4. NIO数据访问方式

我们知道当我们通过IO读取磁盘数据时,需要使用操作系统的系统调用方法,而这会涉及用户态到内核态的切换,操作系统需要把数据读取到内核态空间,然后将数据送到用户态,这是十分重的一个操作,NIO提供了两个优化方法:

  • FIleChannel.transferXXX

减少数据从内核到用户空间中的复制,数据直接在哪和空间中移动,下图首先是传统数据访问方式,然后是tansferXXX方式:

在这里插入图片描述
在这里插入图片描述

  • FileChannel.map

它将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时,直接操作这个文件数据,这种方式就直接跳过了数据从内核空间向用户空间复制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/215739.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mirrors and reflections for VR

专为虚拟现实而建,但也非常适合非虚拟现实桌面和移动项目 这是URP管道,从Unity2019.4.16一直测试到2023年。 完全工作场景预览,轻松修改着色器材质。着色器支持折射,可以制作很酷的效果。 镜子/反射可以互相反射,而不仅仅是2...想象一下一个电梯,3面镜子都互相反射,直到…

【PTA刷题】 求子串(代码+详解)

【PTA刷题】 求子串(代码详解) 题目 请编写函数&#xff0c;求子串。 函数原型 char* StrMid(char *dst, const char *src, int idx, int len);说明&#xff1a;函数取源串 src 下标 idx 处开始的 len 个字符&#xff0c;保存到目的串 dst 中&#xff0c;函数值为 dst。若 len…

算法-02-排序-冒泡插入选择排序

一般最经典的、最常用的&#xff1a;冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。那么我们如何分析一个"排序算法"呢&#xff1f; 1-分析排序算法要点 时间复杂度&#xff1a;具体是指最好情况、最坏情况、平均情况下的时间复杂…

现代雷达车载应用——第2章 汽车雷达系统原理 2.1节

经典著作&#xff0c;值得一读&#xff0c;英文原版下载链接【免费】ModernRadarforAutomotiveApplications资源-CSDN文库。 2.1 基本雷达功能 雷达系统通过天线或天线阵列向空间辐射电磁能量。辐射的电磁能量“照亮”周围的目标。“被照亮”的目标拦截一些辐射能量&#xff0…

如何搭建eureka-server

在Spring Cloud项目的pom文件中添加eureka-server的starter依赖坐标 <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 http://ma…

每日一道c语言

任务描述 题目描述:输入10个互不相同的整数并保存在数组中&#xff0c;找到该最大元素并删除它&#xff0c;输出删除后的数组 相关知识&#xff08;略&#xff09; 编程要求 请仔细阅读右侧代码&#xff0c;结合相关知识&#xff0c;在Begin-End区域内进行代码补充&#xf…

论文阅读《Learning Adaptive Dense Event Stereo from the Image Domain》

论文地址&#xff1a;https://openaccess.thecvf.com/content/CVPR2023/html/Cho_Learning_Adaptive_Dense_Event_Stereo_From_the_Image_Domain_CVPR_2023_paper.html 概述 事件相机在低光照条件下可以稳定工作&#xff0c;然而&#xff0c;基于事件相机的立体方法在域迁移时性…

【头歌系统数据库实验】实验9 SQL视图

目录 第1关&#xff1a;请为三建工程项目建立一个供应情况的视图V_SPQ&#xff0c;包括供应商代码(SNO)、零件代码(PNO)、供应数量(QTY) 第2关&#xff1a;从视图V_SPQ找出三建工程项目使用的各种零件代码及其数量 第3关&#xff1a;从视图V_SPQ找出供应商S1的供应情况 第4…

事业单位选岗技巧

事业单位选岗技巧 下面这些都是不需要笔试直接面试的岗位&#xff0c;一定不要被自己限制的条件所卡死了&#xff0c;一定要灵活&#xff0c;一定要放的开

C++STL库的 deque、stack、queue、list、set/multiset、map/multimap

deque 容器 Vector 容器是单向开口的连续内存空间&#xff0c; deque 则是一种双向开口的连续线性空 间。所谓的双向开口&#xff0c;意思是可以在头尾两端分别做元素的插入和删除操作&#xff0c;当然&#xff0c; vector 容器也可以在头尾两端插入元素&#xff0c;但是在其…

三防平板|手持终端PDA|8寸/10寸工业三防平板电脑主板方案定制

近年来&#xff0c;随着科技的快速发展&#xff0c;三防平板成为了各行各业中不可或缺的工具。三防平板采用IP67级别的防护设计&#xff0c;通过了多项测试标准&#xff0c;如国标和美标&#xff0c;具备防水、防摔、防尘、防撞、防震、防跌落以及防盐雾等多重防护功能。因此&a…

ARM:作业3

按键中断代码编写 代码: key_it.h #ifndef __KEY_IT_H__ #define __KEY_IT_H__#include "stm32mp1xx_gpio.h" #include "stm32mp1xx_exti.h" #include "stm32mp1xx_rcc.h" #include "stm32mp1xx_gic.h"void key1_it_config(); voi…

vxe-table 右键菜单+权限控制(v3)

1.menu-config 是用于配置右键菜单的属性。通过 menu-config 属性&#xff0c;定义右键菜单的内容、显示方式和样式。 通过 menu-config 属性配置了右键菜单&#xff0c;其中的 options 属性定义了右键菜单的选项。用户在表格中右键点击时&#xff0c;将会弹出包含这些选项的自…

练练手之“四环”“磁铁”(svg)

文本是闲暇之余练习svg的运用的产物&#xff0c;记录以备有需。 <svg xmlns"http://www.w3.org/2000/svg" viewBox"0 0 500 500" width"500px" height"500px"><path d"M150,100 A50,50 0 1,1 150,99.999" stroke&q…

【数据结构(九)】顺序存储二叉树(2)

文章目录 1. 相关概念2. 顺序存储二叉树的遍历 1. 相关概念 从数据存储来看&#xff0c;数组存储方式和树的存储方式可以相互转换&#xff0c;即数组可以转换成树&#xff0c;树也可以转换成数组&#xff0c;看右面的示意图。 转换原则:     1.上图的二叉树的结点&#xff…

【深度学习】注意力机制(五)

本文介绍一些注意力机制的实现&#xff0c;包括CSRA/Spatial Shift/Triplet Attention/Coordinate Attention/ACmix。 【深度学习】注意力机制&#xff08;一&#xff09; 【深度学习】注意力机制&#xff08;二&#xff09; 【深度学习】注意力机制&#xff08;三&#xff…

python 实现 AIGC 大模型中的概率论:生日问题的基本推导

在上一节中&#xff0c;我们对生日问题进行了严谨的阐述&#xff1a;假设屋子里面每个人的生日相互独立&#xff0c;而且等可能的出现在一年 365 天中的任何一天&#xff0c;试问我们需要多少人才能让某两个人的生日在同一天的概率超过 50%。 处理抽象逻辑问题的一个入手点就是…

centos 7.9 二进制部署 kubernetes v1.27.7

文章目录 1. 预备条件2. 基础配置2.1 配置root远程登录2.2 配置主机名2.3 安装 ansible2.4 配置互信2.5 配置hosts文件2.6 关闭防firewalld火墙2.7 关闭 selinux2.8 关闭交换分区swap2.9 修改内核参数2.10 安装iptables2.11 开启ipvs2.12 配置limits参数2.13 配置 yum2.14 配置…

css实现姓名两端对齐

1.1 效果 1.2 主要代码 text-align-last: justify; 1.3 html完整代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0&quo…

【LeetCode刷题-树】-- 116.填充每个节点的下一个右侧节点指针

116.填充每个节点的下一个右侧节点指针 方法&#xff1a;层次遍历 /* // Definition for a Node. class Node {public int val;public Node left;public Node right;public Node next;public Node() {}public Node(int _val) {val _val;}public Node(int _val, Node _left, N…