IO(了解)
Java IO方式大体上可以分为三类,基于不同的io模型可以简单分为同步阻塞的BIO,同步非阻塞的NIO和异步非阻塞的AIO。
阻塞IO 和 非阻塞IO
这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了)
同步IO 和 非同步IO
这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题: 前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。
BIO(blocking IO)
BIO就是传统的java io编程,其相关的类和接口在java.io包下。同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。使用经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
在高并发的情况下,服务端会产生大量线程,线程间会发生竞争和上下文切换,同时要占用栈空间和CPU资源,而且其中有些线程可能什么事情都不会做,一直阻塞着,这些情况都会造成服务端性能下降。
Java中的BIO分布式分为两种:
- 传统BIO:一请求一应答;
- 伪异步IO:通过线程池固定线程的最大数量,可以防止资源的浪费。
NIO(java non-blocking IO)
NIO是从java1.4版本开始引入的一个新IO API,可以代替标准的java IO API。NIO与原来的IO具有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事。如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
NIO有三大核心部分:Channel(通道) ,Buffer(缓冲区) ,Selector(选择器)
- NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中
- Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。
BIO 和 NIO 的区别
- BIO 以流的方式处理数据,而 NIO 以块的方式处理数据;
- BIO 是阻塞的, NIO 是非阻塞的
- BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。
AIO( Asynchronous I/O)
AIO是在JDK1.7中推出的新的IO方式–异步非阻塞IO,也被称为NIO2.0,AIO在进行读写操作时,直接调用API的read和write方法即可,这两种均是异步的方法,且完成后会主动调用回调函数。
异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
Java提供了四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。
JAVA-BIO(基本IO)
按照字节流和字符流分类
字节流,红色部分读写非对应
输入字节流
- InputStream是所有数据字节流的父类,它是一个抽象类。
- ByteArrayInputStream、StringBufferInputStream、FileInputStream是三种基本的介质流,它们分别从Byte数组、StringBuffer、和本地文件中读取数据,PipedInputStream是从与其他线程共用的管道中读取数据。
- ObjectInputStream和所有FileInputStream 的子类都是装饰流(装饰器模式的主角)。
输出字节流 OutputStream
- OutputStream是所有输出字节流的父类,它是一个抽象类。
- ByteArrayOutputStream、FIleOutputStream是两种基本的介质,它们分别向Byte 数组,和本地文件中写入数据。PipedOutputStream是从与其他线程共用的管道中写入数据。
- ObjectOutputStream和所有FileOutputStream的子类都是装饰流。
字符流,红色部分读写非对应
字符输入流Reader
- Reader是所有的输入字符流的父类,它是一个抽象类。
- CharReader、StringReader 是两种基本的介质流,它们分别将Char数组、String中读取数据。PipedInputReader 是从与其他线程共用的管道中读取数据。
- BuffereReader 很明显的是一个装饰器,它和其子类复制装饰其他Reader对象。
- FilterReader 是所有自定义具体装饰流的父类,其子类PushbackReader 对Reader 对象进行装饰,增加一个换行符。
- InputStreamReader 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。FileReader 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream转变为Reader 的方法。
字符输出流Writer
- Writer 是所有输出字符流的父类,它是一个抽象类。
- CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向Char 数组、String 中写入数据。PipedInputWriter 是从与其他线程共用的管道中读取数据。
- BuffereWriter 很明显是一个装饰器,他和其子类复制装饰其他Reader对象。
- FilterWriter 和PrintStream 及其类似,功能和使用也非常相似。
- OutputStreamWriter 是OutputStream 到Writer 转换到桥梁,它的子类FileWriter 其实就是一个实现此功能的具体类(具体可以研究一下SourceCode)
按照数据操作分类
InputStream和OutputStream常用方法
File常用方法
| 方法 | 说明 |
|---|---|
| public boolean createNewFile() throws IOException | 该方法的作用是创建指定的文件。该方法只能用于创建文件,不能用于创建文件夹,且文件路径中包含的文件夹必须存在。 |
| public boolean delete() | 该方法的作用是删除当前文件或文件夹。如果删除的是文件夹,则该文件夹必须为空。如果需要删除一个非空的文件夹,则需要首先删除该文件夹内部的每个文件和文件夹,然后在可以删除 |
| public boolean exists() | 该方法的作用是判断当前文件或文件夹是否存在。 |
| public String getAbsolutePath() | 该方法的作用是获得当前文件或文件夹的绝对路径 |
| public String getName() | 该方法的作用是获得当前文件或文件夹的名称 |
| public String getParent() | 该方法的作用是获得当前路径中的父路径 |
| public boolean isDirectory() | 该方法的作用是判断当前File对象是否是目录。 |
| public boolean isFile() | 该方法的作用是判断当前File对象是否是文件 |
| public long length() | 该方法的作用是返回文件存储时占用的字节数。该数值获得的是文件的实际大小,而不是文件在存储时占用的空间数。 |
| public String[] list() | 该方法的作用是返回当前文件夹下所有的文件名和文件夹名称。说明,该名称不是绝对路径。 |
| public File[] listFiles() | 该方法的作用是返回当前文件夹下所有的文件对象。包含其属性。 |
| public boolean mkdir() | 该方法的作用是创建当前文件文件夹,若某一级文件夹不存在创建失败,返回flase |
| public boolean mkdirs() | 该方法的作用是创建文件夹,如果当前路径中包含的父目录不存在时,也会自动根据需要创建 |
| public boolean renameTo(File dest) | 该方法的作用是修改文件名。在修改文件名时不能改变文件路径,如果该路径下已有该文件,则会修改失败。 |
| public boolean setReadOnly() | 该方法的作用是设置当前文件或文件夹为只读。 |
在file的尾部添加数据的两种方法
- 方法1:利用RandomAccessFile类
- 将randomAccessFile模式设置为rw
- 将randomAccessFile移动(seek)到文件末尾
- 追加数据
- 关闭流
- 方法2:利用FileWriter类
- 将FileWriter构造方法第二个参数置为true.表示在尾部追加
- 追加数据
- 关闭流
JAVA-NIO
Standard IO是对字节流的读写,在进行IO之前,首先创建一个流对象,流对象进行读写操作都是按字节 ,一个字节一个字节的来读或写。而NIO把IO抽象成块,类似磁盘的读写,每次IO操作的单位都是一个块,块被读入内存之后就是一个byte[],NIO一次可以读或写多个字节。
NIO有三大核心部分:Channel(通道) ,Buffer(缓冲区) ,Selector(选择器)
Channel(通道)
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
常用的Channel:
-
FileChannel: 从文件中读写数据;
-
DatagramChannel: 通过 UDP 读写网络中数据;
-
SocketChannel: 通过 TCP 读写网络中数据;
-
ServerSocketChannel: 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
FileChannel基本使用
创建FileChannel
1.使用FileInputStream或FileOutputStream的getChannel()方法:
FileInputStream fileInputStream = new FileInputStream("文件地址");
fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("文件地址");
fileOutputStream.getChannel();
2.使用RandomAccessFile的getChannel()方法:
RandomAccessFile randomAccessFile = new RandomAccessFile("文件地址", "rw");
FileChannel channel = randomAccessFile.getChannel();
3.使用FileChannel.open()创建:
FileChannel fileChannel = FileChannel.open(Paths.get("文件地址"), StandardOpenOption.READ);
FileChannel常用方法
| 方法 | 说明 |
|---|---|
| public int read(ByteBuffer dst) throws IOException | 读取数据到dst缓存区,返回的值表示读取到的字节数,如果读到了文件末尾,返回值为 -1。读取数据时,position 会往后移动。 |
| public long read(ByteBuffer[] dsts, int offset, int length) throws IOException | 读取数据到dsts数组缓存区,offset代表下标,length代表长度。比如dsts数组位5,offset=1,length=3,则把数据读取到1,2,3角标的ByteBuffer中 |
| public long read(ByteBuffer[] dsts) throws IOException | 内部调用read(dsts, 0, dsts.length) |
| public int write(ByteBuffer src) throws IOException | 将缓存去中的position到Limit-1的数据通过通道写到文件中,返回写入数据的长度 |
| public long write(ByteBuffer[] srcs, int offset, int length) throws IOException | 将srcs中的缓存去中的position到Limit-1的数据通过通道写到文件中,返回写入数据的长度。offset和length含义同上面的read方法 |
| public long write(ByteBuffer[] srcs) throws IOException | 调用write(srcs, 0, srcs.length) |
| public long position() | 返回文件字节数据的偏移量 |
| public FileChannel position(long newPosition) throws IOException | 设置此通道的文件位置。将该位置设置为大于文件当前大小的值是合法的,但这不会更改文件的大小。稍后试图在这样的位置读取字节将立即返回已到达文件末尾的指示。稍后试图在这种位置写入字节将导致文件扩大,以容纳新的字节;在以前文件末尾和新写入字节之间的字节值是未指定的。 |
| public long size() throws IOException | 返回文件大小 |
| public FileChannel truncate(long size) throws IOException | 将此通道的文件截取为给定大小。如果给定大小 小于该文件的当前大小,则截取该文件,丢弃文件新末尾后面的所有字节。如果给定大小大于或等于该文件的当前大小,则不修改文件。无论是哪种情况,如果此通道的文件位置大于给定大小,则将位置设置为该大小。 |
| public void force(boolean metaData) throws IOException | 强制将所有对此通道的文件更新写入包含该文件的存储设备中。metaData 参数可用于限制此方法必须执行的 I/O 操作数量。为此参数传入 false 指示只需将对文件内容的更新写入存储设备;传入 true 则指示必须写入对文件内容和元数据的更新,这通常需要一个以上的 I/O 操作。此参数是否实际有效取决于基础操作系统,因此是未指定的。 |
| public long transferTo(long position, long count,WritableByteChannel target)throws IOException | 将字节从此通道的文件传输到给定的可写入字节通道。 参数:position - 文件中的位置,从此位置开始传输;必须为非负数count - 要传输的最大字节数;必须为非负数target - 目标通道返回:实际已传输的字节数,可能为零 |
| public long transferFrom(ReadableByteChannel src,long position, long count)throws IOException | 将字节从给定的可读取字节通道传输到此通道的文件中 参数:src - 源通道position - 文件中的位置,从此位置开始传输;必须为非负数count - 要传输的最大字节数;必须为非负数 返回:实际已传输的字节数,可能为零 |
| public int read(ByteBuffer dst, long position) throws IOException | 从给定的文件位置开始,从此通道读取字节序列,并写入给定的缓冲区。参数:dst - 要向其中传输字节的缓冲区;position - 开始传输的文件位置;必须为非负数; 返回:读取的字节数,可能为零,如果给定的位置大于或等于该文件的当前大小,则返回 -1 |
| public int write(ByteBuffer src, long position) throws IOException | 从给定的文件位置开始,将字节序列从给定缓冲区写入此通道。 参数:src - 要传输其中字节的缓冲区;position - 开始传输的文件位置;必须为非负数; 返回:写入的字节数,可能为零 |
| public MappedByteBuffer map(MapMode mode,long position, long size) throws IOException | 将此通道的文件区域直接映射到内存中。 参数:mode - 根据是按只读、读取/写入或专用(写入时拷贝)来映射文件,分别为 FileChannel.MapMode 类中所定义的 READ_ONLY、READ_WRITE 或 PRIVATE 之一;position - 文件中的位置,映射区域从此位置开始;必须为非负数;size - 要映射的区域大小;必须为非负数且不大于 Integer.MAX_VALUE |
| public FileLock lock(long position, long size, boolean shared) throws IOException | 获取此通道的文件给定区域上的锁定。 参数:position - 锁定区域开始的位置;必须为非负数;size - 锁定区域的大小;必须为非负数,并且 position + size 的和必须为非负数;shared - 要请求共享锁定,则为 true,在这种情况下此通道必须允许进行读取(可能是写入)操作;要请求独占锁定,则为 false,在这种情况下此通道必须允许进行写入(可能是读取)操作;返回:一个锁定对象,表示新获取的锁定 |
| public final FileLock lock() throws IOException | 获取对此通道的文件的独占锁定 |
| public FileLock tryLock(long position, long size, boolean shared)throws IOException | 试图获取对此通道的文件给定区域的锁定 参数同lock(long position, long size, boolean shared) |
| public final FileLock tryLock() throws IOException | 试图获取对此通道的文件的独占锁定 |
Buffer(缓冲区)
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
常用的Buffer:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
缓冲区状态变量
capacity: 最大容量;position: 当前已经读写的字节数;limit: 还可以读写的字节数;
缓冲区状态变量的变化:
- 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
- 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5,limit 保持不变。
- 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
- 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
- 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
常用方法
Selector(选择器)
NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。
应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
Selector的使用
- 创建选择器
Selector selector = Selector.open();
2.将通道注册到选择器上 通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
当使用多个具体事件时可以用SelectionKey.OP_READ | SelectionKey.OP_WRITE。
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector,
SelectionKey.OP_ACCEPT);
3.轮询获取不同事件并做处理
while (true) {
int count = selector.select(); // 获取已就绪事件的数量
if (count == 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 获取已就绪键集
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isAcceptable()) {
// 接收处理
} else if (key.isConnectable()) {
// 连接处理
} else if (key.isReadable()) {
// 读取处理
} else if (key.isWritable()) {
// 写入处理
}
key.remove(); // 移除键,防止下次重复处理
}
}
一些常用方法
1.Selector
| 方法 | 说明 |
|---|---|
| boolean isOpen() | 判断selector是否是open状态,如果调用了close()方法则会返回false |
| Selector open() | 打开一个选择器 |
| int select(long timeout) | 在超时时间内,有就绪事件时才会返回,其次超过时间也会返回 |
| int select() | 阻塞直到有事件就绪时才会返回 |
| Set< SelectionKey > keys() | 当前Channel注册在Selector上面的所有的key |
| Set< SelectionKey > selectedKeys() | 当前Channel所有已就绪的事件 |
| Selector wakeup() | 调用该方法会时,阻塞在select()处的线程会立即返回。即使当前不存在线程阻塞在select()处,那么下一个select()方法也会立即返回 |
| void close() | 用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效 |
| SelectorProvider provider() | 返回创建此通道的提供者。 |
2.SelectionKey
| 方法 | 说明 |
|---|---|
| Object attach(Object ob) | 将给定的对象附加到此键 |
| Object attachment() | 获取当前的附加对象 |
| void cancel() | 请求取消此键的通道到其选择器的注册。 |
| SelectableChannel channel() | 返回为之创建此键的通道。 |
| int interestOps() | 获取此键的 interest 集合。 |
| SelectionKey interestOps(int ops) | 将此键的 interest 集合设置为给定值。 |
| boolean isAcceptable() | 检测此键的通道是否已准备好接受新的套接字连接。 |
| boolean isConnectable() | 检测此键的通道是否已完成其套接字连接操作。 |
| boolean isReadable() | 检测此键的通道是否已准备好进行读取。 |
| boolean isValid() | 检测此键是否有效。 |
| boolean isWritable() | 检测此键的通道是否已准备好进行写入。 |
| int readyOps() | 获取此键的 ready 操作集合。 |
| Selector selector() | 返回为此选择器创建的键。 |
JAVA-AIO(了解)
AIO 主要使用Java提供了四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。
暂不需要了解