NIO第一部分缓冲区和通道

       系统运行的性能瓶颈通常在IO读写,包括对端口和文件的操作上,在打 开一个IO通道后,read()将一直等待在端口一边读取字节内容,如果没有内容进来,read()一直等待下去,改进做法就是开设线程,让线程去等待,但是这样做相当耗费资源(传统socket通讯服务器设计模式)。
       在 Java 编程中,所有 IO 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。流 IO 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。
       从JDK 1.4引入了NIO(new io),NIO 的创建目的是为了让 Java 程序员可以实现高速 IO 而无需编写自定义的本机代码,并提供了异步操作的API(IO提供的则是同步操作的API)。NIO 将最耗时的 IO 操作(即填充和提取缓冲区)转移回操作系统,以块的方式处理数据,因而可以极大地提高速度。
      Java NIO非堵塞技术实际是采取Reactor模式,或者说是Observer模式为我们监察IO端口,如果有内容进来,会自动通知我们,这样,就不必开启多个线程死等,从外界看,实现了流畅的IO读写,不堵塞了。 
      在JDK 1.4中原来的 IO 包和 NIO 已经很好地集成了。 java.io.* 已经以NIO为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如, java.io.* 包中的一些类包含以块的形式读写数据的方法,这使得即使在更面向流的系统中,处理速度也会更快。

     Sun 官方标榜的特性如下:
           – 为所有的原始类型提供 (Buffer) 缓存支持。
           – 字符集编码解码解决方案。
           – Channel :一个新的原始 IO 抽象。
           – 支持锁和内存映射文件的文件访问接口。
           – 提供多路(MultiplexiIIg)、非阻塞式(non-bloking)的高伸缩性网络 IO 。
  
      Buffer是一块连续的内存块,是NIO数据读或写的中转地。在面向流的 IO 中,您将数据直接写入或者将数据直接读到 Stream 对象中。 在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。
 一个Buffer对象是固定数量的数据的容器。概念上,缓冲区是包在一个对象内的基本数据元素数组。

package java.nio; 
public abstract class Buffer { 
	public final int capacity()
		//储存在缓冲区中的最大数据容量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
	public final int position()
	public final Buffer position (int position)	
		//缓冲区的位置,缓冲区中已经写了多少数据或从缓冲区中获取了多少数据,下一个字节放到数组的哪一个元素中或来自数组的哪一个元素
	public final int limit()
	public final Buffer limit (int newLimit)
		//缓冲区的限制。缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数
	public final Buffer mark()
		//缓冲区的位置设置标记
	public final Buffer reset()
	public final Buffer clear()
		//不会改变缓冲区的数据,而只是简单的重置了缓冲区的主要索引值。(1). 它将limit设置为与capacity相同。(2). 它设置position为0。
	public final Buffer flip()
		
	public final Buffer rewind()
	public final int remaining()
	public final boolean hasRemaining()
	public abstract boolean isReadOnly()
}

     说明: 
           这些函数将引用返回到它们在(this)上被引用的对象。这是一个允许级联调用的类设计方法。级联调用允许这种类型的代码:

buffer.mark( );
buffer.position(5); 
buffer.reset( ); 
被简写为:
buffer.mark().position(5).reset(); 

      java.nio中的类被特意地设计为支持级联调用(java.lang.StringBuffer类也使用了这种级联调用)。

      例如:测试Buffer的方法 

