通道是NIO的一个主要创新,用于在Buffer与通道另一端之间进行有效的数据传输,这点在NIO技术-1-缓冲区有讲过,这里不在赘述。
I/O可以分为文件IO和流IO,那么通道对应的就可以分为文件通道(FileChannel)和流通道(流通道就是套接字通道,SocketChannel),所以NIO中有四种通道实现类:
- FileChannel:文件通道,用于操作文件I/O
- ServerSocketChannel:服务器套接字通道,用于TCP连接响应客户端连接
- SocketChannel:套接字通道,用于TCP协议,客户端连接服务器后,服务器和客户端都会有一个
- SocketChannel,就可以互相发送数据了
- DatagramChannel:数据报通道,用于UDP协议
打开一个通道的方法如下:
//打开一个文件通道,指定为可读写
RandomAccessFile raf = new RandomAccessFile("d:/test.txt", "rw");
FileChannel fc = raf.getChannel();
// 打开一个服务器套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 打开一个套接字通道,
SocketChannel sc = SocketChannel.open();
// 打开一个数据报通道
DatagramChannel dc = DatagramChannel.open();
文件通道还可以通过底层文件句柄的的方式获得,但是这样有可能导致不能读写文件
//不要使用这种方式获取通道实例
FileInputStream fis = new FileInputStream("d:/test.txt");
FileChannel fileChannel = fis.getChannel();
ByteBuffer buff = ByteBuffer.allocate(8192);
fileChannel.write(buff);
还可以通过java.nio.channels.Channels这个工具类获取通道实例,下面是一个例子:
// 创建一个可读通道
ReadableByteChannel rbc = Channels.newChannel(System.in);
// 创建一个可写通道
WritableByteChannel wbc = Channels.newChannel(System.out);
// 创建一个大小为8192字节的字节缓冲区
ByteBuffer buff = ByteBuffer.allocate(8192);
// 轮询将可读通道的数据读到缓冲区
while (rbc.read(buff) != -1) {
// 翻转缓冲区
buff.flip();
String str = new String(buff.array()).trim();
// 若输入"bye"则关闭通道
if (str.equals("bye")) {
rbc.close();
wbc.close();
break;
}
// 将缓冲区的数据写入到可写通道
wbc.write(buff);
// 轮询缓冲区是否还有剩余数据
while (buff.hasRemaining()) {
wbc.write(buff);
}
// 清空缓冲区
buff.clear();
}
通道可以以阻塞(blocking)或非阻塞(non-blocking)模式运行,阻塞模式会一直等待某个操作直到返回结果;非阻塞不会一直等待,要么返回null,要么返回执行完的结果。只有流通道才能已non-blocking模式运行,如Socket和Pipe。
Socket通道类继承SelectableChannel,只有SelectableChannel类才能与选择器(Selector)一起使用。
关闭通道使用close()方法,调用close()方法根据操作系统的网络实现不同可能会出现阻塞,可以在任何时候多次调用close();若出现阻塞,第一次调用close()后会一直等待;若第一次调用close()成功关闭后,之后再调用close()会立即返回,不会执行任何操作。
在一个已关闭的通道上进行I/O操作会抛出ClosedChannelException,可以通道isOpen()方法来检查通道时候为打开状态。
NIO中的通道都实现了InterruptibleChannel,若某个线程上有一个处于阻塞状态的通道,线程被中断会抛出ClosedByInterruptException,并会关闭通道。可以调用isInterrupted()方法检查某个线程的interrupt状态。
一、分散读到多个缓冲区 & 从多个缓冲区聚集写入通道
//test.txt中只有一行数据:whoareu?
//创建2个缓冲区,组成一个缓冲区数组
//打开一个文件通道,将test.txt中的数据读到缓冲区数组中
//缓冲区数组会自动填充buffA和buffB这两个缓冲区
//buffA填满后,再继续填充buffB
//达到分散读取数据到多个缓冲区中
ByteBuffer buffA = ByteBuffer.allocate(6);
ByteBuffer buffB = ByteBuffer.allocate(5);
ByteBuffer[] buffArr = { buffA, buffB };
RandomAccessFile raf = new RandomAccessFile("test.txt", "r");
FileChannel fc = raf.getChannel();
fc.read(buffArr);
System.out.println(new String(buffA.array()));//输出who
System.out.println(new String(buffB.array()));//输出areu?
fc.close();
raf.close();
//创建两个Buffer,组成一个Buffer Array
//将Buffer Array的数据写入到文件通道到
//达到聚集缓冲区其中写入通道目的
byte[] byteA = "hello ".getBytes("UTF-8");
ByteBuffer buffC = ByteBuffer.wrap(byteA);
byte[] byteB = "world!".getBytes("UTF-8");
ByteBuffer buffD = ByteBuffer.wrap(byteB);
ByteBuffer[] allBuff = {buffC,buffD};
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
FileChannel fc = raf.getChannel();
fc.write(allBuff);
fc.close();
raf.close();
二、文件通道(FileChannel)
FileChannel不能直接创建,只能通过创建一个文件对象(RandAccessFile、FileInputStream、FileOutputStream)后调用其getChannel()方法获得。FileChannel是线程安全,多个进程并发操作同一文件不会引起任何问题;兵法行为受低层操作系统或文件系统影响。
FileChannel类保证同一个JVM上的所有FileChannel实例看到的文件内容是一致的,但不能保证外部的非Java进程看到的该文件视图一致,也可能一致,这取决于低层操作系统的实现。
打开一个文件通道可以使用下面方式:
//RandomAccessFile有2中构造方法,下面的构造方法等同于:
//RandomAccessFile raf = new RandomAccessFile(new File("test.txt"), "rw");
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
FileChannel fc = raf.getChannel();
RandomAccessFile构造方法的第二个参数含义如下:
- "rw":对文件可读可写,若文件不存在则会创建该文件
- "r":只读
- "rws":对文件可读写,并且文件中数据和元信息(若更新时间等)的每个更新都会写入到磁盘
- "rwd":对文件可读写,并且文件数据的每个更新会同步写入到磁盘
文件锁在Jdk1.4时才被提供,当多个程序并发操作同一个文件时,可以使用文件锁来锁定文件同一时刻只能接受一个程序的IO操作。文件锁分为独占锁和共享锁,FileChannel提供了文件锁的API,在FileChannel的文件锁方式中,锁的对象是文件而不是通道或线程,也就是说文件锁不适用于同一个JVM上多个线程并发访问文件的情况。同一个JVM中,一个线程获得了某个文件的独占锁,第二个线程也可以获得这个文件的独占锁;但是,在不同的JVM中,第一个JVM的线程获得的某个文件的独占所,第二个JVM的线程会被阻塞。导致这样的情况的原因是文件锁是由操作系统在进程级上来判优的,而不是在线程级上。
文件锁可以通过FileChannel的lock()或tryLock()方法获取,两者的区别如下:
- lock():阻塞,第一个线程获取文件锁后,第二个线程必须等待
- tryLock():非阻塞,若不能立即获得文件锁则返回null
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
FileChannel fc = raf.getChannel();
//--------------------------------------------
//阻塞获得文件独占锁,并锁定文件所有数据
FileLock lock0 = fc.lock();
//阻塞获得文件独占锁,并锁定文件指定数据
FileLock lock1 = fc.lock(0, 8192, false);
//阻塞获得文件共享锁,并锁定文件0 ~ 8192字节的数据
FileLock lock2 = fc.lock(0, 8192, true);
//--------------------------------------------
//非阻塞获取文件独占所,等同于lock()
FileLock lock3 = fc.tryLock();
//非阻塞获取文件独占所,等同于fc.lock(0, 8192, false);
FileLock lock4 = fc.tryLock(0, 8192, false);
//非阻塞获取文件共享锁,等同于fc.lock(0, 8192, true);
FileLock lock5 = fc.tryLock(0, 8192, true);
FileLock对象关联FileChannel,FileLock API如下:
- channel():获取关联的FileChannel
- isShared():判断是共享锁还是独占锁,返回ture是共享锁,返回false是独占锁
- overlaps(long position, long size):判断当前文件锁锁定的区域是否有交叉,也就是是否也被别线程锁定了
- isValid():判断当前文件锁是否有效
- release():释放文件锁,通道被关闭或JVM关闭时也会释放文件锁。
实际应用中,一般使用共享锁读文件,使用独占锁写文件