您的位置:首页 > 编程语言 > Java开发

Java 新IO技术(NIO) JDK1.4

2016-10-30 16:24 369 查看


在JDK1.4以前,I/O输入输出处理,我们把它称为旧I/O处理,在JDK1.4开始,java提供了一系列改进的输入/输出新特性,这些功能被称为新I/O(NEW I/O),新添了许多用于处理输入/输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中的很多类以NIO为基础进行了改写,新添了满足新I/O的功能。


Java NIO和IO的主要区别

IONIO
面向流

面向缓冲

阻塞IO

非阻塞IO


选择器

一、面向流与面向缓冲

        Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。

        Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

二、阻塞与非阻塞

        Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

        Java NIO的非阻塞模式,如使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。一个单独的线程可以管理多个输入和输出通道(channel)。

三、选择器(Selectors)

        Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。


面向缓冲(Buffer)

在整个Java的心I/O中,所以操作都是以缓冲区进行的,使操作的性能大大提高。

在Buffer中存在一系列的状态变量,这状态变量随着写入或读取都可能会被概念,在缓冲区开元使用是三个值表示缓冲区的状态。
position:表示下个缓冲区读取或写入的操作指针,没向缓冲区中华写入数据的时候 此指针就会改变,指针永远放在写入的最后一个元素之后。即:如果写入了4个位置的数据,则posotion会指向第5个位置。
Limit:表示还有多少数据可以存储或读取,position<=limit
capacity:表示缓冲区的最大容量,limit<=capacity,此值在分配缓冲区时被设置。一般不改变。
示例代码(以IntBuffer为例)

1、通过
IntBuffer.allocate(int capacity),<strong style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">创建IntBuffer缓冲区</strong>

import java.nio.IntBuffer ;
public class IntBufferDemo01{
public static void main(String args[]){
IntBuffer buf = IntBuffer.allocate(10) ;	// 准备出10个大小的缓冲区
System.out.print("1、写入数据之前的position、limit和capacity:") ;
System.out.println("position = " + buf.position() + ",limit = " + buf.limit() + ",capacty = " + buf.capacity()) ;
int temp[] = {5,7,9} ;
4000
// 定义一个int数组
buf.put(3) ;	// 设置一个数据
buf.put(temp) ;	// 此时已经存放了四个记录
System.out.print("2、写入数据之后的position、limit和capacity:") ;
System.out.println("position = " + buf.position() + ",limit = " + buf.limit() + ",capacty = " + buf.capacity()) ;

buf.flip() ;	//向缓冲去写入数据后,在读取缓冲区写入的数据之前,需要重设缓冲区
// postion = 0 ,limit = 原本position
System.out.print("3、准备输出数据时的position、limit和capacity:") ;
System.out.println("position = " + buf.position() + ",limit = " + buf.limit() + ",capacty = " + buf.capacity()) ;
System.out.print("缓冲区中的内容:") ;
while(buf.hasRemaining()){
int x = buf.get() ;
System.out.print(x + "、") ;
}
}
}


2、通过buf.slice(),创建子缓冲区
import java.nio.IntBuffer ;
public class IntBufferDemo02{
public static void main(String args[]){
IntBuffer buf = IntBuffer.allocate(10) ;	// 准备出10个大小的缓冲区
IntBuffer sub = null ;	// 定义子缓冲区
for(int i=0;i<10;i++){
buf.put(2 * i + 1) ;	// 在主缓冲区中加入10个奇数
}
//创建子缓冲区之前,需要先设置好position和limit
// 需要通过slice() 创建子缓冲区
buf.position(2) ;
buf.limit(6) ;
sub = buf.slice() ;
for(int i=0;i<sub.capacity();i++){
int temp = sub.get(i) ;
sub.put(temp-1) ;
}

buf.flip() ;	// 重设缓冲区
buf.limit(buf.capacity()) ;
System.out.print("主缓冲区中的内容:") ;
while(buf.hasRemaining()){
int x = buf.get() ;
System.out.print(x + "、") ;
}
}
}


3、创建只读缓冲区
import java.nio.IntBuffer ;
public class IntBufferDemo03{
public static void main(String args[]){
IntBuffer buf = IntBuffer.allocate(10) ;	// 准备出10个大小的缓冲区
IntBuffer read = null ;	// 定义子缓冲区
for(int i=0;i<10;i++){
buf.put(2 * i + 1) ;	// 在主缓冲区中加入10个奇数
}
read = buf.asReadOnlyBuffer()  ;// 创建只读缓冲区

read.flip() ;	// 重设缓冲区
System.out.print("主缓冲区中的内容:") ;
while(read.hasRemaining()){
int x = read.get() ;
System.out.print(x + "、") ;
}
read.put(30) ;	// 修改,错误
}
}



通道(Channel)

1、基本概念


Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

Java NIO的通道类似流,但又有些不同:
既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
通道可以异步地读写。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。




Channel的实现

这些是Java NIO中最重要的通道的实现:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel

FileChannel 从文件中读写数据。