public class TestByteBufferOne {
	public static void main(String[] args) throws Exception {
		ByteBuffer buffer = ByteBuffer.allocate(128);
		showLocationInfo(buffer);	//0-128-128
		buffer.put((byte)'l');
		buffer.put((byte)'c');
		buffer.put((byte)'r');
		showLocationInfo(buffer);	//3-128-128
		printContent(buffer);		//lcr
		buffer.flip();
		buffer.put((byte)'a');
		buffer.put((byte)'b');
		buffer.put((byte)'c');
		showLocationInfo(buffer);	//3-3-128
		printContent(buffer);		//abc
		buffer.put((byte)'d');		//java.nio.BufferOverflowException
		buffer.clear();
		showLocationInfo(buffer);	//0-128-128
	}
	private static void showLocationInfo (ByteBuffer buffer) {
		System.out.println(buffer.position() + "-" + buffer.limit() + "-" + buffer.capacity()); 
	}
	private static void printContent (ByteBuffer buffer) {
		int len = buffer.position();
		for (int i = 0 ; i < len ; i++) {
			System.out.print((char)buffer.get(i));
		}
		System.out.println();
	}
	/**
	 * flip的作用就是将position位置设为0,limit位置设置为原来position的位置值。
	 * 由于原来有三个元素,position为3则执行flip后limit为3,再put时就超过了limit范围,报错
	 */
}

       最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。
       因为大多数标准IO操作都使用 ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作: 

1.ByteBuffer 类中有四个 get() 方法:
	1). byte get();
	2). ByteBuffer get( byte dst[] );
	3). ByteBuffer get( byte dst[], int offset, int length );
	4). byte get( int index );
2.ByteBuffer 类中有五个 put() 方法:
	1). ByteBuffer put( byte b );
	2). ByteBuffer put( byte src[] );
	3). ByteBuffer put( byte src[], int offset, int length );
	4). ByteBuffer put( ByteBuffer src );
	5). ByteBuffer put( int index, byte b );
3.ByteBuffer 还有用于读写不同类型的值的其他方法,如下所示:
	* getByte()
	//......
	* putByte()
	//......
4.缓冲区分配和包装
	ByteBuffer buffer = ByteBuffer.allocate( 1024 );
	allocate()方法分配一个新的字节缓冲区。还可将一个现有的数组转换为缓冲区
	byte array[] = new byte[1024];
	ByteBuffer buffer = ByteBuffer.wrap(array);
5.缓冲区分片
	buffer.position( 3 );
	buffer.limit( 7 );	//缓冲区为从3到6
	ByteBuffer slice = buffer.slice();
	注意:片段和缓冲区共享同一个底层数据数组,对片段数据所作的修改也将反应到缓冲区中
6.只读缓冲区
	可以通过调用缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。 
7.将文件映射到内存
	MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024 );
	将文件的前1024个字节映射到内存中。

       例如:接受用户输入,并将其放到缓冲区中,在结束输入后将刚才输入的数据以String的形式输出 

public class TestByteBufferTwo {
	public static void main(String[] args) throws Exception {
		int c ;
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		//创建一个容量为1024字节的ByteBuffer,如果发现创建的缓冲区容量太小,唯一的选择就是重新创建一个大小合适的缓冲区.
		while ((c = System.in.read()) != '0') {
			if (c == '\n' || c == '\r') {
				continue;
			}
			System.out.println((char)c);
			buffer.put((byte)c);
		}
		System.out.println((char)c);
		buffer.flip();
		//下面两句等价于buffer.array(),但是输出完正确值后会有空白框的乱码,如下方式则没有
		byte [] bytes = new byte[buffer.limit()];
		buffer.get(bytes);
		String str = new String(bytes);
		System.out.println("str = " + str);
		buffer.clear();
	}
}

       ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型: 

* ByteBuffer
	* MappedByteBuffer
* CharBuffer
* ShortBuffer
* IntBuffer
* LongBuffer
* FloatBuffer
* DoubleBuffer	

      每一个Buffer类都是Buffer接口的一个实例。除了ByteBuffer,每一个Buffer类都有完全一样的操作,只是它们所处理的数据类型不一样。 

 

      Channel是一个对象,与流的不同之处在于通道是双向的,可以通过它读取和写入数据(一个流必须是 InputStream 或者 OutputStream 的子类)。 

public abstract class FileChannel
	extends AbstractInterruptibleChannel
		implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
public abstract class SocketChannel
	extends AbstractSelectableChannel
		implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
