Java NIO
4. Java NIO 核心组件:Selector 选择器,Pipe 管道
Channel 通道
Java NIO 的主要 Channel 实现类有以下:
- FileChannel:用于文件 I/O
- SocketChannel,ServerChannel:用于 TCP I/O
- DatagramChannel:用于 UDP I/O
这些类都位于
java.nio.channels 包下;
FileChannel 文件通道
Java FileChannel 文件通道可以由
FileInputStream,FileOutputStream,RandomAccessFile
这3个类通过
getChannel() 方法产生;
常用的方法 API
文件通道的读写示例
//通过文件通道向文件写入缓冲区数据
FileChannel outChannel = new FileOutputStream("data.dat").getChannel();
ByteBuffer buffer = ByteBuffer.wrap("Are you ok ? Mi fans !".getBytes("UTF-8"));
outChannel.write(buffer);
outChannel.close();
//通过文件通道从文件读取数据到缓冲区
FileChannel inChannel = new FileInputStream("data.dat").getChannel();
buffer = ByteBuffer.allocate(1024);
inChannel.read(buffer);
//从缓冲区读取读取内容并转化为字符串
buffer.flip();
byte[] bytesTemp = new byte[buffer.remaining()];
buffer.get(bytesTemp);
String content = new String(bytesTemp,"UTF-8"); //content = "Are you ok ? Mi fans !"
//or
buffer.flip();
content = Charset.forName("UTF-8").decode(buffer).toString(); //content = "Are you ok ? Mi fans !"
inChannel.close();
通道之间传输数据,快速复制文件
通过 FileChannel 的
tranferFrom 和
tranferTo方法可以将一个通道的数据传输到另一个通道中,基于这个特性,可以使用这两个方法进行快速文件复制,文件复制的速度比常规 FileInputStream,FileOuputSrteam 逐字节复制的速度要快上多倍,我测试10G文件地复制速度要差了5倍左右;
FileChannel in = new FileInputStream("data1.dat").getChannel();
FileChannel out = new FileOutputStream("data1_copy.dat").getChannel();
out.transferFrom(in,0, in.size()); // or: in.transferTo(0,in.size(),out);
in.close();
out.close();
MappedByteBuffer 内存映射文件
内存映射文件允许创建和修改那些因为太大而不能完全放置入内存的文件,在使用内存映射文件的情况下,我们可以假定整个文件都放置在内存中,而且可以把它当做非常大的数组来访问;
FileChannel in = new FileInputStream("bigFile.dat").getChannel();
//映射源文件中 0 - 1024*1024 (共 1 M)的片段,此时该片段并没有真正读入内存中
MappedByteBuffer mapBuffer = in.map(FileChannel.MapMode.READ_ONLY,0,1024 * 1024);
//读取 mapBuffer 中 0 - 1024(1kb)的子片段到字节缓冲区中
mapBuffer.load().position(0).limit(1024);
ByteBuffer buffer = mapBuffer.slice();
while(buffer.hasRemaining())
System.out.println(buffer.get());
//向 mapBuffer 中 1024 - 2048 的子片段写入缓冲区内容
mapBuffer.load().position(1024).limit(2048);
buffer = mapBuffer.slice();
while(buffer.hasRemaining())
buffer.put((byte)233);
in.close();
使用映射文件技术来复制大容量文件,虽然速度比直接传输通道数据要慢一些,但是节约内存,而且在此基础实现多线程复制也是很容易的;
FileChannel src = new FileInputStream("bigFile.dat").getChannel();
FileChannel copy = new FileOutputStream("bigFile_copy.dat").getChannel();
MappedByteBuffer mapBuf = null;
int cur = 0;
int size = 1024 * 1024; //限制每一个文件映射缓冲区大小为 1M;
while(cur < src.size()){
mapBuf = src.map(FileChannel.MapMode.READ_ONLY, cur, cur + size < src.size() ? size : src.size() - cur);
copy.write(mapBuf.load());
cur += size;
}
src.close();
copy.close();
FileLock 文件锁定
JDK 1.4 引入了文件锁概念,允许同步地访问某个作为共享资源的文件,这种技术是基于 FileChannel 通道技术的;
竞争统一文件的线程可以是以下角色:
- 两个线程存在与不同的 JVM 虚拟机上,
- 一个是Java线程,一个是操作系统中的某个本地线程;
文件锁对于其他操作系统线程是可见的,这是因为 Java 文件加锁是直接映射到本地操作系统的加锁工具上;
对文件添加文件锁,只需要调用
FileChannel
的
lock() /tryLock(),即可获取整个文件的
FileLock;
- tryLock():获取文件的非阻塞锁,设法获取文件锁,如果无法获取(当其他进程已经持有相同的文件锁,且不共享时),直接从调用方法返回;
- lock():获取文件的阻塞锁,阻塞进程直到该文件锁可以获取,或者调用线程被中断;
FileOutputStream fos = new FileOutputStream("data.dat");
FileLock flock = fos.getChannel().tryLock();
while(flock == null){ //尝试直到获取到文件锁
TimeUnit.MILLISECONDS.sleep(100);
flock = fos.getChannel().tryLock();
}
....
flock.release(); //释放文件锁
//也可以对文件通道的某一部分加锁
tryLock(long position, long size, boolean true); //true:表示该锁是否是独占的
SocketChannel
,
ServerSocketChannel
,
DaragramChannel
不需要加锁,因为它们本身就是从单进程体
SelectableChannel
继承实现的;
对映射文件部分加锁
文件映射通常应用于极大的文件,可能需要对这种巨大的文件进行部分加锁,以便其他进程可以修改文件的未加锁部分;
以下示例多线程使用文件映射技术,对文件进行复制;
public class MappedCopyTest {
private FileChannel src = null;
private FileChannel copy = null;
private final int PER_MAPPED_SIZE = 1024 * 1024 * 10; //映射缓冲区大小限制 10 M;
private final int THREAD_COUNY = 5; //线程总数
public void copy(String srcPath,String destPath) throws IOException {
src = new FileInputStream(srcPath).getChannel();
copy = new FileOutputStream(destPath).getChannel();
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < THREAD_COUNY ; i ++)
exec.execute(new CopyThread(src.size() / THREAD_COUNY * i));
exec.shutdown();
}
class CopyThread extends Thread{
private long cur = 0;
public CopyThread(long position){
this.cur = position;
}
public void run() {
try {
int mappedSize = (int) (cur + src.size() / THREAD_COUNY < src.size() ? src.size() / THREAD_COUNY : src.size() - cur);
FileLock lock = src.lock(cur,cur + mappedSize, false); //给该子线程复制区域加独占锁
MappedByteBuffer mapBuff = src.map(FileChannel.MapMode.READ_ONLY, cur, mappedSize);
int subcur = 0;
while (subcur < mappedSize){
int end = subcur + PER_MAPPED_SIZE < mappedSize ? subcur + PER_MAPPED_SIZE : mappedSize;
mapBuff.load().position(subcur).limit(end);
copy.write(mapBuff.slice());
subcur += PER_MAPPED_SIZE;
}
lock.release(); //文件锁释放
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
SocketChannel 和 ServerScoketChannel TCP 通道
SocketChannel 和 ServerSocketChannel 用于将通道与 TCP 网络套接字链接,相当于在网络编程中使用 Java 网络套接字 Socket,这两个类的用法类似于 FileChannel,以下是一个简单那个的网络程序示例:
TCPServer
public class TcpServer {
public static void main(String[] args) throws IOException {
// 创建 ServerSocketChannel,并绑定 TCP 端口,使用阻塞模式
ServerSocketChannel ssc = ServerSocketChannel.open();
//ssc.configureBlocking(false); //设置为非阻塞模式
ssc.bind(new InetSocketAddress("127.0.0.1",2333));
while(true){
//获取套接字通道 SocketChannel
SocketChannel sc = ssc.accept();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//从 SocketChannel 读取内容到缓冲区
sc.read(buffer);
buffer.flip();
System.out.println("from client["+ sc.getRemoteAddress() + "] : "
+ Charset.forName("UTF-8").decode(buffer).toString());
//从缓冲区写入内容到 SocketChannel
buffer.clear();
buffer.put("Hi, My friend!".getBytes("UTF-8")).flip();
sc.write(buffer);
sc.close();
}
}
}
TCPClient
public class TCPClient {
public static void main(String[] args) throws IOException {
//创建 SocketChannel,并链接 TCP 端口
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("127.0.0.1", 2333));
ByteBuffer buffer = ByteBuffer.allocate(1024);
//向 SocketChannel 写入缓冲区数据
buffer.put("Hello world!".getBytes("UTF-8")).flip();
sc.write(buffer);
//从 SocketChannel 读取数据到缓冲区
buffer.clear();
sc.read(buffer);
buffer.flip();
System.out.println("from server[" + sc.getRemoteAddress() + "] : "
+ Charset.forName("UTF-8").decode(buffer).toString());
sc.close();
}
}
DaragramChannel UDP通道
DaragramChannel 用于创建 UDP 网络通道,用法类似于 SocketChannel ,以下一个简单示例:
UDPServer
public class UDPServer {
public static void main(String[] args) throws IOException {
DatagramChannel dc = DatagramChannel.open();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//绑定端口,并从某个端口读取信息
dc.bind(new InetSocketAddress("127.0.0.1",2323));
dc.receive(buffer);
buffer.flip();
System.out.println("from client[" + dc.getRemoteAddress() + "] : "
+ Charset.forName("UTF-8").decode(buffer).toString());
dc.close();
}
}
UDPClient
public class UDPClient {
public static void main(String[] args) throws IOException {
DatagramChannel dc = DatagramChannel.open();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//从缓冲区发送数据通道 DatagramChannel 到某个 IP
buffer.put("deep dark ♂ fantastic".getBytes("UTF-8")).flip();
dc.send(buffer,new InetSocketAddress("127.0.0.1",2323));
dc.close();
}
}
Scatter / Gather 分散/聚集向量 I/O
在Java NIO中,通道提供了 分散/聚集向量I/O的重要功能, 通过这种技术,使用单个write()函数将字节从一组缓冲区写入流,并且可以使用单个read()函数将字节从流读取到一组缓冲区中,分别通过
ScatteringByteChannel、GatheringByteChannel 对该功能提供支持;
分散读取
ByteBuffer buffer1 = ByteBuffer.allocate(64);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
ScatteringByteChannel sc = (ScatteringByteChannel) new FileInputStream("data.dat");
sc.read(buffer1);
sc.read(buffer2);
System.out.println(buffer1.asIntBuffer().get());
System.out.println(Charset.forName("UTF-8").decode(buffer2).toString());
sc.close();
聚合输入
ByteBuffer buffer1 = ByteBuffer.wrap("Are you OK?".getBytes("UTF-8"));
ByteBuffer buffer2 = ByteBuffer.wrap(new byte[]{1,2,3,4,5,6,7,8});
GatheringByteChannel gc = (GatheringByteChannel) new FileOutputStream("dataout.dat");
gc.write(buffer1);
gc.write(buffer2);
gc.close();