DatagramChannel 能通过UDP读写网络中的数据。

SocketChannel 能通过TCP读写网络中的数据。

ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

通过通道可以完成双向的输入和输出操作。在通道还有一种方式称为内存映射

几种读入的方式的比较

RandomAccessFile   较慢

FileInputStream     较慢

缓冲读取      速度较快

内存映射      速度最快


示例代码
import java.nio.ByteBuffer ;
import java.nio.channels.FileChannel ;
import java.io.File ;
import java.io.FileOutputStream ;
import java.io.FileInputStream ;
public class FileChannelDemo02{
public static void main(String args[]) throws Exception{
File file1 = new File("d:" + File.separator + "note.txt") ;
File file2 = new File("d:" + File.separator + "outnote.txt") ;
FileInputStream input = null ;
FileOutputStream output = null ;
output = new FileOutputStream(file2) ;
input = new FileInputStream(file1) ;
FileChannel fout = null ;	// 声明FileChannel对象
FileChannel fin = null ;	// 定义输入的通道
fout = output.getChannel() ;	// 得到输出的通道
fin = input.getChannel() ;	// 得到输入的通道
ByteBuffer buf = ByteBuffer.allocate(1024) ;

int temp = 0 ;
while((temp=fin.read(buf))!=-1){
buf.flip() ;
fout.write(buf) ;
buf.clear() ;	// 清空缓冲区,所有的状态变量的位置恢复到原点
}
fin.close() ;
fout.close() ;
input.close() ;
output.close() ;
}
}import java.nio.ByteBuffer ;
import java.nio.channels.FileChannel ;
import java.io.File ;
import java.io.FileOutputStream ;
import java.io.FileInputStream ;
public class FileChannelDemo02{
public static void main(String args[]) throws Exception{
File file1 = new File("d:" + File.separator + "note.txt") ;
File file2 = new File("d:" + File.separator + "outnote.txt") ;
FileInputStream input = null ;
FileOutputStream output = null ;
output = new FileOutputStream(file2) ;
input = new FileInputStream(file1) ;
FileChannel fout = null ;	// 声明FileChannel对象
FileChannel fin = null ;	// 定义输入的通道
fout = output.getChannel() ;	// 得到输出的通道
fin = input.getChannel() ;	// 得到输入的通道
ByteBuffer buf = ByteBuffer.allocate(1024) ;

int temp = 0 ;
while((temp=fin.read(buf))!=-1){
buf.flip() ;
fout.write(buf) ;
buf.clear() ;	// 清空缓冲区,所有的状态变量的位置恢复到原点
}
fin.close() ;
fout.close() ;
input.close() ;
output.close() ;
}
}

  2、内存映射文件

通过内存映射文件的方式,读取文件的内容,是所有方式中速度最快的。
通过File Channel的map()方法

(一)通道映射技术

 1) 其实就是一种快速读写技术,它将通道所连接的数据节点中的全部或部分数据直接映射到内存的一个Buffer中,而这个内存Buffer块就是节点数据的映像,你直接对这个Buffer进行修改会直接影响到节点数据,而这个Buffer也不是普通的Buffer,叫做MappedBuffer,即镜像Buffer,对该Buffer进行修改会直接影响到实际的节点(更新到节点);

    2) 由于是内存镜像,因此处理速度非常快!!

    3) map原型:MappedByteBuffer map(MapMode mode, long position, long size);  // 将节点中从position开始的size个字节映射到返回的MappedByteBuffer中

    4) mode印出来映射的三种模式,在这三种模式下得到的将是三种不同的MappedByteBuffer:三种模式都是Channel的内部类MapMode中定义的静态常量,这里以FileChannel举例

        i. FileChannel.MapMode.READ_ONLY:得到的镜像只能读不能写(只能使用get之类的读取Buffer中的内容);

          ii. FileChannel.MapMode.READ_WRITE:得到的镜像可读可写(既然可写了必然可读),对其写会直接更改到存储节点;

          iii. FileChannel.MapMode.PRIVATE:得到一个私有的镜像,其实就是一个(position, size)区域的副本罢了,也是可读可写,只不过写不会影响到存储节点,就是一个普通的ByteBuffer了!!

    5) 映射的规矩:

          i. 使用InputStream获得的Channel可以映射,使用map时只能指定为READ_ONLY模式,不能指定为READ_WRITE和PRIVATE,否则会抛出运行时异常!

          ii. 使用OutputStream得到的Channel不可以映射!!并且OutputStream的Channel也只能write不能read!

          iii. 只有RandomAccessFile获取的Channel才能开启任意的这三种模式!

    6) MappedByteBuffer的用法和普通的ByteBuffer一样,只不过它的功能被上述的映射规则所限制了,比如只读的就只能get不能put,可写的以及私有的put、get都能用;

    7) 示例:只读映射