public abstract class DatagramChannel
	extends AbstractSelectableChannel
		implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
public abstract class ServerSocketChannel extends AbstractSelectableChannel

       用于读取、写入、映射和操作文件的通道,多个并发线程可安全地使用文件通道 
       java.io 包的每个 FileInputStream、FileOutputStream 和 RandomAccessFile 类添加了 getChannel 方法获得文件通道FileChannel。
             通过 FileInputStream 实例的 getChannel 方法所获得的通道将允许进行读取操作。
             通过 FileOutputStream 实例的 getChannel 方法所获得的通道将允许进行写入操作。
             如果使用模式 "r" 创建 RandomAccessFile 实例,则通过该实例的 getChannel 方法所获得的通道将允许进行读取操作。
             如果使用模式 "rw" 创建实例,则获得的通道将允许进行读取和写入操作。

      java.nio.channels.FileLock  表示文件区域锁定的标记。
      JDK 1.4引入了文件加锁机制,允许我们同步访问一个共享文件。不过,竞争同一文件的两个线程有可能在不同的java虚拟机上,或者一个是java线程,另一个是操作系统中其他的某个线程,但文件锁对其他线程或其他操作系统进程都是可见的,因为java的文件加锁直接映射到了本地操作系统的加锁机制。
      注意:这里的锁是指锁定其他应用程序,而不是锁定同一虚拟机里访问的同一文件的其他线程 。如果在同一虚拟机两次锁定同一文件或某文件里的同一区域,tryLock()与lock()则会抛出OverlappingFileLockException异常。
      每次通过 FileChannel 类的 lock 或 tryLock 方法获取文件上的锁定时,就会创建一个文件锁定对象。tryLock() 是非阻塞的,它会试着去获取这个锁,但是如果得不到(其它进程已经以独占方式得到这个锁了),那它就直接返回;而lock( )是阻塞的,如果得不到锁,它会在一直处于阻塞状态,除非它得到了锁,或者你打断了调用它的线程,或者关闭了它要lock()的channel,否则它是不会返回的。最后用FileLock.release()释放锁。要想获取整个文件的锁,可以用FileChannel的tryLock( )或lock()方法。

      还可以像这样锁住文件的某一部分 tryLock(long position, long size, boolean shared)  或者 lock(long position, long size, boolean shared),其中第三个参数表示是否是共享锁。锁是独占的还是共享的,这要由操作系统来决定。如果操作系统不支持共享锁,而程序又申请了一个共享锁,那么它会返回一个独占锁。你可以用FileLock.isShared( )来查询锁的类型(共享还是独占)。
      另外锁定写文件通道new FileOutputStream(path).getChannel().tryLock();时,它会清掉原文件中的内容,所以当文件中有内容时最好使用 new FileOutputStream(path, true).getChannel().tryLock(); 以追加方式打开写文件通道。或者使用RandomAccessFile类来创建文件通道然后锁定 new RandomAccessFile(path, "rw").getChannel().tryLock();这样它不会破坏锁定的文件的内容。
      SocketChannel,DatagramChannel,以及 ServerSocketChannel是不需要锁的,因为它们是从单进程实体继承而来;一般来说,你是不会让两个进程去共享一个网络socket的。

      如果使用原来的 IO,那么我们只需创建一个 FileInputStream 并从它那里读取。而在 NIO 中,情况稍有不同:我们首先从 FileInputStream 获取一个 FileInputStream 对象,然后使用这个通道来读取数据。
      在 NIO 系统中,任何时候执行一个读操作,您都是从通道中读取,但是您不是 直接从通道读取。因为所有数据最终都驻留在缓冲区中,所以您是从通道读到缓冲区中。

      因此读取文件涉及三个步骤:(1) 从 FileInputStream 获取 Channel,(2) 创建 Buffer,(3) 将数据从 Channel 读到 Buffer 中。

