Java NIO使用和总结
0.前言
NIO即New IO,是在java io机制的基础上增加的内容。这篇主要学习和使用它的用法, 主要的学习来自并发编程网http://ifeve.com/java-nio-all/
1.Channel(通道)
Channel其实就是对流进行了改进,使得既可以读也可以写,而一般意义上的流通常是单向的,这就是Channel的产生。下面看一下它的用法:
/**1.通道基本用法
* channel类型
* FileChannel 文件中读写数据
* DatagramChannel UDP中读写数据
* SocketChannel TCP中读写数据
* ServerSocketChannel 监听TCP连接信息
*/
public static void main(String[] args) throws Exception {
/*文件输入流*/
FileInputStream filInputStream = new FileInputStream(new File("channel1.txt"));
/*从中获取通道*/
FileChannel channel1 = filInputStream.getChannel();
/*分配48字节capacity的ByteBuffer*/
ByteBuffer ByteBuffer = ByteBuffer.allocate(6);
/*写模式初始*/
// System.out.println("Channel1.main()position:"+ByteBuffer.position());
// System.out.println("Channel1.main()limit:"+ByteBuffer.limit());
// System.out.println("Channel1.main()capacity:"+ByteBuffer.capacity());
/*把数据从channel中读取到ByteBuffer*/
int ByteRead = channel1.read(ByteBuffer);
while(-1 != ByteRead){
/*写模式结束*/
// System.out.println("Channel1.main()position:"+ByteBuffer.position());
// System.out.println("Channel1.main()limit:"+ByteBuffer.limit());
// System.out.println("Channel1.main()capacity:"+ByteBuffer.capacity());
System.out.println("Channel1-ByteRead-wri:"+ByteRead);
/*反转buffer,其实就是从写模式切换到读模式,后面详述*/
ByteBuffer.flip();
/*读模式开始*/
// System.out.println("Channel1.main()position:"+ByteBuffer.position());
// System.out.println("Channel1.main()limit:"+ByteBuffer.limit());
// System.out.println("Channel1.main()capacity:"+ByteBuffer.capacity());
while(ByteBuffer.hasRemaining()){
System.out.println("Channel1-ByteRead-get:"+ByteBuffer.get());
}
/*读模式结束*/
// System.out.println("Channel1.main()position:"+ByteBuffer.position());
// System.out.println("Channel1.main()limit:"+ByteBuffer.limit());
// System.out.println("Channel1.main()capacity:"+ByteBuffer.capacity());
ByteBuffer.clear();
ByteRead = channel1.read(ByteBuffer);
}
}
2.Buffer
之前我们对流进行操作时,通常会用一个byte或者一个byte数组来接受,但是我们需要更多的功能,比如记录读到哪一个位置,这个时候操作数组很难满足,所以就有了Buffer,下面看一下它的用法:
/**2.buffer基本用法
* Buffer的capacity,position和limit
* capacity:不管是读模式,还是写模式,都代表缓冲区的长度
* limit:
* 在写模式下,limit其实就是capacity
* 在读模式下,limit代表已经写入元素的数量
* position:
* 在写模式下,从零开始不管增加,最大值是capacity,position的值就代表已经写入的数据数量
* 在读模式下,也是从零开始,按照写入的顺序把数据读取出来,最大值是limit,position的值就代表已经读取的数据数量
*/
上图的箭头方法分别表示数据的写入和读取的方向。写模式下,position从上往下,读模式下,position也是从上往下。 实际上就是position原本指向3,变成读模式后,就指向1了,如何测试buffer的位置,以及limit和capacity,只要把上面代码的注释去掉即可。 另外buffer还有一些其他的方法:
/**@Description
* clear()和compact方法、
* clear():如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值
* compact():compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。
* limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
*/
/**@Description
* 此外,还有其他一些方法
* mark(),rewind(),reset(),equals()和compareTo()方法
*/
3.Scatter和Gather的思想
/**3.Scatter/Gather(分散/聚集)
* Scattering Reads是指数据从一个channel读取到多个buffer中
* Gathering Writes是指数据从多个buffer写入到同一个channel
*/
看一下这种思想的用法:
public static void main(String[] args) throws Exception {
/*文件输入流*/
FileInputStream filInputStream = new FileInputStream(new File("channel1.txt"));
filInputStream.available();
/*从中获取通道*/
FileChannel channel1 = filInputStream.getChannel();
/*分配48字节capacity的ByteBuffer*/
ByteBuffer bufferHeader = ByteBuffer.allocate(8);
ByteBuffer bufferBody = ByteBuffer.allocate(8);
ByteBuffer[] bufferArray = {bufferHeader,bufferBody};
/*把数据从channel中读取到ByteBuffer*/
channel1.read(bufferArray);
System.out.println("bufferHeader:"+bufferHeader);
System.out.println("bufferBody:"+bufferBody);
bufferHeader.flip();
while(bufferHeader.hasRemaining()){
System.out.println("Channel1.main():"+bufferHeader.get());
}
ByteBuffer bufferHeader2 = ByteBuffer.allocate(8);
ByteBuffer bufferBody2 = ByteBuffer.allocate(8);
bufferHeader2.put("a".getBytes());
bufferBody2.put("b".getBytes());
/*注意切换成读模式,否则channel2中得不到正确的信息*/
bufferHeader2.flip();
bufferBody2.flip();
System.out.println("bufferHeader2:"+bufferHeader2);
System.out.println("bufferBody2:"+bufferBody2);
ByteBuffer[] bufferArray2 = {bufferHeader2,bufferBody2};
/*文件输入流*/
FileOutputStream fileOutputStream = new FileOutputStream(new File("channel2.txt"));
/*从中获取通道*/
FileChannel channel2 = fileOutputStream.getChannel();
channel2.write(bufferArray2);
}
4.通道之间的数据传输
/**4.通道之间数据传输
* 在Java NIO中,如果两个通道中有一个是FileChannel,那你可以直接将数据
* 从一个channel(译者注:channel中文常译作通道)传输到另外一个channel。
* */
看一下它们的用法:
public static void main(String[] args) throws IOException {
/*测试transferFrom方法*/
RandomAccessFile fromFile = new RandomAccessFile("channel1.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("channel2.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position,count);
System.out.println("Channel1.main()");
}
public static void main(String[] args) throws IOException {
/*测试transferTo方法*/
RandomAccessFile fromFile = new RandomAccessFile("channel1.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("channel2.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
System.out.println("Channel1.main()");
}
5.关于几种流传输速度的比较
传统的流在数据传输方面,可以设置自定义的数组大小,因为一个一个字节的传输肯定是最慢的;下面的测试数据大小为60M
1)数组大小为1024(通常应用中设置的值)时,传输测试:
public static void main(String[] args) throws IOException {
/*1.传统的输入输出流,一次读取1024字节*/
FileInputStream fileInputStream1 = new FileInputStream(new File("E:\\Majintao\\software1.rar"));
FileOutputStream fileOutputStream1 = new FileOutputStream(new File("E:\\Majintao\\software2.rar"));
Date dateFirst1 = new Date();
int readByte1 = 0;
byte[] byte1 = new byte[1024];
while(-1 != (readByte1 = fileInputStream1.read(byte1))){
fileOutputStream1.write(byte1,0,readByte1);
}
fileInputStream1.close();
fileOutputStream1.close();
Date dateLast1 = new Date();
Long timeDue1 = dateLast1.getTime()-dateFirst1.getTime();
System.out.println("传统流传输时间:"+timeDue1);
}
传统流传输时间:794
2)缓冲流,实际上是对流增加了一层缓冲,传输测试:
/*2.使用缓冲流来传输*/
FileInputStream fileInputStream2 = new FileInputStream(new File("E:\\Majintao\\software1.rar"));
FileOutputStream fileOutputStream2 = new FileOutputStream(new File("E:\\Majintao\\software3.rar"));
BufferedInputStream bufferedInputStream1 = new BufferedInputStream(fileInputStream2);
BufferedOutputStream bufferedOutputStream1 = new BufferedOutputStream(fileOutputStream2);
Date dateFirst2 = new Date();
int readByte2 = 0;
byte[] byte2 = new byte[1024];
while(-1 != (readByte2 = bufferedInputStream1.read(byte2))){
bufferedOutputStream1.write(byte2,0,readByte2);
}
bufferedInputStream1.close();
bufferedOutputStream1.close();
fileInputStream2.close();
fileOutputStream2.close();
Date dateLast2 = new Date();
Long timeDue2 = dateLast2.getTime()-dateFirst2.getTime();
System.out.println("缓冲流传输时间:"+timeDue2);
缓冲流传输时间:189
3)字符流,其实字符流本不应该参与这次的数据比较,因为它的优势是处理字符文件,这里还是把它拿出来测试,传输测试:
/*3.使用字符流*/
FileReader fileReader = new FileReader("E:\\Majintao\\software1.rar");
FileWriter fileWriter = new FileWriter("E:\\Majintao\\software4.rar");
Date dateFirst3 = new Date();
char[] char1 = new char[1024];
int readByte3 = 0;
while(-1 != (readByte3 = fileReader.read(char1))){
fileWriter.write(char1);
}
fileReader.close();
fileWriter.close();
Date dateLast3 = new Date();
Long timeDue3 = dateLast3.getTime()-dateFirst3.getTime();
System.out.println("字符流流传输时间:"+timeDue3);
字符流流传输时间:4516
4)自定义接受数组的大小,在使用传统流时,自定义最合适的数组大小,传输测试:
/*4.在上面1的例子上自定义缓冲数组的大小,调整为最合适的大小*/
FileInputStream fileInputStream4 = new FileInputStream(new File("E:\\Majintao\\software1.rar"));
FileOutputStream fileOutputStream4 = new FileOutputStream(new File("E:\\Majintao\\software5.rar"));
Date dateFirst4 = new Date();
int readByte4 = 0;
byte[] byte4 = new byte[204800];
while(-1 != (readByte4 = fileInputStream4.read(byte4))){
fileOutputStream4.write(byte4,0,readByte4);
}
fileInputStream4.close();
fileOutputStream4.close();
Date dateLast4 = new Date();
Long timeDue4 = dateLast4.getTime()-dateFirst4.getTime();
System.out.println("传统流传输时间(自定义缓冲数组大小):"+timeDue4);
传统流传输时间(自定义缓冲数组大小):77
5)使用filechannel的transferTo或者transferFrom方法(缺点是只能传输2G以下的数据),传输测试:
/*5.使用fileChannel*/
FileInputStream fileInputStream5 = new FileInputStream(new File("E:\\Majintao\\software1.rar"));
FileOutputStream fileOutputStream5 = new FileOutputStream(new File("E:\\Majintao\\software6.rar"));
FileChannel inFileChannel = fileInputStream5.getChannel();
FileChannel outFileChannel = fileOutputStream5.getChannel();
Date dateFirst5 = new Date();
inFileChannel.transferTo(0,inFileChannel.size(),outFileChannel);
fileInputStream5.close();
fileOutputStream5.close();
inFileChannel.close();
outFileChannel.close();
Date dateLast5 = new Date();
Long timeDue5 = dateLast5.getTime()-dateFirst5.getTime();
System.out.println("filechannel传输:"+timeDue5);
filechannel传输:52
可以看到速度比自定义最合适的数组还要快
6)使用filechannel+buffer,传输测试: /*6.使用fileChannel 通过buffer */
FileInputStream fileInputStream6 = new FileInputStream(new File("E:\\Majintao\\software1.rar"));
FileOutputStream fileOutputStream6 = new FileOutputStream(new File("E:\\Majintao\\software7.rar"));
Date dateFirst6 = new Date();
FileChannel inFileChannel6 = fileInputStream6.getChannel();
FileChannel outFileChannel6 = fileOutputStream6.getChannel();
int readByte6 = 0;
ByteBuffer buffer = ByteBuffer.allocate(409600);
while(-1 != (readByte6 = inFileChannel6.read(buffer))){
buffer.flip();
outFileChannel6.write(buffer);
buffer.clear();
}
fileInputStream6.close();
fileOutputStream6.close();
inFileChannel6.close();
outFileChannel6.close();
Date dateLast6 = new Date();
Long timeDue6 = dateLast6.getTime()-dateFirst6.getTime();
System.out.println("filechannel--buffer传输:"+timeDue6);
filechannel--buffer传输:79
可以看到速度等同或者说略慢于自定义最合适数组
PS:来对几种传输做一个比较,同时还会引出两个问题:
/**
*在速度的比较上
*最快的是transferFrom和transferTo方法(示例5)
*然后是channel-buffer-channel(示例6)和自定义byte(示例4)
*接着是缓冲流(示例2)
*接着是(示例1),通常我们没有调整缓冲数组大小,直接设置为1024;
*接着是字符流(示例3)
*下面有两个问题
*1)为什么channel会比较快
*因为底层是C操作
*2)在自定义缓冲数组时(示例4),如何设置最合适的大小?为什么我们通常设置为1024
*这个问题暂时不明白,后面研究时并且补充上;这里提供一个网上的答案:
*每次文件读写是以簇为单位,每簇都要消耗时间,如果byte数大于一簇,肯定要多花时间。 不过现在电脑最小的簇也是4K,你的这两个对象没有差别。
*如果你的io流是针对网络,那么就不是簇,而是一个包的大小。有些包的载量比1K小,可能会有些差别。
*/
6.同步(阻塞)与异步(非阻塞)
/**6.NIO中--------------关于同步(阻塞),异步(非阻塞),在数据读取和进程推进这两个方面的应用场景
*由于非阻塞,所以线程可以做其他事情,但是这个读写需要监听,你当然可以设置一段时间就尝试读取一次,
*但是现在系统中提供了监听的方法,selector; 并且由于理论上可以设置监听多个,因为多个channel同时
*变为可读写的几率并不高,这样在一个线程中相比在多个线程中监听多个channel相比,减少了线程上下文
*切换的开销。 select与非阻塞channel搭配更好,因为在监听到多个channel就绪时,在处理过程中,可能在
*其中一个channel中阻塞,所以这也说明了NIO适合用在"多而小"的情况下,"多"指的是连接数据多,"小"指的
*是一次传输的数据量小。
*与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,
*因为FileChannel不能切换到非阻塞模式,而套接字通道都可以,意思就是Selector适用于网络编程,后面再讲
*/
7.channel方法拾遗
/**7.channel方法拾遗
* close()方法
* position()方法,获得(设置)当前位置
* size()方法 返回所关联文件的大小
* truncate()方法 截取一个文件
*/
8.Pipe 管道
/**8.pipe()操作,用于在线程间数据传输
* 多线程管道 是单向的
* sinkChannel 数据写入其中
* sourceChannel 从中读取数据
* 设置完成sinkChannel和sourceChannel之后开启多线程的操作
*/
使用示例如下,其中channel1.txt内容为:12345678
public static void main(String[] args) throws IOException {
Pipe pipe = Pipe.open();
SinkChannel sinkChannel = pipe.sink();
Pipe.SourceChannel sourceChannel = pipe.source();
Thread thread1 = new Thread(new Runnable() {
public void run() {
/*文件输入流*/
try {
FileInputStream filInputStream = null;
filInputStream = new FileInputStream(new File("channel1.txt"));
/*从中获取通道*/
FileChannel channel1 = filInputStream.getChannel();
ByteBuffer buf = ByteBuffer.allocate(8);
buf.clear();
channel1.read(buf);
buf.flip();
while(buf.hasRemaining()) {
sinkChannel.write(buf);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Channel1.main(...).new Runnable() {...}.run()");
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
ByteBuffer buf = ByteBuffer.allocate(8);
try {
int bytesRead = sourceChannel.read(buf);
buf.flip();
while(buf.hasRemaining()){
System.out.println("Channel1.main():"+buf.get());
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
结果如下:
Channel1.main(...).new Runnable() {...}.run()
Channel1.main():49
Channel1.main():50
Channel1.main():51
Channel1.main():52
Channel1.main():53
Channel1.main():54
Channel1.main():55
Channel1.main():56
打印的结果就是12345678的ASCII码9.NIO和IO总结
/**9.NIO相对IO的特点
* IO NIO
* 面向流 面向缓冲(Buffer)
* 阻塞IO 非阻塞IO(Channel)
* 无 选择器(Selector)
*
* buffer的设计不仅仅是对原有的缓冲的封装,同时还支持了非阻塞IO(channel),为什么这么说:
* 每次读写操作都先把数据放到Buffer里面,然后多次调用Channel的写方法对数据进
* 行操作,依靠对Buffer的大小来判断数据的完整性,如果是读方法,则根据read的返回值int判断
*/
总结:学习和使用了channel的基本用法, 主要是注意它的一些特点,从而合适的应用场景中想到它,不必详记它的语法~ IO的内容有很多,从来没有系统学习过,后面准备复习和总结下IO的用法。