BIO(阻塞式IO)
一、定义
1、什么BIO
BIO是block I/O简写,名为阻塞式IO,见名就可以知道,他是一种阻塞式的IO,有阻塞式不难想象肯定有非阻塞式,那么什么是阻塞式和非阻塞式呢?
2、阻塞式
简单来说,在服务器中bio是一个连接由一个专门的线程来服务的工作模式。就像餐厅里来一个客人就给这个客人安排一个专用服务员,这个服务员就只服务这一个客人直到他离开为止,若餐厅里服务员有限,则多余的客人只能等其它客人服务完才能收到服务。
3、非阻塞式,在我另一篇博客讲解
二、常用实体类讲解
1、接口摘要
Closeable | Closeable 是可以关闭的数据源或目标。 |
---|---|
DataInput | DataInput 接口用于从二进制流中读取字节,并根据所有 Java 基本类型数据进行重构。 |
DataOutput | DataOutput 接口用于将数据从任意 Java 基本类型转换为一系列字节,并将这些字节写入二进制流。 |
Externalizable | Externalizable 实例类的唯一特性是可以被写入序列化流中,该类负责保存和恢复实例内容。 |
FileFilter | 用于抽象路径名的过滤器。 |
FilenameFilter | 实现此接口的类实例可用于过滤器文件名。 |
Flushable | Flushable 是可刷新数据的目标地。 |
ObjectInput | ObjectInput 扩展 DataInput 接口以包含对象的读操作。 |
ObjectInputValidation | 允许验证图形中对象的回调接口。 |
ObjectOutput | ObjectOutput 扩展 DataOutput 接口以包含对象的写入操作。 |
ObjectStreamConstants | 写入 Object Serialization Stream 的常量。 |
Serializable | 类通过实现 java.io.Serializable 接口以启用其序列化功能。 |
2、类摘要
BufferedInputStream | BufferedInputStream 为另一个输入流添加一些功能,即缓冲输入以及支持 mark 和 reset 方法的能力。 |
BufferedOutputStream | 该类实现缓冲的输出流。 |
BufferedReader | 从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。 |
BufferedWriter | 将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。 |
ByteArrayInputStream | ByteArrayInputStream 包含一个内部缓冲区,该缓冲区包含从流中读取的字节。 |
ByteArrayOutputStream | 此类实现了一个输出流,其中的数据被写入一个 byte 数组。 |
CharArrayReader | 此类实现一个可用作字符输入流的字符缓冲区。 |
CharArrayWriter | 此类实现一个可用作 Writer 的字符缓冲区。 |
Console | 此类包含多个方法,可访问与当前 Java 虚拟机关联的基于字符的控制台设备(如果有)。 |
DataInputStream | 数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型。 |
DataOutputStream | 数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中。 |
File | 文件和目录路径名的抽象表示形式。 |
FileDescriptor | 文件描述符类的实例用作与基础机器有关的某种结构的不透明句柄,该结构表示开放文件、开放套接字或者字节的另一个源或接收者。 |
FileInputStream | FileInputStream 从文件系统中的某个文件中获得输入字节。 |
FileOutputStream | 文件输出流是用于将数据写入 File 或 FileDescriptor 的输出流。 |
FilePermission | 此类表示对文件和目录的访问。 |
FileReader | 用来读取字符文件的便捷类。 |
FileWriter | 用来写入字符文件的便捷类。 |
FilterInputStream | FilterInputStream 包含其他一些输入流,它将这些流用作其基本数据源,它可以直接传输数据或提供一些额外的功能。 |
FilterOutputStream | 此类是过滤输出流的所有类的超类。 |
FilterReader | 用于读取已过滤的字符流的抽象类。 |
FilterWriter | 用于写入已过滤的字符流的抽象类。 |
InputStream | 此抽象类是表示字节输入流]的所有类的超类。 |
InputStreamReader | InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。 |
LineNumberInputStream | **已过时。**此类错误假定字节能充分表示字符。 |
3、结构图
### 4、以文件读写为例
public static final String READ_UTF8_FILE_PATH = "D:\\input-utf8.txt";
public static final String READ_UNICODE_FILE_PATH = "D:\\input-unicode.txt";
public static final String WRITE_BYTES_FILE_PATH = "D:\\output-bytes.txt";
public static final String WRITE_CHARS_FILE_PATH = "D:\\output-char.txt";
1、字节流操作文件
/**
* 按照字节流的方式读取文件内容
*
* Step 1.根据文件路径,构建文件对象
* Step 2.创建输入流用来读取文件内容
* Step 3.创建字节数组来存放读取内容
* Step 4.关闭读取文件的输入流
*
* @return
*/
public void readFileByFileInputStream() {
System.out.println("=== readFileByFileInputStream Start ===");
// 构建文件对象
File inputFile = new File(READ_UTF8_FILE_PATH);
// 初始化输入流
InputStream inputStream = null;
try {
// 创建字节输入流
inputStream = new FileInputStream(inputFile);
// 读取到1KB字节数组中
//若是带有中文字符做好是按字符所占字节来读,不然可能会出现乱码
byte[] buffer = new byte[100];
// 每次读取的字节数
int readLength;
// 读取数据并放到buffer数组中
while ((readLength = inputStream.read(buffer)) != -1) {
// UTF-8为变长编码,一个汉字占3个字节
System.out.println("本次读取" + readLength + "个字节数据内容为:" + new String(buffer));
}
} catch (FileNotFoundException e) {
// 文件未找到时异常处理
e.printStackTrace();
} catch (IOException e) {
// 读取过程中,删除文件会出此异常
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
// 关闭流过程,也有可能出现异常
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("=== readFileByFileInputStream End ===");
}
/**
* 通过字节流的方式写入信息到文件
*
* Step 1.根据文件路径,构建文件对象
* Step 2.创建字节输出流写出信息到文件
* Step 3.构造待写出的内容,并转为字节数组
* Step 4.关闭读取文件的字符输出流
*/
public void writeFileByFileOutputStream() {
System.out.println("=== writeFileByFileOutputStream Start ===");
// 创建写出文件
File file = new File(WRITE_BYTES_FILE_PATH);
// 初始化字节输出流
OutputStream outputStream = null;
// 写出内容
String outInfo = "写出测试";
// 转成字节数组
byte[] byteArray = outInfo.getBytes();
try {
// 创建输出字节流
outputStream = new FileOutputStream(file);
outputStream.write(byteArray);
System.out.println("按照字节流成功写出内容:"+outInfo);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
// 关闭写出流时,注意抓异常
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("=== writeFileByFileOutputStream End ===");
}
/**
* 通过字节流的方式将文件内容拷贝到另一个文件中
*
* Step 1.根据文件路径,构建源文件对象
* Step 2.根据文件路径,构造目的文件对象
* Step 3.创建字节输入流从源文件中读取信息
* Step 4.将读入到内存的信息再写出到目的文件中
* Step 5.拷贝完成后关闭输入输出流
*/
public void copyFile() {
System.out.println("=== copyFile Start ===");
// 输入文件对象
File inFile = new File(READ_UTF8_FILE_PATH);
// 输出文件对象
File outFile = new File(WRITE_BYTES_FILE_PATH);
// 初始化输入流
InputStream inputStream = null;
// 初始化输出流
OutputStream outputStream = null;
try {
// 将输入流怼到输入文件,使程序内存与磁盘建立联系
inputStream = new FileInputStream(inFile);
// 将输出流怼到输出文件,使程序内存与磁盘建立联系
outputStream = new FileOutputStream(outFile);
while (true) {
// 读取信息到内存
int temp = inputStream.read();
// 拷贝完成
if (temp == -1) {
break;
}
// 将内容拷贝到输出文件中
outputStream.write(temp);
}
System.out.println("拷贝文件成功完成");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
// 关闭输入流异常后,也要保证输出流关闭
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
System.out.println("=== copyFile End ===");
}
2、字符流操作文件
/**
* 按照字符流的方式读取文件内容
*
* Step 1.根据文件路径,构建文件对象
* Step 2.创建字符输入流读取文件内容
* Step 3.创建字符数组来存放读取内容
* Step 4.关闭读取文件的字符输入流
*
* @return
*/
public void readFileByFileReader(){
System.out.println("=== readFileByFileReader Start ===");
// 根据路径拿到文件对象
File file = new File(READ_UTF8_FILE_PATH);
// 初始化字符输入流
Reader fileReader = null;
// 初始化存放读取内容的字符数组
char[] charArray = new char[100];
// 初始化一个字符
char once;
try {
fileReader = new FileReader(file);
// 一次读取一个数组长度的字符串
fileReader.read(charArray);
System.out.println(charArray);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileReader != null) {
try {
// 关闭流过程,也有可能出现异常
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("=== readFileByFileReader End ===");
}
/**
* 通过字符流的方式写入信息到文件
*
* Step 1.根据文件路径,构建文件对象
* Step 2.创建字符输出流写出信息到文件
* Step 3.构造待写出的内容,并转为字符数组
* Step 4.关闭读取文件的字符输出流
*/
public void writeFileByFileWriter(){
System.out.println("=== writeFileByFileWriter Start ===");
// 创建写出文件
File file = new File(WRITE_CHARS_FILE_PATH);
// 初始化字符输出流
Writer fileWriter = null;
String strInfo = "字符写出数据";
try {
// 创建输出字符流
fileWriter = new FileWriter(file);
// 写出内容
fileWriter.write(strInfo);
System.out.println("按照字符流成功写出内容:"+strInfo);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fileWriter != null){
try {
// 关闭写出流时,注意抓异常
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("=== writeFileByFileWriter End ===");
}
3、bufferedxxx缓冲流
==流的读写是比较耗时的操作,因此为了提高性能,==便有缓冲的这个概念(什么是缓冲?假如你是个搬砖工,你工头让你把1000块砖从A点运到B点,你可以一次拿一块砖从A点运到B点放下砖,这样你要来回跑1000次,大多数的时间开销在路上了;你还可以使用一辆小车,在A点装满一车的砖,然后运到B点放下砖,如果一车最多可以装500块,那么你来回两次便可以把这些砖运完。这里的小车便是那个缓冲),在java bio中使用缓冲一般有两种方式。==一种是自己申明一个缓冲数组,利用这个数组来提高读写效率;另一种方式是使用jdk提供的处理流BufferedXXX类。==下面我们分别演示不使用缓冲读写,使用自定义的缓冲读写,使用BufferedXXX缓冲读写一个文件。
1、无缓冲读写文件
/**
* 拷贝文件(方法一)
* @param src 被拷贝的文件
* @param dest 拷贝到的目的地
*/
public static void copyByFileStream(File src,File dest){
FileInputStream fis = null;
FileOutputStream fos = null;
long start = System.currentTimeMillis();
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
int b = 0;
while((b = fis.read()) != -1){//一个字节一个字节的读
fos.write(b);//一个字节一个字节的写
}
} catch (Exception e) {
e.printStackTrace();
} finally{
close(fis,fos);
}
System.out.println("使用FileOutputStream拷贝大小"+getSize(src)+"的文件未使用缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒");
}
2、自定义数组做缓冲读写文件
/**
* 拷贝文件(方法二)
* @param src 被拷贝的文件
* @param dest 拷贝到的目的地
* @param size 缓冲数组大小
*/
public static void copyByFileStream(File src,File dest,int size){
FileInputStream fis = null;
FileOutputStream fos = null;
long start = System.currentTimeMillis();
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
int b = 0;
byte[] buff = new byte[size];//定义一个缓冲数组
//读取一定量的数据(read返回值表示这次读了多少个数据)放入数组中
while((b = fis.read(buff)) != -1){
fos.write(buff,0,b);//一次将读入到数组中的有效数据(索引[0,b]范围的数据)写入输出流中
}
} catch (Exception e) {
e.printStackTrace();
} finally{
close(fos,fis);
}
System.out.println("使用FileOutputStream拷贝大小"+getSize(src)+"的文件使用了缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒,生成的目标文件大小为"+getSize(dest));
}
3、使用BufferedXXX类使用默认大小缓冲来读写文件
/**
* 拷贝文件(方法三)
* @param src
* @param dest
*/
public static void copyByBufferedStream(File src,File dest) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
long start = System.currentTimeMillis();
try{
bis = new BufferedInputStream(new FileInputStream(src));
bos = new BufferedOutputStream(new FileOutputStream(dest));
int b = 0;
while( (b = bis.read())!=-1){
bos.write(b);//使用BufferedXXX重写的write方法进行写入数据。该方法看似未缓冲实际做了缓冲处理
}
bos.flush();
}catch(IOException e){
e.printStackTrace();
}finally{
close(bis,bos);
}
System.out.println("使用BufferedXXXStream拷贝大小"+getSize(src)+"的文件使用了缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒");
}
4、使用BufferedXXX类自定义大小缓冲来读写文件
/**
* 拷贝文件(方法四)
* @param src 被拷贝的文件对象
* @param dest 拷贝目的地文件对象
* @param size 自定义缓冲区大小
*/
public static void copyByBufferedStream(File src,File dest,int size) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
long start = System.currentTimeMillis();
try{
bis = new BufferedInputStream(new FileInputStream(src));
bos = new BufferedOutputStream(new FileOutputStream(dest));
int b = 0;
byte[] buff = new byte[size];
while( (b = bis.read(buff))!=-1){//数据读入缓冲区
bos.write(buff,0,b);//将缓存区数据写入输出流中
}
bos.flush();
}catch(IOException e){
e.printStackTrace();
}finally{
close(bos,bis);
}
System.out.println("使用BufferedXXXStream拷贝大小"+getSize(src)+"的文件使用了缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒");
}
4、流close()和流flush()
我们一般在处理文件,一般是一边从输出流中读数据,然后将读出的部分进行处理,最后将处理好的数据写入到输出流中。那么要将一个文件完整的处理完,我们必须知道什么时候已经读到文件的末尾了。
一般来说可以根据read方法返回的值,如果返回了-1表示没有可读取的字节了。
另一种是使用available()方法查看还有多少可供读取的,当输入流每读一个字节,available()返回的值便减小1,这种模式很像游标的模式,但要注意的是available的适用场景是非阻塞读取,如本地文件读取,如果是网络io使用该方法,可能你拿到的值就不对了。
总的来说一般输入流提供的读取方法是可以获得文件是否结束的标志,比如流默认的read方法,根据返回值是否非负,比如PrintReader和BufferedReader的readLine()方法,根据返回数据是否非空。
4.1 关于flush()的问题
为什么缓冲输出流写数据结束需要调用flush方法?我们以BufferedOutputStream的write(int b)方法源码为例,源码如下:
public synchronized void write(int b) throws IOException {
if (count >= buf.length) {
flushBuffer();
}
buf[count++] = (byte)b;
}123456
可以得知,BufferedOutputStream在write时候,只有count >= buf.length,即缓冲区数据填满的时候才会自动调用flushBuffer()将缓冲区数据进行写入,也就是说如果缓冲区数据未满则将不会写入,这时我们需人为的调用flush()方法将未满的缓冲区数据进行写入,如下例,可以看出未flush和flush后的区别。
public static void copyByBufferedStream(File src,File dest,int size) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
long start = System.currentTimeMillis();
try{
bis = new BufferedInputStream(new FileInputStream(src));
bos = new BufferedOutputStream(new FileOutputStream(dest));
int b = 0;
byte[] buff = new byte[size];
while( (b = bis.read(buff))!=-1){
bos.write(buff,0,b);
}
System.out.println("拷贝大小"+getSize(src)+"的文件使用了缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒,未flush生成的目标文件大小为"+getSize(dest));
bos.flush();
System.out.println("拷贝大小"+getSize(src)+"的文件使用了缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒,flush后生成的目标文件大小为"+getSize(dest));
}catch(IOException e){
e.printStackTrace();
}
}
//测试
public static void main(String[] args) {
File src = new File("E:\\iotest\\1.bmp");
File dest = new File("E:\\iotest\\1_copy.bmp");
copyByBufferedStream(src, dest, 8192);
}
//测试结果:
拷贝大小864054字节的文件使用了缓冲数组耗时:3毫秒,未flush生成的目标文件大小为860160字节
拷贝大小864054字节的文件使用了缓冲数组耗时:3毫秒,flush后生成的目标文件大小为864054字节
从上面测试可以知道,如果未使用flush,带来的后果可能会造成部分数据丢失,为什么说是可能?因为如果文件大小刚好是缓冲区的整倍数,即最后一次写入的数据刚好填满缓冲区,write方法也会自动flushBuffer。另一种原因是,调用close方法后会自动将缓冲区的数据flush,我们看看close方法源码,由于BufferedOutputStream类中并没有重写close方法,因此我们去看看直接父类FilterOutputStream的close,源码如下:
public void close() throws IOException {
try (OutputStream ostream = out) {
flush();
}
}12345
从上可以看出,在调用close后,其实内部调用了flush(),因此我们在调用close后,数据也能保证数据完整写入,我们验证下我们刚才的分析:
public static void copyByBufferedStream(File src,File dest,int size) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
long start = System.currentTimeMillis();
try{
bis = new BufferedInputStream(new FileInputStream(src));
bos = new BufferedOutputStream(new FileOutputStream(dest));
int b = 0;
byte[] buff = new byte[size];
while( (b = bis.read(buff))!=-1){
bos.write(buff,0,b);
}
System.out.println("拷贝大小"+getSize(src)+"的文件使用了缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒,未flush,未调close生成的目标文件大小为"+getSize(dest));
}catch(IOException e){
e.printStackTrace();
}finally{
close(bos,bis);//一个工具方法,内部调用了输出流的close方法,文章后面部分可以看到该方法的源码
System.out.println("拷贝大小"+getSize(src)+"的文件使用了缓冲数组耗时:"+(System.currentTimeMillis()-start)+"毫秒,未flush,调用close后生成的目标文件大小为"+getSize(dest));
}
}
//测试
public static void main(String[] args) {
File src = new File("E:\\iotest\\1.bmp");
File dest = new File("E:\\iotest\\1_copy.bmp");
copyByBufferedStream(src, dest, 8192);
}
//测试结果如下:
拷贝大小864054字节的文件使用了缓冲数组耗时:5毫秒,未flush,未调close生成的目标文件大小为860160字节
拷贝大小864054字节的文件使用了缓冲数组耗时:6毫秒,未flush,调用close后生成的目标文件大小为864054字节
4.2 关于网络流中使用available()方法的问题
当你在网络io中,比如你用socket编程时获取到的流进行读写时,会发现使用available方法有问题,原因是网络io的特点是:
**1.非实时性。**你调用available()方法判断剩余流的大小时,远端数据可能还未发送,或者要发送的数据处于队列中,因此通过available()拿到的可用长度可能是0
**2.非连续性。**由于网络数据传输中,一般会分段多次发送,available仅仅能返回本次的可用长度。
鉴于以上两个特点,使用available判断网络io还有多少数据可读是不合适的,因此解决该问题一般采用自定义协议,比如文件大小,文件名等信息放入流的头几个字节中,接收方根据收到的头信息来解析出对法传送的文件大小,根据大小来判断还剩多少字节需要读取,是否读取完毕
4.3 有gc为什么还需要手动释放资源?
1)gc只能释放内存资源,而不能释放与内存无关资源。
2)gc回收具有不确定性,你根本不知道它什么时候会回收,而对于需要程序员手动回收的资源往往具有这样的特点:资源开销大,不用需要立即释放;或者资源是系统唯一的,不释放会导致别的程序也无法使用该资源。那对于具有这些特点的资源就必须保证不使用后能够立即释放出这部分资源,而不能把这件事情交给一个具有不确定性的gc来完成。
3)有人可能会说java IO流资源虽然不能被gc直接释放,但可以利用finalizer机制来释放非java资源,事实上java也确实在IO流的一些类中这么做了,如下:
/**
* 该段代码摘自FileInputStream源码,jdk版本1.8
*/
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
/* if fd is shared, the references in FileDescriptor
* will ensure that finalizer is only called when
* safe to do so. All references using the fd have
* become unreachable. We can call close()
*/
close();
}
}12345678910111213
但是请注意,这仅仅是api程序员的严谨,防止由于我们这些程序员的大意忘记手动close资源,这依旧不是我们不手动调用close方法释放资源的借口。
因为第一finalize的执行时机是在gc前,而gc具有时间不确定性,所以finalize执行时间也不具有确定性,对于需要及时回收的资源finalize无法保证及时。
第二,finalize不是析构函数,jvm也根本就不能保证finalize一定会执行,那么就更不能依赖finalizer机制释放资源了。
1.需手动释放的常见资源
1)java IO流资源
2)jdbc资源(Connection,PrepareStatement,ResultSet)
3)android中的bitmap资源
2资源关闭顺序问题
单个资源关闭往往没什么可说的,直接关闭即可,但在java很多类体系中往往存在依赖关系和资源装饰关系,这个时候就有关闭先后问题,否则还会引发异常
1)先开后关原则(栈原则)
整个模型像栈一样,先开的后关(先进后出)。这个原则很像生活中的一件事情,那就是使用燃气灶。使用燃气灶的时候我们都是先开气阀,再打开燃气灶的开关,做完饭我们则先关闭燃气灶开关,最后再关上气阀。(电脑主机和显示器的开关顺序也满足这个原则,开机先开显示器后开主机,关机先关主机后关显示器)
jdbc资源的开关顺序如下:
先打开Connection资源,再打开PrepareStatement资源,最后打开ResultSet资源。使用完毕后先调用ResultSet的close方法,再调用PrepareStatement的close方法,最后调用Connection的close方法。
java IO流资源的开关顺序如下:
我们一般先打开一个输入流进行读取操作,然后将读取的数据写入输出流中,关闭时按照上面的原则,则应该先关输出流,然后关闭输入流。但是,我们发现在java io流关闭操作中,即使顺序反了也不会出现异常。因为我们关闭流的时机是在读写完成之后,并且输出流和输入流一般用一个中间buff数组做数据传递关联,并不像jdbc中资源之间的强依赖关联,所以即使关闭一个,另一个并不受影响。
2)由外到内原则(洋葱原则)
如果资源存在包装嵌套关系,则先关闭外层,后关闭内层的。
java io流中,处理流装饰节点流,我们应该先关闭装饰流,再关闭节点流。原则上是这样,但是我们发现这样反而会出现程序异常,因为java api上已经帮我们做了这样的事情。就是在处理流的close方法中调用了节点流的close方法。因此对java io流资源,如果是处理流,我们只需要调用处理流的close方法即可
三、总结
- Bio工作模式的缺陷
(1)线程创建资源开销巨大。
如果有10000个连接怎么办?创建10000个线程?很明显不现实,太多的线程对系统资源的开销是巨大的。
(2)单个线程的资源浪费。
一个请求来了,如果这个线程从被创建为这个连接服务开始就一直在工工作,那么就不存在资源浪费,就像你找来一个服务员,他只服务一个客人,但他期间一直都有工作作,那么他的劳动力就算不上浪费。
但是绝大多数情况下客人多服务员的需求都是暂时的,服务员很多时候都在发呆玩手机,等待着回应客人的请求,这样这个服务员的劳动力就被浪费了。那么在Bio中线程等待服务的时候是什么样的呢,那就是阻塞,线程等待着为连接服务(读数据/写数据/业务处理),很多时候BIo的服务子线程都是在阻塞状态下的。
- Bio阻塞在哪了?
bio的阻塞,主要体现在两个地方,如代码中注释所示。
①若一个服务器启动就绪,那么主线程就一直在等待着客户端的连接,这个等待过程中主线程就一直在阻塞。
②在连接建立之后,一个子线程服务于这个连接,那么这个子线程提供的服务无非就是socket通信,在读取到socket信息之前,这个子线程也是一直在等待,一直处于阻塞的状态下的。
注:代码中采用了连接池,这样可以节约一些线程重复创建的开销,但是这样的优化作用是有限的。
注:参考文献