FileInputStream fis = new FileInputStream(path);
FileChannel fc = fis.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);//分配一个新的字节缓冲区。
fc.read(buffer);	

    写入文件 

FileOutputStream fout = new FileOutputStream(path);
FileChannel fc = fout.getChannel();
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i=0; i<message.length; ++i) {
	 buffer.put( message[i] );
}
buffer.flip();
fc.write(buffer);

     也可以通过程序读写缓冲区,这个程序不断重复 ― 读、写[读、写][......] ― 直到源文件结束。  

public class TestFileCopy {
	public static void main(String[] args) throws IOException {
		System.out.println((srcFile.length() / (1024 * 1024L)) + "M");	/计算文件大小
//		方法一、
		FileInputStream fis = new FileInputStream(srcFile);
		FileChannel srcChannel =  fis.getChannel();
		FileOutputStream fos = new FileOutputStream(distFile);
		FileChannel distChannel = fos.getChannel();
		distChannel.transferFrom(srcChannel, 0, srcChannel.size());
		//srcChannel.transferTo(distChannel, 0, srcChannel.size());
		distChannel.close();
		srcChannel.close();
//		方法二、
		srcFile.renameTo(new File(distFile));
//		方法三、
		FileInputStream fis = new FileInputStream(srcFile);
		FileOutputStream fos = new FileOutputStream(distFile);
		byte [] bytes = new byte[1024];
		int len = 0;
		while ((len = fis.read(bytes, 0, bytes.length))!= -1) {
			fos.write(bytes, 0, len);
		}
		fos.flush();
		fos.close();
		fis.close();
//		方法四、
		FileInputStream fis = new FileInputStream(srcFile);
		FileOutputStream fos = new FileOutputStream(distFile);
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		FileChannel finc = fis.getChannel();
		FileChannel fouc = fos.getChannel();
		buffer.clear();
		while (finc.read(buffer) != -1) {
			buffer.flip();
			fouc.write(buffer);
			buffer.compact();	//以防写入不完整
		}
		fouc.force(true);	//防止缓存
		fos.flush();
		fos.close();
		finc.close();
		fis.close();
	}
}

    例如:通过在主线程中将file加锁,然后启动一个新的线程模拟对文件的读操作,在锁释放之前读操作无法进行 

public class TestFileChannel {
	public static void main(String[] args) throws IOException, InterruptedException {
		File file = new File("D:\\test\\channel.txt");
		FileOutputStream fos = new FileOutputStream(file, true);
		FileChannel channel = fos.getChannel();
		FileLock lock = channel.tryLock();	//锁定整个文件(主线程中锁定该文件)
		System.out.println("main thread is running ......");
		if (lock.isValid()) {
			System.out.println("get the lock of file " + file.getName());
			FileChannelRunable r = new FileChannelRunable(file);
			Thread thread = new Thread(r);
			thread.start();	
			Thread.sleep(10000);
			System.out.println("main thread is still running ......");			
			fos.write("我刚才被被锁定啦。。。".getBytes()); // 注意:主线程向文件写入数据
			System.out.println("main thread write data to file " + file.getName());			
			lock.release();
			System.out.println("release the lock!");
		}
		fos.close();
	}
}

public class FileChannelRunable implements Runnable {
	private boolean flag = true;
	private File file;
	public FileChannelRunable(File file) {
		this.file = file;
	}
	@Override
	public void run() {
		while (flag) {
			try {
				Thread.sleep(500);
				FileReader fr = new FileReader(file);
				int c;
				//当通过FileReader读取时捕获异常信息因为File已经被锁定了
				while ((c = fr.read()) != -1) {	
					System.out.print("" + (char) c);
				}
				fr.close();
				flag = false;
			} catch (Exception e) {
				System.out.println("error : " + e.getMessage());
			}
		}
	}
}

      public abstract class MappedByteBuffer extends ByteBuffer
     直接字节缓冲区,其内容是文件的内存映射区域,映射的字节缓冲区是通过 FileChannel.map 方法创建的。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值