Java I/O 网络

对于网络I/O通信,其实站在应用程序的角度,就是面向Socket进行读写的过程,至于如何建立连接,数据包如何传输等问题则由TCP/IP以及更底层的协议保障,这些通常由操作系统和底层硬件负责实现,它们的目的就是希望能简化应用程序的开发。

在Linux系统中,一切皆文件,所以Socket也统一抽象为一个文件描述符,那么对应用程序来讲,收发消息就是对一个文件描述符进行读写。但是它与读写普通的本地磁盘文件也有区别,由于网络的延迟和不可靠性,在读Socket的过程中可能会有大量的时间花在等待数据包的到达上,因此如何提高应用程序在读写Socket过程中的响应性,就成了网络通信编程中一个很重要的问题。

针对网络I/O,Linux提供了五种IO模型,具体可以参考笔记

不过Java并没有全部支持,而是逐步选择支持了三种模型:
在JDK1.4之前,Java的IO模型只支持阻塞式IO(Blocking IO),简称为BIO;
在JDK1.4时,Java支持了I/O多路复用模型, 简称NIO,即新IO模型,不过现在JDK1.8早已成为主流版本,已经没什么新鲜了,所以更多的人愿意将它理解为非阻塞I/O,即None-Blocking IO;
在JDK1.7时,Java对NIO包进行了升级,支持异步I/O(Asynchronous IO),简称为AIO,因为是对nio包的升级,所有也称为NIO2.0;

Java I/O 文件

Java在操作系统的基础上对I/O操作提供了更高级的Api封装,可以对这些Api进行一些粗略的分类,比如从数据格式的角度可以分为基于字节操作的InputStreamOutputStream,以及基于字符操作的ReaderWriter,从数据传输方式的角度可以分为基于磁盘操作的File和基于网络操作的Socket,其实在Java中,I/O主要考虑的就是以怎样的方式将怎样格式的数据进行传输的问题,然后从这两个方向考虑如何提高I/O操作的效率

由于I/O类库有很多类,很难对每个都介绍到位,这里只是简单从文件操作的场景对一些常用的Api做一些梳理,尽量从中去体会Java语言对于I/O操作支持和封装的思路,另外,如果想对I/O有更好的理解,最好先有如下准备知识:

NIO Channel & Selector

Channel 与 Stream 是一个级别,只不过 Stream 是单向的,比如InputStreamOutputStream,而 Channel 可以是单向的,也可以是双向的,既可以同时支持读和写操作,具体由其实现的接口所决定

java.nio.channels
1
2
3
4
5
6
7
public interface ReadableByteChannel extends Channel {
public int read(ByteBuffer dst) throws IOException;
}

public interface WritableByteChannel extends Channel{
public int write(ByteBuffer src) throws IOException;
}

可以看到 read 和 write 方法接受的都是一个 ByteBuffer 参数,其中Channel.read是向ByteBuffer中put数据,然后应用从ByteBuffer中获取,而Channel.write是从ByteBuffer中get数据,然后发送给其他远程主机。两者均返回字节数,然后缓冲区的position位置也会前移对应的字节数,如果只进行了部分传输,缓冲区可以被重新提交给通道并从上次中断的地方继续传输,该过程可以重复进行直到缓冲区的hasRemaining()方法返回false

NIO Buffer

缓冲区 Buffer 是一个固定容量的数组,其作用是一个作为存储器,或者分段运输区,用于存储和检索数据。

JUC AbstractQueuedSynchronizer

队列同步器 AbstractQueuedSynchronizer(AQS),是用来构建锁或者其他同步组件的基础框架。它使用一个int变量来表示同步状态,通过CAS操作对同步状态进行修改,确保状态的改变是安全的。通过内置的FIFO(First In First Out)队列来完成资源获取线程的排队工作。

AQS同步与synchronized同步是采用的两种不同的机制:

synchronized在编译后,会在同步块的前后分别添加monitorenter和monitorexit两个字节码指令,这两个指令需要关联一个监视对象,当线程执行monitorenter指令时,需要首先获得获得监视对象的锁,即进入同步块的凭证,才能进入同步块,当线程离开同步块时,执行monitorexit指令,释放对象锁。

AQS同步中,使用一个int类型的变量state来表示当前同步块的状态。以独占式同步为例,state的有效值为0和1,其中0表示当前同步块中没有线程,1表示同步块中已经有线程在执行。当线程要进入同步块时,需要首先判断state的值是否为0,假设为0,会尝试将state修改为1,只有修改成功了之后,线程才可以进入同步块,并通过CAS操作确保同一时刻只有一个线程操作成功。当线程离开同步块时,会修改state的值为0,并唤醒等待的线程。所以在AQS同步中,线程获得锁实际上是指线程成功修改了状态变量state,而释放锁就是是指线程将状态变量置为了可修改的状态,以便其他线程可以再次尝试修改状态变量

JUC FutureTask

前面整理线程池时说过Executor是执行任务的抽象,而Runnable是任务的抽象。不过,线程池在实际提交任务时,会先将任务包装成一个FutureTask,以实现Future的语义,即异步获取任务结果的能力FutureTask内部实际依赖的是一个Callable,并将任务的执行过程和结果获取委托给它,如果提交的是Runnable,那么也可以通过适配器RunnableAdapter<V>将其适配成一个Callable

java.util.concurrent.Executors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}

static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}

相关类图可以如下所示: