面试过程中我们经常会遇到一些IO框架的问题,并且因为Java1.4中引入的NIO框架可以有效的解决高并发、大量连接、I/O处理的问题,越来越多的在大型网络应用中使用,所以也是需要掌握的框架之一。
一、Java IO(同步、阻塞)
首先看一张图,是对java.io中,流式部分的超类的整理。
java.io.png1. IO流的分类
处理的对象:字节流 和 字符流,二进制数据和编码处理后符合某种格式规定的特定数据。
数据流向:输入流 和 输出流,输入、输出方向不同。
2. 字符流和字节流的区别
1)使用方式上非常相似;
2)缓冲区的使用:字节流在操作时不会使用缓冲区(内存),而是直接操作文件本身,而字符流是使用缓冲区的;字节流操作文本时,即使不关闭(close方法),信息就已经输出了。而字节流不关闭时是不会有任何内容输出的,可以使用flush方法强制刷新缓冲区输出内容。
3)读写单位不同:字节流,以字节(8bit)为单位;字符流,以2个字节的Unicode字符为单位。
4)处理对象不同:字节流,能处理所有类型的对象(音频、视频、图片等);字符流只能处理字符类型的数据;
缓冲区:比较特殊的内存区域;(频繁操作一个资源(文件或数据库)性能会很低,所以将一部分数据读写到缓冲区后,然后一次性读写,类似批量操作,效率比较高)
3. 部分常用流的使用
字节流中,ByteArrayInputStream 和 ByteArrayOutputStream 作为数组字节流,在开发工作中有很多应用的地方。
ByteArrayInputStream:如果需要读取的数据是byte数组(String类型数据可以直接getBytes方法),并转换为流。并将其使用在后续需要的地方。
ByteOutputStream:如果我们需要将一个数据输出到OutputStream,并返回字节数组的情况下,ByteArrayOutputStream用起来就很方便。
static void testByteArrayInputStream() throws Exception {
String str = "test ByteArray";
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(str.getBytes());
//通过toByteArray方法获取到byte[],在转为字符串输出
System.out.println("ByteArrayOutputStream处理,将byte[]转为字符串:"+new String(bos.toByteArray()));
//使用ByteArrayInputStream读取byte[]
InputStream bis = new ByteArrayInputStream(bos.toByteArray());
//读取流中的字节读取到[]bytes中
byte[] bytes = new byte[bos.size()];
bis.read(bytes);
System.out.println("ByteArrayInputStream处理,将byte[]转为字符串:"+new String(bytes));
}
java.io.File:是IO包下的非流式类,文件和目录路径的抽象表示,提供了对文件和目录的操作方法,我们就基于这两个文件,进行测试。
public static File fileA = new File("D:/fileA.txt");
public static File fileB = new File("D:/fileB.txt");
文件读写操作中流的使用
在做文件读取和写入过程中,有几种方案供我们选择,一下列出几种方式
- 单纯使用 FileInputStream 和 FileOutputStream 完成读取和写入(数据量越大,效率越低)
static void testFileStream() throws Exception{
String str = "test FileStream";
OutputStream fos = new FileOutputStream(fileA);
fos.write(str.getBytes());
System.out.println("FileOutputStream 写入完成!");
//读取刚刚写入的文件中的内容
InputStream fis = new FileInputStream(fileA);
byte[] bytes = new byte[fis.available()]; //创建一个和文件大小相同的字节数组
fis.read(bytes); //写入字节数组
System.out.println("FileInputStream读取完成,将byte[]转为字符串:"+new String(bytes));
}
- 使用BufferedInputStream / BufferedOutputStream 包装FileInputStream / FileOutputStream完成读取和写入(效率高于第一种方式)
static void testBufferedStream() throws Exception {
long begin = System.currentTimeMillis();
String str = "test BufferedStream";
FileOutputStream fos = new FileOutputStream(fileB);
//使用BufferedOutoutStream进行一次包装,使用bos对象写文件
BufferedOutputStream bos = new BufferedOutputStream(fos);
bos.write(str.getBytes());
bos.flush();
System.out.println("FileOutputStream 写入完成!");
//读取刚刚写入的文件中的内容
FileInputStream fis = new FileInputStream(fileB);
BufferedInputStream bis = new BufferedInputStream(fis);
byte[] bytes = new byte[bis.available()];
bis.read(bytes);
System.out.println("BufferedInputStream 读取完成,将byte[]转为字符串:"+new String(bytes));
long end = System.currentTimeMillis();
System.out.println("FileWriter执行耗时:" + (end - begin) + " 豪秒");
}
- 使用FileReader 和 FileWriter 进行文件读写操作,大文件处理效率高于前两个。
static void testFileReaderAndWriter() throws IOException {
String str = "test FileReader And FileWriter";
FileWriter fw = new FileWriter(fileC);
BufferedWriter bw = new BufferedWriter(fw);
bw.write(str);
bw.flush();
System.out.println("FileWriter 写入完成!");
FileReader fr = new FileReader(fileC);
BufferedReader br = new BufferedReader(fr);
System.out.println("FileReader 读取完成:" + br.readLine());
}
- SequenceInputStream表示其他流的逻辑串联,其实就是一次读取多个流。
static void testSequenceInputStream() throws Exception{
InputStream osA = new FileInputStream(fileA);
InputStream osB = new FileInputStream(fileB);
InputStream osC = new FileInputStream(fileC);
//建立一个输入流的集合
List<InputStream> inputStreamList = new ArrayList<InputStream>();
inputStreamList.add(osA);
inputStreamList.add(osB);
inputStreamList.add(osC);
//通过Collections.enumeration得到 enumeration 对象
Enumeration<InputStream> enumeration= Collections.enumeration(inputStreamList);
SequenceInputStream sis = new SequenceInputStream(enumeration);
//转BufferedReader 进行读取
BufferedReader br = new BufferedReader(new InputStreamReader(sis));
System.out.println("testSequenceInputStream 读取完成:"+br.readLine());
}
4. 关于流的关闭问题
上述代码中,并没有进行流的关闭操作和异常的处理,这里着重记录一下关于流的关闭问题。
这里抛出几个问题,并作出相应解答。
1)流打开后需不需要关闭?
打开的流就一定要关闭,使用close方法。
2)如果关闭的话,应该在什么位置关闭?
放入finally语句块中执行,先判断流是否为NULL,不为NULL进行关闭。
3)多个流嵌套使用时,关闭规则是什么?
理论上讲,先开启后关闭,后开启先关闭。如使用处理流,可直接关闭处理流,处理流会调用节点流的关闭方法。
Java7之后,InputStream、OutputStream、Reader、Writer,都实现了。Closeable/AutoCloseable接口。可以使用try-with-resources语句来释放java流对象。
//选择流
try(FileInputStream is = new FileInputStream(fileA);FileOutputStream os = new FileOutputStream(fileB)) {
//操作
}catch(FileNotFoundException e) {
e.printStackTrace();
}catch(IOException e) {
e.printStackTrace();
}
二、Java NIO(同步、非阻塞)
了解NIO前,先要整理一下IO的一些特点,并且和NIO进行一些对比。然后根据对比情况,再来讨论一下IO与
NIO的选择,因为NIO的编码相对比较复杂,其实多数主流中间件都是支持NIO的,所以不必我们自己编码实现,了解其原理和性能更为重要。
传统 java.io 包中的 IO 的特点
- 基于stream模型实现,提供了常见的IO功能: File抽象、输入输出流;
- 交互方式:同步、阻塞的方式,面向流(BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter应用了缓冲区技术);
- 在读写动作完成前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序;
java.nio包中NIO的特点
- 可以构建多路复用、同步、非阻塞IO程序,面向缓冲区(Buffer API,Java程序可以直接操作缓冲区);
- 同时提供了更接近操作系统底层的高性能数据操作方式;
- NIO加入了Selector(选择器),可以让一个线程监视(事件驱动机制)多个Channel进行管理,减少线程开销;
两者特点对比下
- 理论上IO面向流,从Stream中逐步读取数据,并没有使用缓冲区,而NIO面向缓冲区,数据操作更加高效,但BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter应用了缓冲区技术,如果合理使用,特定场景下效率并不弱于NIO;
- IO线程是阻塞的,也就是当进行read or writer 时,该线程只能等待至操作完成,期间线程不能做任何事情;而NIO线程是非阻塞的,通过Selector选择器选择合适的Channel进行处理,当一个Channel没有数据可用时,线程就可以取操作其他的Channel进行处理,这样更高效。
1. NIO的核心组件
1. Buffer(缓冲区):NIO的缓冲区不只是简单的byte数组,而是封装过的Buffer类,用于特定原始类型的数据容器,通过Buffer的API,我们可以灵活的操作各类型数据。
2. Channel(通道):负责打开IO设备(文件、硬件设备、网络套接字)的链接,本身不存储数据。
3. Selector(选择器):用于采集Channel的事件,首先将Channel注册到Selector并设置好关心的事件,当该Channel发生对应类型事件时,线程就为其服务。无事件发生,线程挂起让出CPU资源。
4. Charset(字符集):包含了字节和 Unicode 字符之间转换的 charset,还定义了用于创建解码器和编码器以及获取与 charset 关联的各种方法。
2. Buffer(缓冲区)
重要概念
capacity(容量):表示Buffer的最大数据容量,创建后不能更改;
limit(限制):缓冲区读写数据的终止点,limit位置后的区域无法访问;
position(位置):缓冲区读写数据的位置,初始值为0,随着数据加入改变;
mark(标记):调用mark()标记当前position;
rest(重置):重置position至mark()标记;
常用实现类
ByteBuffer , CharBuffer , DoubleBuffer , FloatBuffer , IntBuffer , LongBuffer , ShortBuffer
Buffer下子类的用法都比较类似,使用IntBuffer作为示例,来看一下其中方法使用
static void testBuffer(){
IntBuffer buffer = IntBuffer.allocate(10); //创建一块大小为10的缓冲区
System.out.println("缓冲区信息:"+buffer.toString());
buffer.put(1); //写入数据1
System.out.println("缓冲区信息:"+buffer.toString()+";内容:"+ Arrays.toString(buffer.array()));
buffer.put(2); //写入数据2
buffer.mark(); //标记当前的position
System.out.println("缓冲区信息:"+buffer.toString()+";内容:"+ Arrays.toString(buffer.array()));
buffer.reset(); //重置position至2的位置
buffer.put(3); //写入3
System.out.println("缓冲区信息:"+buffer.toString()+";内容:"+ Arrays.toString(buffer.array()));
}
控制台打印结果
缓冲区信息:java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
缓冲区信息:java.nio.HeapIntBuffer[pos=1 lim=10 cap=10];内容:[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
缓冲区信息:java.nio.HeapIntBuffer[pos=2 lim=10 cap=10];内容:[1, 2, 0, 0, 0, 0, 0, 0, 0, 0]
缓冲区信息:java.nio.HeapIntBuffer[pos=3 lim=10 cap=10];内容:[1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
1;2;3;0
ByteBuffer 的直接缓冲区和非直接缓冲区
直接缓冲区:通过allocateDirect()方法分配的缓冲区,将缓冲区建立在物理内存中,可提高效率,但资源消耗大,不易控制;
非直接缓冲区:通过allocate()方法分配的缓冲区,将缓冲区建立在JVM内存中;
3. Charset(字符集)java.nio.charset包
在学习Channel前,为了方便后续的代码演示,先对Charset做简单的了解,
重要概念
包含了字节和Unicode字符之间转换的charset。
CharsetEncode(编码器):将字符转换成字节。
CharsetDecode(解码器):将字节转换为字符。
解码器和编码器都不能直接创建,需要一个Charset对象来创建对应的解码器和编码器。
4. Channel(通道)java.nio.channels包
本身不能存储数据,通过Buffer(缓冲区)进行读写操作,read()方法,读取通道数据到缓冲区,writer()方法,将缓冲区数据写入通道。
需要节点作为创建基础。例如FileInputStream和FileOutputStream()的getChannel()。RandomAccessFile也能创建文件通道,支持读写模式。通过IO流创建的通道是单向的,使用RandomAccessFile创建的通道支持双向。
常用实现类
FileChannel:读取、写入、映射、操作文件的通道
DataChannel:通过 UDP 读写网络数据通道
SocketChannel:通过 TCP 读写网络数据通道
ServerSocketChannel:监听多个 TCP 连接,对每一个新进来的的链接,都创建一个SocketChannel
读写文件代码示例
//为方便演示代码中未处理Exception
static void testChannels() throws Exception {
File fileA = new File("D:/fileA.txt"); //读取的文件
File fileB = new File("D:/fileB.txt"); //写入的文件
//1. 通过RandomAccessFile 或 FileInputStream getChannel() 获取Channel;
FileChannel fileAChannel = new RandomAccessFile(fileA,"r").getChannel(); // new FileInputStream(fileA).getChannel();
//创建缓冲区,初始大小
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将Channel中数据读取到 byteBuffer
fileAChannel.read(byteBuffer);
System.out.println(byteBuffer.toString()); //读取后,未翻转状态下,缓冲区信息
byteBuffer.flip(); //翻转,将缓冲区状态存数据转为取数据,下方我们要进行数据显示。
System.out.println(byteBuffer.toString()); //翻转后,缓冲区信息,pos=0,lim等于读取数据的长度
//默认字符集创建解码器
CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
System.out.println("缓冲区信息:"+byteBuffer.toString()+";内容:" + decoder.decode(byteBuffer).toString());
//2. 通过Channel将A文件内容,写入B文件
byteBuffer.flip(); //翻转,将缓冲区状态取数据转为存数据
System.out.println(byteBuffer.toString()); //翻转后,缓冲区信息,pos=读取数据的长度,lim等于读取数据的长度
FileChannel fileBChannel = new FileOutputStream(fileB).getChannel();
fileBChannel.write(byteBuffer);
}
控制台打印结果
java.nio.HeapByteBuffer[pos=11 lim=1024 cap=1024]
java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]
缓冲区信息:java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024];内容:我是fileA
java.nio.HeapByteBuffer[pos=0 lim=11 cap=1024]
5. Selector(选择器)java.nio.channels包
以上概念中,我们知道了一个词,多路复用器,简单解释一下。
这里的IO一般指网络IO,多路可以理解为多个TCP/UDP连接(或多个Channel),复用就是复用一个或者少量线程。
IO多路复用:多个TCP/UDP连接产生的IO操作,用一个或少量线程来处理。
但在现有硬件设施基础场CPU有多个内个,如果不适用多任务处理,貌似时在浪费资源,本文先不讨论这个问题,只单纯了解Selector的一些知识。
Selector的使用方法,Selector.open()就是将Channel注册到Selector,并且Channel设置为非阻塞模式,然后Selector通过它的事件驱动机制采集Channel的事件,分配线程为其服务。
事件以及对应的SelectionKey
Accept:有可以接受的连接 ;SelectionKey.OP_ACCEPT
Connect:连接成功 ;SelectionKey.OP_CONNECT
Read:有数据可读 ;SelectionKey.OP_READ
Write:可以写入数据了 ;SelectionKey.OP_WRITE
通过一个小程序,来对NIO有一定深入理解,为了简化,只写了服务端读和客户端写的事件。有兴趣可以自行完善。
服务端示例
//模拟的一个服务端与客户端通信的过程,仅测试代码使用
static void testSelector() throws Exception{
CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
//创建一个Channel对象,并设置为非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8088));
//创建Selector对象
Selector selector = Selector.open();
//向Selector注册事件,validOps() 返回此通道支持的操作
serverSocketChannel.register(selector, serverSocketChannel.validOps());
while (true) {
System.out.println(new Date()+" 等待事件发生");
int number = selector.select(); //该调用会阻塞,直到至少有一个事件就绪、准备发生
if (number == 0) continue;
Set<SelectionKey> selectionKeySet = selector.selectedKeys(); //获取连接的集合
Iterator<SelectionKey> keyIterator = selectionKeySet.iterator(); //迭代,多客户端连接
while (keyIterator.hasNext()) {
final SocketChannel client;
SelectionKey selectionKey = keyIterator.next();
if (selectionKey.isAcceptable()) { //可连接
System.out.println("a connection was accepted by a ServerSocketChannel.");
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
client = channel.accept(); //接受套接字的连接
client.configureBlocking(false);
//接受连接后,将通道绑定为读事件,读取客户端消息
client.register(selector,SelectionKey.OP_READ);
} else if (selectionKey.isConnectable()) { //连接成功
System.out.println("a connection was established with a remote server.");
} else if (selectionKey.isReadable()) { //有数据读
System.out.println("a channel is ready for reading");
SocketChannel readChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
readChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println("客户端消息:"+decoder.decode(byteBuffer));
byteBuffer.clear();
} else if (selectionKey.isWritable()) { //可以写数据了
System.out.println("a channel is ready for writing");
}
keyIterator.remove(); //入不删除,会一直循环
}
}
}
客户端示例
static void testClient() throws Exception{
SocketChannel socketChannel = SocketChannel.open(); //创建通道
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("localhost",8088)); //连接服务端
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT); //注册事件
Scanner scanner = new Scanner(System.in);
while (true){
int select = selector.select(); //阻塞等待事件触发
if(select == 0) continue;
Set<SelectionKey> keys = selector.selectedKeys(); //事件的集合
Iterator<SelectionKey> keyIterator = keys.iterator(); //迭代
while (keyIterator.hasNext()) {
SelectionKey selectionKey = keyIterator.next();
if (selectionKey.isConnectable()) { //连接完成
System.out.println(new Date()+" 连接完成...");
SocketChannel channel = (SocketChannel) selectionKey.channel();
if(channel.isConnectionPending()){ //连接完成
channel.finishConnect(); //确认通道连接已建立
}
channel.register(selector,SelectionKey.OP_WRITE); //注册通道为写模式
}else if(selectionKey.isWritable()){
System.out.println(new Date()+" 输入发送信息");
SocketChannel channel = (SocketChannel) selectionKey.channel();
String str = scanner.nextLine();
if("exit".contains(str)){
break;
}
CharsetEncoder charset = Charset.defaultCharset().newEncoder();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(str.getBytes());
byteBuffer.flip();
channel.write(byteBuffer);
}
keyIterator.remove();
}
}
}
5. Tomcat中BIO、NIO、APR模式
简单些一下相关配置,具体性能可自行百度。
<!-- bio server.xml 配置 (重启生效)-->
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<!-- nio server.xml 配置 (重启生效)-->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" redirectPort="8443" />
<!-- apr server.xml 配置 (重启生效)需要安装 apr 、 apr-utils 、tomcat-native包 -->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol" connectionTimeout="20000" redirectPort="8443" />
三、结尾
在讲NIO开始时说过,其实我们不必自己搭建NIO相关代码,有很多很好用的NIO框架可供我们使用,比如Netty。在后续文章中我会写一下Netty的入门教程。
网友评论