前言
Java中的输入输出操作主要针对数据文件和Socket对象,传统的Java IO操作都是阻塞进行的,比如在读取网络数据的时候如果数据还没有返回那么read方法就会被阻塞一直等到网络数据返回或者发生错误,再读取的时候也采用流式读取数据。在Java1.4中引入了NIO处理输入输出,NIO采用内存映射文件的方式将文件或文件的一段映射到内存中,同时它使用块IO来处理数据效率更高。
缓冲区
缓冲区对象其实就是容器对象,在原来的IO操作中所有的数据读写都是直接通过Stream对象来完成,在NIO中读取数据需要放到缓冲区中,写入数据也需要放到缓冲区中。Java中所有的基础类型都有对应的缓冲区对象,ByteBuffer、DoubleBuffer、IntBuffer等。
缓冲区本质上来说是数组对象,它有三个重要的属性position、limit和capacity,它们的值有(0 <= position <= limit <= capacity)的大小关系。其中position作为操作的开始位置,不管是读取get操作还是写入put操作,position都会比自动更新;limit表明操作position时最多能够操作到的位置,capacity代表当前的缓冲区大小值。
public static void main(String[] args) {
IntBuffer intBuffer = IntBuffer.allocate(10);
for (int i = 0; i < intBuffer.capacity() - 3; i++) {
intBuffer.put(i * 5);
}
intBuffer.flip();
while (intBuffer.hasRemaining()) {
System.out.print(intBuffer.get() + " ");
}
}
// 0 5 10 15 20 25 30
上面的示例中IntBuffer由于是抽象类无法被实例化,就使用IntBuffer提供的静态方法初始化生成子类HeapIntBuffer对象,初始化它的容量为10个Int值,接下来在intBuffer对象中防止7个整数值,这时position=7,limit=10,后面通过flip调用将position=0,limit=7,hasRemaining方法不断比较position和limit属性大小,get方法会增加position大小,当position=limit代表数据完全读取完毕。
缓冲区的数据作为数组存放在JVM堆内存中,不过不同的JVM中数组对象的储存格式有所不同,为此引入了直接缓冲区,直接缓冲区位于JVM堆内存之外,它的请求和释放不参与GC,直接缓冲区使用的内存是通过调用本地操作系统的代码分配的,绕过了标准JVM堆栈。不过通过下面的Demo测试发现分配的内存被算到了堆内存容量中。
public static void main(String[] args) {
// 直接分配10M内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
System.out.println("Allocate Successfully!");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
java -Xms5m -Xmx5m NewIOTest
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:693)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at NewIOTest.main(NewIOTest.java:5)
java -Xms15m -Xmx15m NewIOTest
Allocate Successfully!
通道
通道类似旧IO操作中的Stream对象,不过通道对象能够读也能够写,通道只能读取缓冲区中的数据获取将数据写入到缓冲区中。通道对象代表的是数据文件或者Socket等与程序之间的连接通路,因而所有的Channel都不是通过构造器创建的,而是通过传统的节点InputStream、OutputStream的getChannel方法来返回响应的Channel。
FileInputStream fis = new FileInputStream("Main.java");
FileChannel fileChannel = fis.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
fileChannel.read(byteBuffer);
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
选择子
Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。这里通过实现一个简单的服务器只需要每次将用户输入的内容添加“Hello”之后返回到客户端。
public static void main(String[] args) throws Exception {
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 设置Socket是非阻塞的
socketChannel.configureBlocking(false);
// 绑定到5555端口
socketChannel.bind(new InetSocketAddress(5555));
Selector selector = Selector.open();
// 为服务器注册接收事件
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int count = selector.select();
if (count <= 0) continue;
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
Iterator<SelectionKey> iterable = selectionKeySet.iterator();
while (iterable.hasNext()) {
SelectionKey key = iterable.next();
if (key.isAcceptable()) {
// 如果accept被激活,说明有客户端连接
SocketChannel newSocketChannel = socketChannel.accept();
newSocketChannel.configureBlocking(false);
// 为客户端连接添注册读事件
newSocketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 如果读到客户端的数据
SocketChannel clientSocket = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
clientSocket.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
String text = "Hello: " + new String(byteBuffer.array()) + "\r\n";
// 将Hello + 客户端数据返回给用户
clientSocket.write(ByteBuffer.wrap(text.getBytes()));
}
iterable.remove();
}
}
}