public class Test {

public static void main(String[] args) throws IOException {
File f = new File("out.txt");
try (
FileChannel fcin = new FileInputStream(f).getChannel();
FileChannel fcout = new FileOutputStream("a.txt").getChannel();
) {
MappedByteBuffer mbb = fcin.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
Charset cn = Charset.forName("GBK");
System.out.println(cn.decode(mbb));

mbb.flip(); // 写出去之前要操作就绪
fcout.write(mbb);
}
}
}


(二)RandomAccessFile的通道映射可读可写

1) RandomAccessFile也有getChannel方法获取相应的NIO通道;

    2) 其实NIO通道映射技术最大的受益者就是RandomAccessFile了,因为RandomAccessFile本身就是可读可写I/O通吃,如果RandomAccessFile的节点文件映射到内存中,直接对内存镜像修改来更新文件节点那岂不是效率大大提高吗?

    3) RandomAccessFile获取的通道同样是FileChannel,因此其使用上和之前讲过任何一个普通的Channel没有区别,映射map也是理所当然的一样;

    4) 只不过其Channel在map时只读、读写、私有三种模式都可以使用,不像InputStream和OutputStream那样有种种约束;

!!只不过读写模式下,对内存镜像做出的修改会直接更新到节点文件!!

!!这里插播一个Channel常用的方法:long position(); // 获取当前操作到节点文件的哪个位置

    5) 示例:将文件第一个字符改成“牛”,然后将改动好的整个文件内容复制一遍再追加到文件末尾!

public class Test {

public static void main(String[] args) throws IOException {
File f = new File("out.txt");
try (
FileChannel fc = new RandomAccessFile(f, "rw").getChannel();
) {
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, f.length());
mbb.put("牛".getBytes()); // 注意是字节,一定要转换成字节才行,这里对镜像的修改直接生效到节点文件中了!

fc.position(f.length()); // 定位到文件末尾
mbb.clear(); // 注意!一定要是clear,将limit定位到capacity!如果是flip则当前limit才在第二个字符位置!
fc.write(mbb);
}
}
}



选择器(Selectors)

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。


为什么使用Selector?

仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。

但是,需要记住,现代的操作系统和CPU在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个CPU有多个内核,不使用多任务可能是在浪费CPU能力。不管怎么说,关于那种设计的讨论应该放在另一篇不同的文章中。在这里,只要知道使用Selector能够处理多个通道就足够了。


下面是单线程使用一个 Selector 处理 3 个 channel 的示例图:




要点

使用Selector可以构建一个非阻塞的网络服务。

在新IO实现网络程序需要依靠ServerSocketChannel类与SocketChannel


Selector实例

下面使用Selector完成一个简单的服务器的操作,服务器可以同时在多个端口进行监听,此服务器的主要功能是返回当前时间。
import java.net.InetSocketAddress ;
import java.net.ServerSocket ;
import java.util.Set ;
import java.util.Iterator ;
import java.util.Date ;
import java.nio.channels.ServerSocketChannel ;
import java.nio.ByteBuffer ;
import java.nio.channels.SocketChannel ;
import java.nio.channels.Selector  ;
import java.nio.channels.SelectionKey  ;
public class DateServer{
public static void main(String args[]) throws Exception {
int ports[] = {8000,8001,8002,8003,8005,8006} ; // 表示五个监听端口
Selector selector = Selector.open() ;	// Selector的创建
for(int i=0;i<ports.length;i++){
ServerSocketChannel initSer = null ;
initSer = ServerSocketChannel.open() ;	// 打开服务器的通道
initSer.configureBlocking(false) ;	// 服务器配置为非阻塞
ServerSocket initSock = initSer.socket() ;
InetSocketAddress address = null ;
address = new InetSocketAddress(ports[i]) ;	// 实例化绑定地址
initSock.bind(address) ;	// 进行服务的绑定
initSer.register(selector,SelectionKey.OP_ACCEPT) ;	// 等待连接
System.out.println("服务器运行,在" + ports[i] + "端口监听。") ;
}
// 要接收全部生成的key,并通过连接进行判断是否获取客户端的输出
int keysAdd = 0 ;
while((keysAdd=selector.select())>0){	// 选择一组键,并且相应的通道已经准备就绪
Set<SelectionKey> selectedKeys = selector.selectedKeys() ;// 取出全部生成的key
Iterator<SelectionKey> iter = selectedKeys.iterator() ;
while(iter.hasNext()){
SelectionKey key = iter.next() ;	// 取出每一个key
if(key.isAcceptable()){
ServerSocketChannel server = (ServerSocketChannel)key.channel() ;
SocketChannel client = server.accept() ;	// 接收新连接
client.configureBlocking(false) ;// 配置为非阻塞
ByteBuffer outBuf = ByteBuffer.allocateDirect(1024) ;	//
outBuf.put(("当前的时间为:" + new Date()).getBytes()) ;	// 向缓冲区中设置内容
outBuf.flip() ;
client.write(outBuf) ;	// 输出内容
client.close() ;	// 关闭
}
}
selectedKeys.clear() ;	// 清楚全部的key
}

}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息