mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-20 22:17:09 +08:00
Update BIO,NIO,AIO summary.md
This commit is contained in:
parent
2ef10c8f01
commit
bdf8aa5bbf
@ -1,241 +1,347 @@
|
||||
熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也是你学习 Netty 的基础。
|
||||
|
||||
# static 关键字
|
||||
<!-- MarkdownTOC -->
|
||||
|
||||
## static 关键字主要有以下四种使用场景
|
||||
- [BIO,NIO,AIO 总结](#bionioaio-总结)
|
||||
- [1. BIO \(Blocking I/O\)](#1-bio-blocking-io)
|
||||
- [1.1 传统 BIO](#11-传统-bio)
|
||||
- [1.2 伪异步 IO](#12-伪异步-io)
|
||||
- [1.3 代码示例](#13-代码示例)
|
||||
- [1.4 总结](#14-总结)
|
||||
- [2. NIO \(New I/O\)](#2-nio-new-io)
|
||||
- [2.1 NIO 简介](#21-nio-简介)
|
||||
- [2.2 NIO的特性/NIO与IO区别](#22-nio的特性nio与io区别)
|
||||
- [1)Non-blocking IO(非阻塞IO)](#1non-blocking-io(非阻塞io))
|
||||
- [2)Buffer\(缓冲区\)](#2buffer缓冲区)
|
||||
- [3)Channel \(通道\)](#3channel-通道)
|
||||
- [4)Selectors\(选择器\)](#4selectors选择器)
|
||||
- [2.3 NIO 读数据和写数据方式](#23-nio-读数据和写数据方式)
|
||||
- [2.4 NIO核心组件简单介绍](#24-nio核心组件简单介绍)
|
||||
- [2.5 代码示例](#25-代码示例)
|
||||
- [AIO \(Asynchronous I/O\)](#aio-asynchronous-io)
|
||||
- [参考](#参考)
|
||||
|
||||
1. 修饰成员变量和成员方法
|
||||
2. 静态代码块
|
||||
3. 修饰类(只能修饰内部类)
|
||||
4. 静态导包(用来导入类中的静态资源,1.5之后的新特性)
|
||||
|
||||
### 修饰成员变量和成员方法(常用)
|
||||
|
||||
被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。
|
||||
|
||||
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
|
||||
|
||||
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
|
||||
# BIO,NIO,AIO 总结
|
||||
|
||||
调用格式:
|
||||
Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用Java的API就可以了。
|
||||
|
||||
- 类名.静态变量名
|
||||
- 类名.静态方法名()
|
||||
在讲 BIO,NIO,AIO 之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。
|
||||
|
||||
如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。
|
||||
**同步与异步**
|
||||
|
||||
测试方法:
|
||||
- **同步:** 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
|
||||
- **异步:** 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
|
||||
|
||||
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
|
||||
|
||||
**阻塞和非阻塞**
|
||||
|
||||
- **阻塞:** 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
|
||||
- **非阻塞:** 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
|
||||
|
||||
举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在哪里傻等着水开(**同步阻塞**)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(**同步非阻塞**)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(**异步非阻塞**)。
|
||||
|
||||
|
||||
## 1. BIO (Blocking I/O)
|
||||
|
||||
同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
|
||||
|
||||
### 1.1 传统 BIO
|
||||
|
||||
BIO通信(一请求一应答)模型图如下(图源网络,原出处不明):
|
||||
|
||||

|
||||
|
||||
采用 **BIO 通信模型** 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在`while(true)` 循环中服务端会调用 `accept()` 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。
|
||||
|
||||
如果要让 **BIO 通信模型** 能够同时处理多个客户端请求,就必须使用多线程(主要原因是`socket.accept()`、`socket.read()`、`socket.write()` 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 **一请求一应答通信模型** 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 **线程池机制** 改善,线程池还可以让线程的创建和回收成本相对较低。使用`FixedThreadPool` 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。
|
||||
|
||||
**我们再设想一下当客户端并发访问量增加后这种模型会出现什么问题?**
|
||||
|
||||
在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。
|
||||
|
||||
### 1.2 伪异步 IO
|
||||
|
||||
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
|
||||
|
||||
伪异步IO模型图(图源网络,原出处不明):
|
||||
|
||||

|
||||
|
||||
采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
|
||||
|
||||
伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层任然是同步阻塞的BIO模型,因此无法从根本上解决问题。
|
||||
|
||||
### 1.3 代码示例
|
||||
|
||||
下面代码中演示了BIO通信(一请求一应答)模型。我们会在客户端创建多个线程依次连接服务端并向其发送"当前时间+:hello world",服务端会为每个客户端线程创建一个线程来处理。代码示例出自闪电侠的博客,原地址如下:
|
||||
|
||||
[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a)
|
||||
|
||||
**客户端**
|
||||
|
||||
```java
|
||||
public class StaticBean {
|
||||
/**
|
||||
*
|
||||
* @author 闪电侠
|
||||
* @date 2018年10月14日
|
||||
* @Description:客户端
|
||||
*/
|
||||
public class IOClient {
|
||||
|
||||
String name;
|
||||
静态变量
|
||||
static int age;
|
||||
public static void main(String[] args) {
|
||||
// TODO 创建多个线程,模拟多个客户端连接服务端
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Socket socket = new Socket("127.0.0.1", 3333);
|
||||
while (true) {
|
||||
try {
|
||||
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
|
||||
Thread.sleep(2000);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}).start();
|
||||
|
||||
public StaticBean(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
静态方法
|
||||
static void SayHello() {
|
||||
System.out.println(Hello i am java);
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return StaticBean{ +
|
||||
name=' + name + ''' + age + age +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
```
|
||||
}
|
||||
|
||||
```java
|
||||
public class StaticDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
StaticBean staticBean = new StaticBean(1);
|
||||
StaticBean staticBean2 = new StaticBean(2);
|
||||
StaticBean staticBean3 = new StaticBean(3);
|
||||
StaticBean staticBean4 = new StaticBean(4);
|
||||
StaticBean.age = 33;
|
||||
StaticBean{name='1'age33} StaticBean{name='2'age33} StaticBean{name='3'age33} StaticBean{name='4'age33}
|
||||
System.out.println(staticBean+ +staticBean2+ +staticBean3+ +staticBean4);
|
||||
StaticBean.SayHello();Hello i am java
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 静态代码块
|
||||
|
||||
静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
|
||||
|
||||
静态代码块的格式是
|
||||
|
||||
```
|
||||
static {
|
||||
语句体;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
|
||||
|
||||

|
||||
|
||||
静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问.
|
||||
|
||||
|
||||
### 静态内部类
|
||||
|
||||
静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:
|
||||
|
||||
1. 它的创建是不需要依赖外围类的创建。
|
||||
2. 它不能使用任何外围类的非static成员变量和方法。
|
||||
|
||||
|
||||
Example(静态内部类实现单例模式)
|
||||
|
||||
```java
|
||||
public class Singleton {
|
||||
|
||||
声明为 private 避免调用默认构造方法创建对象
|
||||
private Singleton() {
|
||||
}
|
||||
|
||||
声明为 private 表明静态内部该类只能在该 Singleton 类中被访问
|
||||
private static class SingletonHolder {
|
||||
private static final Singleton INSTANCE = new Singleton();
|
||||
}
|
||||
|
||||
public static Singleton getUniqueInstance() {
|
||||
return SingletonHolder.INSTANCE;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance() `方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。
|
||||
|
||||
这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。
|
||||
|
||||
### 静态导包
|
||||
|
||||
格式为:import static
|
||||
|
||||
这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法
|
||||
|
||||
```java
|
||||
|
||||
|
||||
Math. --- 将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用
|
||||
如果只想导入单一某个静态方法,只需要将换成对应的方法名即可
|
||||
|
||||
import static java.lang.Math.;
|
||||
|
||||
换成import static java.lang.Math.max;具有一样的效果
|
||||
|
||||
public class Demo {
|
||||
public static void main(String[] args) {
|
||||
|
||||
int max = max(1,2);
|
||||
System.out.println(max);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 补充内容
|
||||
|
||||
### 静态方法与非静态方法
|
||||
|
||||
静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。
|
||||
|
||||
Example
|
||||
**服务端**
|
||||
|
||||
```java
|
||||
class Foo {
|
||||
int i;
|
||||
public Foo(int i) {
|
||||
this.i = i;
|
||||
}
|
||||
/**
|
||||
* @author 闪电侠
|
||||
* @date 2018年10月14日
|
||||
* @Description: 服务端
|
||||
*/
|
||||
public class IOServer {
|
||||
|
||||
public static String method1() {
|
||||
return An example string that doesn't depend on i (an instance variable);
|
||||
|
||||
}
|
||||
public static void main(String[] args) throws IOException {
|
||||
// TODO 服务端处理客户端连接请求
|
||||
ServerSocket serverSocket = new ServerSocket(3333);
|
||||
|
||||
public int method2() {
|
||||
return this.i + 1; Depends on i
|
||||
}
|
||||
// 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理
|
||||
new Thread(() -> {
|
||||
while (true) {
|
||||
try {
|
||||
// 阻塞方法获取新的连接
|
||||
Socket socket = serverSocket.accept();
|
||||
|
||||
}
|
||||
```
|
||||
你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:`Foo bar = new Foo(1);bar.method2();`
|
||||
// 每一个新的连接都创建一个线程,负责读取数据
|
||||
new Thread(() -> {
|
||||
try {
|
||||
int len;
|
||||
byte[] data = new byte[1024];
|
||||
InputStream inputStream = socket.getInputStream();
|
||||
// 按字节流方式读取数据
|
||||
while ((len = inputStream.read(data)) != -1) {
|
||||
System.out.println(new String(data, 0, len));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}).start();
|
||||
|
||||
总结:
|
||||
|
||||
- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
|
||||
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
|
||||
|
||||
### static{}静态代码块与{}非静态代码块(构造代码块)
|
||||
|
||||
相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。
|
||||
|
||||
不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。
|
||||
|
||||
一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays类,Character类,String类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的.
|
||||
|
||||
Example
|
||||
|
||||
```java
|
||||
public class Test {
|
||||
public Test() {
|
||||
System.out.print(默认构造方法!--);
|
||||
}
|
||||
|
||||
非静态代码块
|
||||
{
|
||||
System.out.print(非静态代码块!--);
|
||||
}
|
||||
静态代码块
|
||||
static {
|
||||
System.out.print(静态代码块!--);
|
||||
}
|
||||
|
||||
public static void test() {
|
||||
System.out.print(静态方法中的内容! --);
|
||||
{
|
||||
System.out.print(静态方法中的代码块!--);
|
||||
} catch (IOException e) {
|
||||
}
|
||||
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
}
|
||||
}).start();
|
||||
|
||||
Test test = new Test();
|
||||
Test.test();静态代码块!--静态方法中的内容! --静态方法中的代码块!--
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
当执行 `Test.test();` 时输出:
|
||||
### 1.4 总结
|
||||
|
||||
```
|
||||
静态代码块!--静态方法中的内容! --静态方法中的代码块!--
|
||||
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
|
||||
|
||||
|
||||
|
||||
## 2. NIO (New I/O)
|
||||
|
||||
### 2.1 NIO 简介
|
||||
|
||||
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
|
||||
|
||||
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
|
||||
|
||||
### 2.2 NIO的特性/NIO与IO区别
|
||||
|
||||
如果是在面试中回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。如果,你把这些都回答上了我觉得你对于 NIO 就有了更为深入一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。
|
||||
|
||||
#### 1)Non-blocking IO(非阻塞IO)
|
||||
|
||||
**IO流是阻塞的,NIO流是不阻塞的。**
|
||||
|
||||
Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
|
||||
|
||||
Java IO的各种流是阻塞的。这意味着,当一个线程调用 `read()` 或 `write()` 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
|
||||
|
||||
#### 2)Buffer(缓冲区)
|
||||
|
||||
**IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。**
|
||||
|
||||
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。
|
||||
|
||||
在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
|
||||
|
||||
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。
|
||||
|
||||
#### 3)Channel (通道)
|
||||
|
||||
NIO 通过Channel(通道) 进行读写。
|
||||
|
||||
通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
|
||||
|
||||
#### 4)Selectors(选择器)
|
||||
|
||||
NIO有选择器,而IO没有。
|
||||
|
||||
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
|
||||
|
||||

|
||||
|
||||
### 2.3 NIO 读数据和写数据方式
|
||||
通常来说NIO中的所有IO都是从 Channel(通道) 开始的。
|
||||
|
||||
- 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
|
||||
- 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。
|
||||
|
||||
数据读取和写入操作图示:
|
||||
|
||||

|
||||
|
||||
|
||||
### 2.4 NIO核心组件简单介绍
|
||||
|
||||
NIO 包含下面几个核心的组件:
|
||||
|
||||
- Channel(通道)
|
||||
- Buffer(缓冲区)
|
||||
- Selector(选择器)
|
||||
|
||||
整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。我们上面已经对这三个概念进行了基本的阐述,这里就不多做解释了。
|
||||
|
||||
### 2.5 代码示例
|
||||
|
||||
代码示例出自闪电侠的博客,原地址如下:
|
||||
|
||||
[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a)
|
||||
|
||||
客户端 IOClient.java 的代码不变,我们对服务端使用 NIO 进行改造。以下代码较多而且逻辑比较复杂,大家看看就好。
|
||||
|
||||
```java
|
||||
/**
|
||||
*
|
||||
* @author 闪电侠
|
||||
* @date 2019年2月21日
|
||||
* @Description: NIO 改造后的服务端
|
||||
*/
|
||||
public class NIOServer {
|
||||
public static void main(String[] args) throws IOException {
|
||||
// 1. serverSelector负责轮询是否有新的连接,服务端监测到新的连接之后,不再创建一个新的线程,
|
||||
// 而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等
|
||||
Selector serverSelector = Selector.open();
|
||||
// 2. clientSelector负责轮询连接是否有数据可读
|
||||
Selector clientSelector = Selector.open();
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// 对应IO编程中服务端启动
|
||||
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
|
||||
listenerChannel.socket().bind(new InetSocketAddress(3333));
|
||||
listenerChannel.configureBlocking(false);
|
||||
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
|
||||
|
||||
while (true) {
|
||||
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
|
||||
if (serverSelector.select(1) > 0) {
|
||||
Set<SelectionKey> set = serverSelector.selectedKeys();
|
||||
Iterator<SelectionKey> keyIterator = set.iterator();
|
||||
|
||||
while (keyIterator.hasNext()) {
|
||||
SelectionKey key = keyIterator.next();
|
||||
|
||||
if (key.isAcceptable()) {
|
||||
try {
|
||||
// (1)
|
||||
// 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
|
||||
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
|
||||
clientChannel.configureBlocking(false);
|
||||
clientChannel.register(clientSelector, SelectionKey.OP_READ);
|
||||
} finally {
|
||||
keyIterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}).start();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
while (true) {
|
||||
// (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
|
||||
if (clientSelector.select(1) > 0) {
|
||||
Set<SelectionKey> set = clientSelector.selectedKeys();
|
||||
Iterator<SelectionKey> keyIterator = set.iterator();
|
||||
|
||||
while (keyIterator.hasNext()) {
|
||||
SelectionKey key = keyIterator.next();
|
||||
|
||||
if (key.isReadable()) {
|
||||
try {
|
||||
SocketChannel clientChannel = (SocketChannel) key.channel();
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
|
||||
// (3) 面向 Buffer
|
||||
clientChannel.read(byteBuffer);
|
||||
byteBuffer.flip();
|
||||
System.out.println(
|
||||
Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
|
||||
} finally {
|
||||
keyIterator.remove();
|
||||
key.interestOps(SelectionKey.OP_READ);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}).start();
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当执行 `Test test = new Test();` 时输出:
|
||||
为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题:
|
||||
|
||||
```
|
||||
静态代码块!--非静态代码块!--默认构造方法!--
|
||||
```
|
||||
- JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%
|
||||
- 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug
|
||||
|
||||
Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。
|
||||
|
||||
非静态代码块与构造函数的区别是: 非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。
|
||||
### AIO (Asynchronous I/O)
|
||||
|
||||
### 参考
|
||||
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
|
||||
|
||||
- httpsblog.csdn.netchen13579867831articledetails78995480
|
||||
- httpwww.cnblogs.comchenssyp3388487.html
|
||||
- httpwww.cnblogs.comQian123p5713440.html
|
||||
AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 类型都是同步的,这一点可以从底层IO线程模型解释,推荐一篇文章:[《漫话:如何给女朋友解释什么是Linux的五种IO模型?》](https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41#wechat_redirect) )
|
||||
|
||||
查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,NIO 之前也尝试使用过,不过又放弃了。
|
||||
|
||||
## 参考
|
||||
|
||||
- 《Netty 权威指南》第二版
|
||||
- https://zhuanlan.zhihu.com/p/23488863 (美团技术团队)
|
Loading…
x
Reference in New Issue
Block a user