1.阻塞I/O模型
阻塞I/O模型是常见的I/O模型,在读写数据时客户端会发生阻塞。
阻塞I/O模型的工作流程为:在用户线程发出I/O请求之后,内核会检查数据是否就绪,此时用户线程一直阻塞等待内存数据就绪;在内存数据就绪后,内核将数据复制到用户线程中,并返回I/O执行结果到用户线程,此时用户线程将解除阻塞状态并开始处理数据。
典型的阻塞I/O模型的例子为data = socket.read(),如果内核数据没有就绪,Socket线程就会一直阻塞在read()中等待内核数据就绪。
2.非阻塞I/O模型
非阻塞I/O模型指用户线程在发起一个I/O操作后,无须阻塞便可以马上得到内核返回的一个结果。
如果内核返回的结果为false,则表示内核数据还没准备好,需要稍后再发起I/O操作。一旦内核中的数据准备好了,并且再次收到用户线程的请求,内核就会立刻将数据复制到用户线程中并将复制的结果通知用户线程。
在非阻塞I/O模型中,用户线程需要不断询问内核数据是否就绪,在内存数据还未就绪时,用户线程可以处理其他任务,在内核数据就绪后可立即获取数据并进行相应的操作。
while(true){
data = socket.read();
if(date==true){//内核数据就绪
//处理并获取内核数据
break;
}else{//内核数据未就绪,用户线程处理其它任务
}
}
3.多路复用I/O模型
多路复用I/O模型是多线程并发编程用得较多的模型,Java NIO就是基于多路复用I/O模型实现的。在多路复用I/O模型中会有一个被称为Selector的线程不断轮询多个Socket的状态,只有在Socket有读写事件时,才会通知用户线程进行I/O读写操作。
因为在多路复用I/O模型中只需一个线程就可以管理多个Socket(阻塞I/O模型和非阻塞 1/O模型需要为每个Socket都建立一个单独的线程处理该Socket上的数据),并且在真正有Socket读写事件时才会使用操作系统的I/O资源,大大节约了系统资源。
Java NIO在用户的每个线程中都通过selector.select()查询当前通道是否有事件到达,如果没有,则用户线程会一直阻塞。而多路复用I/O模型通过一个线程管理多个Socket通道,在Socket有读写事件触发时才会通知用户线程进行I/O读写操作。因此,多路复用I/O模型在连接数众多且消息体不大的情况下有很大的优势。尤其在物联网领域比如车载设备实时位置、智能家电状态等定时上报状态且字节数较少的情况下优势更加明显,一般一个经过优化后的16核32GB服务器能承载约10万台设备连接。
非阻塞I/O模型在每个用户线程中都进行Socket状态检查,而在多路复用I/O模型中是在系统内核中进行Socket状态检查的,这也是多路复用I/O模型比非阻塞I/O模型效率高的原因。
多路复用I/O模型通过在一个Selector线程上以轮询方式检测在多个Socket上是否有事件到达,并逐个进行事件处理和响应。因此,对于多路复用I/O模型来说,在事件响应体(消息体)很大时,Selector线程就会成为性能瓶颈,导致后续的事件迟迟得不到处理,影响下一轮的事件轮询。在实际应用中,在多路复用方法体内一般不建议做复杂逻辑运算,只做数据的接收和转发,将具体的业务操作转发给后面的业务线程处理。
4.信号驱动I/O模型
在信号驱动I/O模型中,在用户线程发起一个I/O请求操作时,系统会为该请求对应的Socket注册一个信号函数,然后用户线程可以继续执行其他业务逻辑;在内核数据就绪时,系统会发送一个信号到用户线程,用户线程在接收到该信号后,会在信号函数中调用对应的I/O读写操作完成实际的I/O请求操作。
5.异步I/O模型
在异步I/O模型中,用户线程会发起一个asynchronous read操作到内核,内核在接收到synchronous read请求后会立刻返回一个状态,来说明请求是否成功发起,在此过程中用户线程不会发生任何阻塞。接着,内核会等待数据准备完成并将数据复制到用户线程中,在数据复制完成后内核会发送一个信号到用户线程,通知用户线程asynchronous读操作已完成。在异步I/O模型中,用户线程不需要关心整个I/O操作是如何进行的,只需发起一个请求,在接收到内核返回的成功或失败信号时说明I/O操作已经完成,直接使用数据即可。
在异步I/O模型中,I/O操作的两个阶段(请求的发起、数据的读取)都是在内核中自动完成的,最终发送一个信号告知用户线程I/O操作已经完成,用户直接使用内存写好的数据即可,不需要再次调用I/O函数进行具体的读写操作,因此在整个过程中用户线程不会发生阻塞。
在信号驱动模型中,用户线程接收到信号便表示数据已经就绪,需要用户线程调用I/O函数进行实际的I/O读写操作,将数据读取到用户线程;而在异步I/O模型中,用户线程接收到信号便表示I/O操作已经完成(数据已经被复制到用户线程),用户可以开始使用该数据了。
异步I/O需要操作系统的底层支持,在Java 7中提供了AsynchronousI/O操作。
6.JavaI/O
在整个Java.io包中最重要的是5个类和1个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader,1个接口指的是Serializable。具体的使用方法请参考JDK API。
7.JavaNIO
Java NIO的实现主要涉及三大核心内容:Selector(选择器)、Channel(通道)和Buffer(缓冲区)。Selector用于监听多个Channel的事件,比如连接打开或数据到达,因此,一个线程可以实现对多个数据Channel的管理。传统I/O基于数据流进行I/O读写操作;而Java NIO基于Channel和Buffer进行I/O读写操作,并且数据总是被从Channel读取到Buffer中,或者从Buffer写入Channel中。
8.Java NIO和传统I/O的区别
(1)I/O是面向流的,NIO是面向缓冲区的:在面向流的操作中,数据只能在一个流中连续进行读写,数据没有缓冲,因此字节流无法前后移动。而在NIO中每次都是将数据从一个Channel读取到一个Buffer中,再从Buffer写入Channel中,因此可以方便地在缓冲区中进行数据的前后移动等操作。该功能在应用层主要用于数据的粘包、拆包等操作,在网络不可靠的环境下尤为重要。
(2)传统I/O的流操作是阻塞模式的,NIO的流操作是非阻塞模式的。在传统I/O下,用户线程在调用read()或write()进行I/O读写操作时,该线程将一直被阻塞,直到数据被读取或数据完全写入。NIO通过Selector监听Channel上事件的变化,在Channel上有数据发生变化时通知该线程进行读写操作。对于读请求而言,在通道上有可用的数据时,线程将进行Buffer的读操作,在没有数据时,线程可以执行其他业务逻辑操作。对于写操作而言,在使用一个线程执行写操作将一些数据写入某通道时,只需将Channel上的数据异步写入Buffer即可,Buffer上的数据会被异步写入目标Channel上,用户线程不需要等待整个数据完全被写入目标Channel就可以继续执行其他业务逻辑。
非阻塞I/O模型中的Selector线程通常将I/O的空闲时间用于执行其他通道上的I/O操作,所以一个Selector线程可以管理多个输入和输出通道。
9.JavaNIO Selector
Selector用于检测在多个注册的Channel上是否有I/O事件发生,并对检测到的I/O事件进行相应的响应和处理。因此通过一个Selector线程就可以实现对多个Channel的管理,不必为每个连接都创建一个线程,避免线程资源的浪费和多线程之间的上下文切换导致的开销。同时,Selector只有在Channel上有读写事件发生时,才会调用I/O函数进行读写操作,可极大减少系统开销,提高系统的并发量。
10.JavaNIO Channel
Channel和I/O中的Stream(流)类似,只不过Stream是单向的(例如InputStream、OutputStream),而Channel是双向的,既可以用来进行读操作,也可以用来进行写操作。
NIO中Channel的主要实现有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel,分别对应文件的I/O、UDP、TCPI/O、Socket Client和Socker Server操作。
11.JavaNIO Buffer
Buffer实际上是一个容器,其内部通过一个连续的字节数组存储I/O上的数据。在NIO中,Channel在文件、网络上对数据的读取或写入都必须经过Buffer。
客户端在向服务端发送数据时,必须先将数据写入Buffer中,然后将Buffer中的数据写到服务端对应的Channel上。服务端在接收数据时必须通过Channel将数据读入Buffer中,然后从Buffer中读取数据并处理。
在NIO中,Buffer是一个抽象类,对不同的数据类型实现不同的Buffer操作。常用的Buffer实现类有:ByteBuffer、IntBuffer、CharBuffer、LongBuffer、DoubleBuffer、FloatBuffer、ShortBuffer。
12.序列化
Java对象在JVM运行时被创建、更新和销毁,当JVM退出时,对象也会随之销毁,即这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,我们常常需要将对象及其状态在多个应用之间传递、共享,或者将对象及其状态持久化,在其他地方重新读取被保存的对象及其状态继续进行处理。这就需要通过将Java对象序列化来实现。
在使用Java序列化技术保存对象及其状态信息时,对象及其状态信息会被保存在一组字节数组中,在需要时再将这些字节数组反序列化为对象。注意,对象序列化保存的是对象的状态,即它的成员变量,因此类中的静态变量不会被序列化。
对象序列化除了用于持久化对象,在RPC(远程过程调用)或者网络传输中也经常被使用。
13.Java序列化API的使用
Java序列化API为处理对象序列化提供了一个标准机制,具体的Java系列化需要注意以下事项。
- 类要实现序列化功能,只需实现java.io.Serializable接口即可。
- 序列化和反序列化必须保持序列化的ID 一致,一般使用privatestatic final long serialVersionUID定义序列化ID。
- 序列化并不保存静态变量。
- 在需要序列化父类变量时,父类也需要实现Serializable接口。
- 使用Transient关键字可以阻止该变量被序列化,在被反序列化后,transient变量的值被设为对应类型的初始值,例如,int类型变量的值是 0,对象类型变量的值是null。
public class Wroker implements Serializable{
//定义序列化id
private static final long serialVersionUID=123456789L;
//name属性将被初始化
private String name;
//transient修饰的变量不会被初始化
private transient int salary;
//静态变量属于类信息,不属于对象的状态,因此不会被序列化
static int age=100;
}
以上代码通过implements Serializable实现了一个序列化的类。注意,transient修饰的属性和static修饰的静态属性不会被序列化。
对象通过序列化后在网络上传输时,基于网络安全,我们可以在序列化前将一些敏感字段(用户名、密码、身份证号码)使用秘钥进行加密,在反序列化后再基于秘钥对数据进行解密。这样即使数据在网络中被劫持,由于缺少秘钥也无法对数据进行解析,这样可以在一定程度上保证序列化对象的数据安全。
14.序列化和反序列化
在Java生态中有很多优秀的序列化框架,比如arvo、protobuf、thrift、fastjson。我们也可以基于JDK原生的ObjectOutputStream和ObjectInputStream类实现对象进行序列化及反序列化,并调用其writeObject和readObject方法实现自定义序列化策略。
//序列化数据
FileOutputStream fos=new FileOutputStream("work");
ObjectOutputStream oos=new ObjectOutputStream(fos);
Wroker wroker=new Wroker();
wroker.setName("alax");
oos.writeObject(wroker);
//反序列化数据
FileInputStream fis=new FileInputStream("work");
ObjectInputStream ois=new ObjectInputStream(fis);
Wroker wroker=(Wroker)ois.readObject();
wroker.getName();