Java面试宝典-java基础08
- 71、BIO、NIO、AIO有哪些应用场景
- 72、简述一下BIO的编程流程
- 73、NIO的三大核心部分是什么?
- 74、NIO中buffer的四大属性是什么?
- 75、对比一下BIO和NIO?
- 76、FileChannel是做什么的?
- 77、简述一下Selector选择器
- 78、lambda表达式是什么,有哪些应用场景?
- 79、Java8 “::”是什么,有哪些应用场景?
- 80、Java 8中parallel是做什么用的?
71、BIO、NIO、AIO有哪些应用场景
Java中的BIO、NIO、AIO各有其特定的应用场景,主要取决于它们的特点和适用条件。
- BIO(Blocking I/O):适用于一些简单的、低频的、短连接的通信场景,例如HTTP请求。由于BIO是同步阻塞IO模型,它在高并发环境下性能较差,需要为每个连接创建一个线程,且线程切换开销较大。因此,BIO更适合连接数较少且不需要高并发处理的网络应用,如Web应用中的Servlet。
- NIO(Non-blocking I/O):适用于连接数较多、并发性要求较高的网络应用,如高性能的服务器应用、网关应用等。NIO提供了Channel、Selector、Buffer等新的抽象,可以构建多路复用的、同步非阻塞IO程序,同时提供了更接近操作系统底层高性能的数据操作方式。NIO的出现解决了服务器在同一时间能够处理多个客户端请求的问题,目前频繁出现在物联网、社交媒体实时通信等场景中。
- AIO(Asynchronous I/O):适用于连接数目多且连接比较长(重操作)的架构,如相册服务器等。AIO是异步非阻塞的IO操作方式,基于事件和回调机制实现。应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO方式充分调用OS参与并发操作,编程比较复杂,适用于需要高性能和高并发处理的场景。
综上所述,BIO、NIO、AIO各有其优势和适用场景,选择哪种模型取决于具体的应用需求和技术挑战。
72、简述一下BIO的编程流程
- 服务器端启动一个ServerSocket;
- 客户端启动Socket对服务器进行通 信,默认情况下服务器端需要对每 个客户 建立一个线程与之通讯;
- 客户端发出请求后, 先咨询服务器 是否有线程响应,如果没有则会等 待,或者被拒绝;
- 如果有响应,客户端线程会等待请 求结束后,在继续执行;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务器启动,等待连接...");
while (true) {
// 监听并接受客户端的连接请求
Socket socket = serverSocket.accept();
// 为每个客户端连接创建一个新线程
new Thread(() -> {
try {
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
System.out.println("接收到客户端消息:" + new String(buffer, 0, length));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
73、NIO的三大核心部分是什么?
Selector(选择器)、Channel(通道)、Buffer(缓冲区)。
- Selector 对应一个线程, 一个线程对应多个channel(连接);
- 该图反应了有三个channel 注册到 该selector //程序;
- 每个channel 都会对应一个Buffer;
- 程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念;
- Selector 会根据不同的事件,在各个通道上切换;
- Buffer 就是一个内存块 , 底层是有一个数组;
- 数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是 输出流, 不能双向,但是NIO的Buffer 是可以读也可以写, 需要flip 方法切换;
- channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统 通道就是双向的;
NIO是面向缓冲区,或者说面向块编程,数据读取到一个 它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就 增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求 的数量比HTTP1.1大了好几个数量级。
缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个 容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对 象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
74、NIO中buffer的四大属性是什么?
Java NIO中的Buffer有四大基本属性:capacity、limit、position和mark。
- Capacity(容量):表示Buffer能够容纳的最大数据量,这个值在Buffer创建时被设定并且不能更改。Capacity定义了Buffer的物理容量,即Buffer内部数组的大小,是Buffer的最大存储元素量。
- Limit(限制):表示Buffer的读写字节的终点,即从该索引开始(包含该索引),不能对之后的元素进行读写操作。Limit的值可以修改,它定义了Buffer中有效数据的范围。
- Position(位置):表示下一个要被读取或写入的元素索引。Position随着读写的进行而移动,用于追踪下一个操作的位置。
- Mark(标记):是一个可记忆的位置值,通过调用mark()方法可以设置mark为当前的position值,之后在任何时刻,通过调用reset()方法,可以让Position等于Mark,除非进行设置,否则Mark值是不存在的。Mark属性用于在读写过程中设置一个临时的位置标记,方便后续快速回到该位置。
这四个属性共同协作,使得Buffer能够在读写操作中灵活地管理数据,实现高效的数据处理和传输。
75、对比一下BIO和NIO?
Java中的BIO(Blocking I/O)和NIO(Non-blocking I/O)是两种处理I/O操作的方式。
- BIO (Blocking I/O):
同步阻塞I/O模式,数据的读取写入会阻塞当前线程直到操作完成。BIO适用于少数连接和低负载场景,因为一个线程只能处理一个连接。 - NIO (Non-blocking I/O):
同步非阻塞I/O模式,可以通过多路复用器Selector同时监听多个注册的通道,非阻塞地进行I/O操作。NIO适用于多数连接和高负载场景,因为可以使用较少的线程处理更多的连接。
对比点:
-
阻塞性:BIO阻塞,NIO非阻塞。
-
内存控制:NIO基于缓冲区和通道,内存控制更好。
-
线程模型:BIO每连接一个线程,NIO一个线程可处理多个连接。
-
选择器:NIO有选择器,可以同时监听多个通道上的事件。
代码示例对比:
BIO 示例(阻塞式):
try (Socket socket = new Socket("localhost", 8080)) {
// 阻塞式读取数据
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead = in.read(buffer);
} catch (IOException e) {
e.printStackTrace();
}
NIO 示例(非阻塞式):
try (Selector selector = Selector.open();
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080))) {
// 设置为非阻塞模式
socketChannel.configureBlocking(false);
// 注册到选择器
socketChannel.register(selector, SelectionKey.OP_READ);
while (true) {
// 非阻塞式选择通道
selector.select();
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
SelectionKey key = selectedKeys.next();
selectedKeys.remove();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 非阻塞式读取数据
channel.read(buffer);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
在实际应用中,你可以根据应用场景选择适合的I/O模式。如果是低负载、简单连接的场景,可以选择BIO;如果是高负载、大量连接的场景,推荐使用NIO或者更高级的AIO。
76、FileChannel是做什么的?
FileChannel主要用来对本地文件进行 IO 操作,常见的方法有:
- read,从通道读取数据并放到缓冲区中
- write,把缓冲区的数据写到通道中
- transferFrom,从目标通道 中复制数据到当前通道
- transferTo,把数据从当 前通道复制给目标通道
77、简述一下Selector选择器
- Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连 接,就会使用到Selector(选择器)。
- Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然 后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个 通道,也就是管理多个连接和请求。
- 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少 了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
- 避免了多线程之间的上下文切换导致的开销。
78、lambda表达式是什么,有哪些应用场景?
在Java 8中,Lambda表达式被引入作为一个重要新特性,使得Java能够进行函数式编程,并在并发性能上取得了实质性的进步。Lambda表达式简化了匿名内部类的形式,并达到了同样的效果,使得代码更加简洁。
应用场景:
- 集合操作:Lambda表达式可以与集合操作方法(如forEach、filter、map、reduce)结合使用,对集合中的元素进行遍历、筛选、映射、聚合等操作。
- 排序:Lambda表达式可以用于自定义的排序功能,通过传递不同的比较规则实现对集合中元素的排序。
- 线程编程:Lambda表达式可以简化线程编程中的代码,例如使用Lambda表达式创建Runnable对象,或使用Lambda表达式实现函数式接口来处理线程任务。
- GUI事件处理:Lambda表达式可用于简化GUI事件处理代码,如为按钮、菜单等组件注册事件监听器。
- 数据处理:对于大数据集的处理,如统计、过滤、转换等,Lambda表达式表现出色。其并行处理的能力可以提高数据处理的效率。
- Web开发:Lambda表达式可以简化Web开发中的重复性代码,例如通过Lambda表达式实现控制器、过滤器、拦截器等。
79、Java8 “::”是什么,有哪些应用场景?
在Java 8中,双冒号“::”是一个方法引用操作符,也被称为方法引用符。它用于引用类的方法,并返回一个函数接口(function interface)。这与lambda表达式有所不同,因为lambda表达式需要自定义一个lambda体,而“::”则直接引用一个已存在的方法。
“::”操作符在Java 8中有多种用法:
- 引用静态方法:使用“类名::静态方法名”的格式。例如,Integer::parseInt就是引用Integer类的parseInt静态方法。
- 引用对象方法:使用“实例对象::实例方法”的格式。例如,对于字符串的substring方法,可以写成String::substring。
- 引用构造方法:使用“类名::new”的格式。例如,User::new就是引用User类的构造方法。
Java8 “::”有哪些应用场景?
由于“::”操作符可以方便地引用类的方法,并返回函数接口,因此在需要传递函数作为参数或者需要简化代码的场景中非常有用。
例如,在集合操作、事件处理、替代策略模式、流式编程等场景中,都可以看到它的身影。此外,由于方法引用符可以替代lambda表达式,因此在需要减少代码冗余和提高可读性的地方,也可以考虑使用它。
80、Java 8中parallel是做什么用的?
Stream.parallel() 方法是 Java 8 中 Stream API 提供的一种并行处理方式。在处理大量数据或者耗时操作时,使用 Stream.parallel() 方法可以充分利用多核 CPU 的优势,提高程序的性能。
Stream.parallel() 方法是将串行流转化为并行流的方法。通过该方法可以将大量数据划分为多个子任务交由多个线程并行处理,最终将各个 子任务的计算结果合并得到最终结果。使用 Stream.parallel() 可以简化多线程编程,减少开发难度。需要注意的是,并行处理可能会引入线程安全等问题,需要根据具体情况进行选择。