• 第2章 深入分析java I/O的工作机制(上)


    java的I/O操作类在包java.io下,大致分成4组:

      所有文件的存储都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再存储这些字节到磁盘。在读取文件时,也是一个字节一个字节读取。

      字节流可以用于任何类型的对象,包括二进制对象;而字符流只能处理字符或者字符串

      字节流是最基本的,所有的InputStrem和OutputStream的子类都是,主要用在处理二进制数据,它是按字节来处理的。

      但实际中很多的数据是文本,又提出了字符流的概念,它是按虚拟机的encode来处理,也就是要进行字符集的转化。

      字符和字节流这两个之间通过 InputStreamReader,OutputStreamWriter来关联,实际上是通过byte[]和String来关联 在实际开发中出现的汉字问题实际上都是在字符流和字节流之间转化不统一而造成的。

    • 基于字节操作的I/O接口:InputStream 和 OutputStream。处理的字节。
    • 基于字符操作的I/O接口:Writer 和 Reader。(一个字符占两个字节),字符流处理的单元是2个字节的Unicode字符。字符流是由JAVA虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成,所以它对多国语言支持比较好。如果是音频文件、图片、歌曲,就用字节流好点,如果是中文(文本),用字符流好点。
    • 基于磁盘操作的I/O接口:File。
    • 基于网络操作的I/O接口:Socket(不在java.io包下。)

    2.1 java的I/O类库的基本架构

      2.1.1 基于字节的I/O操作接口

        InputStream和OutputStream

        inputStream的read()返回int:0到255范围内的int字节值。如果到达末尾而没有可用的字节,返回-1.因此不能用于0-255

      来表示的值就得用字符流来读取。

        2.1.2 基于字符的I/O操作接口

        不管是磁盘还是网络传输,最小的存储单元是字节,而不是字符,所以I/O操作的都是字节而不是字符。

         但是为什么有操作字符的I/O接口呢?这是因为程序中经常操作的数据都是字符形式的,为了操作方便当然要提供一个直接写字符的I/O接口。

                 从字符到字节必须经过编码转换,而编码又耗时,编码还容易出现乱码。

            Writer类提供了一个抽象方法write(char cbuf[], int off, int len).

                 读字符的接口是int read(char cbuf[], int off, in len):  返回的int:作为整数读取的字符(16位,2个字节),范围是0到65535,如果已经到流的末尾,

       则返回-1

        2.1.3 字节与字符的转化接口

      数据持久化或网络传输都是以字节进行的,所以必须要有从字符到字节或从字节到字符的转化。

        InputStreamReader类:从字节到字符转化的桥梁, 从inputStream到reader的过程是指定编码字符集,否则将采用操作系统默认的字符集,很可能出现乱码问题。

        StreamDecoder正是完成从字节到字符的解码的实现类。

    补充:IO部分介绍不详细,此处补充一些IO的知识

    补充1:如果需要创建文件需要以下操作:判断映射的文件是否真实存在fie.existes()  ,为true就存在,否则不存在。

     如果文件不存在要调用file.createdNewFile() 创建真实文件,但是,这个方法只是会创建文件本身,如果文件的目录不存在,则不会创建目录。所以需要对父文件存在与否左判断。

      File parent = file.getParentFile()  // 获取父文件

      if(!parent.exists()) parent.mkdirs();  // 创建父文件夹。

    补充二:FileInputStream是有缓冲区的,所以用完之后必须关闭,否则可能导致内存占满,数据丢失。

    int count = 0;
            InputStream streamReder = null;
            try {
                streamReder = new FileInputStream(new File("folder1/fsdfsdf/fsdffd/fileName4.txt"));
                while(streamReder.read() != -1)  // 每次读一个字节,返回的是字节的int表示0 ~ 255
                {
                    count++;
                }
                System.out.println("---长度是: "+count+" 字节");

    补充三:

    OutputStream  out=new FileOutputStream("D:/David/Java/java 高级进阶/files/tiger2.jpg");

     out.write(buffer, 0, numberRead);       //否则会自动被填充0

    补充四:ObjectInputStream和ObjectOutputStream

    但是要实现这种功能,被读取和写入的类必须实现Serializable接口

    补充五:字符流

    FileWriter是被修饰者

    BufferedWriter 是修饰者

    BufferedWriter bw = new BufferedWriter(new FileWriter("file name"));

    上面这个BufferedWriter加了一个缓冲,缓冲写满后再将数据写入硬盘,这样极大的提高性能。必须要有bw.flush()这句,如果没有,而仅有writer.close(),会报异常。

    如果单独使用FileWriter也可以,每写一个数据,硬盘就有一个写动作,性能差。

    OutputStreamWriter write = new OutputStreamWriter(new FileOutputStream(f));
                BufferedWriter writer = new BufferedWriter(write);
                writer.write(content);
                writer.flush(); 
                write.close();
                writer.close();
    ///////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////

    FileReader fk=new FileReader("f:/23.txt"); //从F盘读取一个文件fk接收

    BufferedReader bk=new BufferedReader(fk); //生成一个容器bk把文件fk内容装进去,这样bk的内容就是原文件23.txt的内容了

    从定义上看可能会让你感到困惑,这里解释一下:输入输出是相对于内存设备而言的

    补充六:

      没有明确指定需要使用的字符编码方案时,Java程序通过“java.nio.charset.Charset.defaultCharset().name()”语句来获取默认的字符编码方案,该语句返回的值跟运行Java程序的操作系统的设置有关,在有些操作系统上,该语句返回值可能是UTF-8;在有些操作系统上,该语句返回值可能是GBK;在有些操作系统上,该语句返回值可能是除了UTF-8和GBK以外的其他字符编码方案。这样子,程序的可移植性大大降低。

      编码只发生在JVM和底层操作系统(以及网络传输)之间进行数据传输时,如果程序中没有IO操作,那么所有的String和Char都以unicode编码。当从磁盘读取文件或者往磁盘写入文件时使用的编码要一致,也就是编码和解码使用的字符集要一样才不会出现乱码

      Unicode :又称万国码,顾名思义,unicode中收录了世界各国语言,用以解决传统编码的局限性。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

      UTF-8:(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码.

    1、不同编码表示一个字符所占用字节是不相同的,其中ASCII码占一个字节,GB2312、GBK、unicode都用2字节表示一个字符。而UTF-8是可变长的,英文字母用1字节,汉字用3字节表示。

    2、同一个字符在不同编码表中的位置也是不同的,比如汉字‘中’在GBK中是D6D0,而在unicode中是4E2D。也就导致了当汉字的解码方式和编码方式不同时会产生乱码。

    每个Java程序员都应该记住,Java使用的是Unicode编码。所有的字符在JVM中(内存中)只有一个存在形式就是Unicode。所以一个char占用2字节。

      Java的IO体系中面向字符的IO类只有Reader和Writer但是最常用的FileReader和FileWriter类不支持自定义编码类型只能使用系统默认编码。这样一来,读写文件的编码就一定一致了,也就减少了乱码的可能性。个人理解,这么做可能是强制帮助用户完成编码一致,降低乱码率。如果要自定义编码,要用其父类InputStreamRreader和OutputStreamWriter

    补充七: JAVA中几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是什么?

           字节流和字符流。

           字节流和字符流区别?

           计算机中的一切最终都是二进制的字节形式存在。对于“中国”这些字符,首先要得到其对应的字节,然后将字节写入到输出流。读取时,首先读到的是字节,可是我们要把它显示为字符,我们需要将字节转换成字符。由于这样的需求很广泛,人家专门提供了字符流的包装类。

      底层设备永远只接受字节数据,有时候要写字符串到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,字符流则是直接接受字符串,它内部将串转成字节,再写入底层设备,这为我们向IO设别写入或读取字符串提供了一点点方便

      什么是JAVA序列化?如何实现序列化?

    补充八:Unicode 和 UTF-8区别

    String newStr = new String(oldStr.getBytes(), "UTF-8");

      java中的String类是按照unicode进行编码的,当使用String(byte[] bytes, String encoding)构造字符串时,encoding所指的是bytes中的数据是按照那种方式编码的,而不是最后产生的String是什么编码方式,换句话说,是让系统把bytes中的数据由encoding编码方式转换成unicode编码。如果不指明,bytes的编码方式将由jdk根据操作系统决定。  

    // 打开文件时,指定编码方式。

    InputStreamReader read = new InputStreamReader (new FileInputStream(f),"UTF-8");

      FileReader读取文件的过程中,FileReader继承了InputStreamReader,但并没有实现父类中带字符集参数的构造函数,所以FileReader只能按系统默认的字符集来解码。

      用InputStreamReader代替FileReader,InputStreamReader isr=new InputStreamReader(new FileInputStream(fileName),"UTF-8");这样读取文件就会直接用UTF-8解码,不用再做编码转换。

    // 指定编码方式
    BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(saveFilename),"GB2312"));
    
    PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFilename),"GB2312")));

    补充九:

    发现:所有的字节相关的都是以Stream为结尾; 所有的字符相关的都是以writer/reader为结尾。

    以字节为导向的stream------InputStream/OutputStream。

    InputStream 和 OutputStream是两个abstact类,对于字节为导向的stream都扩展这两个鸡肋(基类^_^);

     --InputStream

          ByteArrayInputStream - 把内存中的一个缓冲区作为InputStream使用。

          StringBufferInputStream -  把一个String对象作为InputStream(不推荐使用) .

          FileInputStream -- 把一个文件作为InputStream,实现对文件的读取操作

          PipedInputStream - 实现了pipe的概念,主要在线程中使用. 管道输入流是指一个通讯管道的接收端。

                            一个线程通过管道输出流发送数据,而另一个线程通过管道输入流读取数据,

                             这样可实现两个线程间的通讯。

         SequenceInputStream - 把多个InputStream合并为一个InputStream .“序列输入流”类允许应用程序把几个输入流连续地合并起来,

                                 并且使它们像单个输入流一样出现。每个输入流依次被读取,直到到达该流的末尾。

                                 然后“序列输入流”类关闭这个流并自动地切换到下一个输入流。

    --OutputSteam

       ByteArrayOutputStream - 把信息存入内存中的一个缓冲区中.该类实现一个以字节数组形式写入数据的输出流。

       FileOutputStream:文件输出流是向 File 或 FileDescriptor 输出数据的一个输出流。

       PipedOutputStream:管道输出流是指一个通讯管道的发送端。 一个线程通过管道输出流发送数据,

                                       而另一个线程通过管道输入流读取数据,这样可实现两个线程间的通讯。

    以字符为导向的stream Reader/Writer

    以Unicode字符为导向的stream,表示以Unicode字符为单位从stream中读取或往stream 中写入信息。
Reader/Writer 为abstact类
以Unicode字符为导向的stream包括下面几种类型:

    -- Reader

    1:    CharArrayReader:与ByteArrayInputStream对应

    2:    StringReader:与StringBufferInputStream对应

    3:   FileReader: 与FileInputStream对应

    4:    PipedReader: 与PipedInputStream对应

    -- Writer:

    1) CharArrayWrite:与ByteArrayOutputStream对应

    2) StringWrite:无与之对应的以字节为导向的stream

    3) FileWrite:与FileOutputStream对应

    4) PipedWrite:与PipedOutputStream对应

    补充十:字符流和字节流之间转

    InputStreamReader和OutputStreamReader:把一个以字节为导向的stream转换成一个以字符为导向的stream。

    一个 InputStreamReader 类是从字节流到字符流的桥梁:它读入字节,并根据指定的编码方式,将之转换为字符流。

    使用的编码方式可能由名称指定,或平台可接受的缺省编码方式。

    InputStreamReader 的 read() 方法之一的每次调用,可能促使从基本字节输入流中读取一个或多个字节。

    为了达到更高效率,考虑用 BufferedReader 封装 InputStreamReader,

    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    InputStreamReader(InputStream) 用缺省的字符编码方式,创建一个 InputStreamReader。

    InputStreamReader(InputStream, String) 用已命名的字符编码方式,创建一个 InputStreamReader。

    OutputStreamWriter 将多个字符写入到一个输出流,根据指定的字符编码将多个字符转换为字节。

    每个 OutputStreamWriter 合并它自己的 CharToByteConverter, 因而是从字符流到字节流的桥梁。

    补充十一:Java IO的一般使用原则

    一、按数据来源(去向)分类:

        1、是文件: FileInputStream, FileOutputStream, FileReader, FileWriter

        2、是byte[]:ByteArrayInputStream, ByteArrayOutputStream

        3、是Char[]: CharArrayReader, CharArrayWriter

        4、是String: StringBufferInputStream(不推荐), StringReader, StringWriter

        5、网络数据流:InputStream, OutputStream, Reader, Writer

     

    二、按是否格式化输出分:

           1、要格式化输出:PrintStream, PrintWriter

     

    三、按是否要缓冲分:

    1、要缓冲:BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter

     

    四、按数据格式分:

    1、二进制格式(只要不能确定是纯文本的): InputStream, OutputStream及其所有带Stream结束的子类

    2、纯文本格式(含纯英文与汉字或其他编码方式);Reader, Writer及其所有带Reader, Writer的子类

    五、按输入输出分:

    1、输入:Reader, InputStream类型的子类

    2、输出:Writer, OutputStream类型的子类

    六、特殊需要:

    1、从Stream到Reader,Writer的转换类:InputStreamReader, OutputStreamWriter

    2、对象输入输出:ObjectInputStream, ObjectOutputStream

    3、进程间通信:PipeInputStream, PipeOutputStream, PipeReader, PipeWriter

    4、合并输入:SequenceInputStream

    5、更特殊的需要:PushbackInputStream, PushbackReader, LineNumberInputStream, LineNumberReader

    补充十二:几个常用类的用法

    FileInputStream :在java中,可以使用InputStream对文件进行读取,就是字节流的输入。

    建立合适大小的byte数组,如果已知输入流的大小

      File f = new File("E:"+File.separator+"java2"+File.separator+"StreamDemo"+File.separator+"test.txt");   
            InputStream in = new FileInputStream(f);   
            byte b[]=new byte[(int)f.length()];     //创建合适文件大小的数组   
            in.read(b);    //读取文件中的内容到b[]数组   
            in.close();   

    如果不知输入流的大小,则肯定需要建立一个很大的byte数组,那么byte中很可能有空的内容,那么如何正确合适的将byte数组的中的内容输出:

      File f = new File("E:"+File.separator+"java2"+File.separator+"StreamDemo"+File.separator+"test.txt");   
            InputStream in = new FileInputStream(f);   
            byte b[] = new byte[1024];   
            int len = 0;   
            int temp=0;                   //所有读取的内容都使用temp接收   
            while((temp=in.read())!=-1){       //当没有读取完时,继续读取   
                b[len]=(byte)temp;   
                len++;   
            }   
            in.close();   

    FileOutputStream:

      try{
         // 构造一个要写的数据:
          byte[] data = "这个例子测试文件写".getBytes("GB2312");
          FileOutputStream fo = new FileOutputStream("MyOut1.data");
          fo.write(data);
          //fo.flush();      //如果OutputStream 的实现使用了缓存,这个方法用于清空缓存里的数据,并通知底层去进行实际的写操作
                             // FileOutputStream 没有使用缓存,因此这个方法调用与否在这个例子的运行结果没有影响。
          fo.close();     
         
        }catch(Exception e)
        {
          e.printStackTrace();
        }

    BufferedInputStream是带缓冲区的输入流,默认缓冲区大小是8M,能够减少访问磁盘的次数,提高文件读取性能;BufferedOutputStream是带缓冲区的输出流,能够提高文件的写入效率。BufferedInputStream与BufferedOutputStream分别是FilterInputStream类和FilterOutputStream类的子类,实现了装饰设计模式。

           OutputStream out =
    38                   new BufferedOutputStream(          //是一种装饰模式
    39                       new FileOutputStream(file), 16);
    40 
    41             // 将ArrayLetters数组的前10个字节写入到输出流中
    42             out.write(ArrayLetters, 0, 10);
    43             // 将“换行符
    ”写入到输出流中
    44             out.write('
    ');
    45 
    46             // TODO!
    47             //out.flush();
    51             out.close();

     字符流:但是最常用的FileReader和FileWriter类不支持自定义编码类型,只能使用系统默认编码。这样一来,读写文件的编码就一定一致了,也就减少了乱码的可能性。个人理解,这么做可能是强制帮助用户完成编码一致,降低乱码率。如果要自定义编码,要用其父类InputStreamRreader和OutputStreamWriter

    // 声明一个File对象 
       File file = new File("hellowolrd.txt"); 
       // 声明一个Write对象 
       Writer writer = null; 
       // 通过FileWriter类来实例化Writer类的对象并以追加的形式写入 
       writer = new FileWriter(file, true); 
       // 声明一个要写入的字符串 
       String str = "字符串形式写入Helloworld"; 
       // 写入文本文件中 
       writer.write(str); 
       // 刷新 
       writer.flush(); 
       // 关闭字符输出流 
       writer.close(); 
     
    
     // 声明一个File对象 
           File file = new File("hellowolrd.txt"); 
           // 声明一个Reader类的对象 
           Reader reader = null; 
           // 通过FileReader子类来实例化Reader对象 
           reader = new FileReader(file); 
           // 声明一个字符数组 
           char[] c = new char[1024]; 
    //     // 将内容输出 
    //     int len = reader.read(c); 
           //循环方式一个一个读 
           int len=0; 
           int temp=0; 
           while((temp=reader.read())!=-1){ 
               c[len]=(char)temp; 
           len++; 
           } 
     
           // 关闭输入流 
           reader.close(); 
     

    字符流读取、写入文件:

    package ioTest;
    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.OutputStreamWriter;
    import java.util.Scanner;
    
    public class CharRWStreamTest01 {
        public static void main(String[] args) {
            File file=new File("Text.txt");
            File file2=new File("Text1.txt");
            try {
                InputStream fis=new FileInputStream(file);        // 定义字节流
                InputStreamReader isr=new InputStreamReader(fis, "utf-8"); // 用字节流作为字符流的输入,但是要指定编码方式
    这里InputStreamReader是由字节流到字符流的桥梁
    BufferedReader br
    =new BufferedReader(isr); // 装饰模式 单缓存 OutputStream os=new FileOutputStream(file2,false); OutputStreamWriter osw=new OutputStreamWriter(os, "utf-8"); BufferedWriter bw=new BufferedWriter(osw); String ss; while((ss=br.readLine())!=null){ bw.write(ss); bw.write(" "); } bw.flush(); br.close(); isr.close(); fis.close(); bw.close(); osw.close(); os.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } 

    2.2 磁盘I/O工作机制

      2.2.1 几种访问文件的方式

      读取和写入文件IO,都调用OS提供的接口,磁盘由OS管理,应用程序要访问物理设备只能通过系统调用的方式来工作。读和写分别对应read() 和 write()。 而系统调用就涉及到内核空间地址和用户空间地址切换的问题,这是OS为了保护系统安全,把用户程序空间和内核空间隔离造成的。虽然保证了安全,但是必然存在数据需要从内核空间向用户空间复制的问题。

      磁盘I/O操耗时,数据从磁盘复制到内核空间,再复制到用户空间,非常缓慢。OS为了加速I/O操作,在内核空间使用缓存机制,将从磁盘读取的文件按照一定的组织方式缓存,如果用户程序访问的是同一段磁盘地址的空间数据,那么OS将从内核缓存中直接取出返回给用户程序,减少I/O响应时间。

        1 标准访问文件的方式

          读和写都是针对内核的高速缓存。

        2 直接I/O的方式

          直接访问磁盘数据。如在数据库管理系统中,由应用程序控制哪些热点数据需要缓存,而不是OS。对热点数据可以进行预加载。

        3 同步访问文件的方式

          数据的读取和写入是同步的,与标准文件的访问方式不同的是 只有数据被成功写到磁盘时才返回给应用程序成功标志。

        4 异步访问文件的方式

        5 内存映射的方式

      2.2.2  Java 访问磁盘文件

       如何将数据持久化到物理磁盘?

      2.2.3 Java 序列化技术

      将一个对象转化成一串二进制表示的字节数组,通过保存和转义这些字节数据来达到持久化的目的。

      需要持久化一个对象,对象必须继承java.io.Serializable接口。

      反序列化是相反的过程,将字节数组再重新构造成对象。反序列化时,必须有原始类作为模板,才能将对象还原。

      序列化总结:

    • 当父类继承Serializable接口时,所有子类都可以别序列化。
    • 子类实现了Serializable接口,父类没有,父类的属性不能序列化(不报错,数据会丢失),子类的属性可以序列化。
    • 如果序列化的属性也是对象,那么它也必须实现Serializable接口,否则报错。
    • 反序列化时,如果对象的属性有修改或删减,则修改部分的属性会丢失,但不会报错。
    • 反序列化时,如果SerialVersionUID被修改,则反序列化时会失败。

    SerialVersionUID的作用:

      简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)

    2.3 网络I/O工作机制

      2.3.1 TCP状态转化

      (1) CLOSED状态

      (2) Listen:Server端在等待连接的状态,Server为此要调用socket, bind, listen函数,进入这个状态。这称为应用程序被打开(等待客户端来连

                             接。)

      (3) SYN-SENT: 有客户端要建立连接,经过三次握手建立。

      (4) 关闭的时候:四次握手关闭。

      2.3.2 影响网络传输的因素

      网络带宽 、  传输距离、 TCP阻塞控制(TCP缓冲区大小)

      2.3.3 Java Socket的工作机制

      socket底层用的TCP/UDP协议。 socket通过绑定端口号,可以唯一确定一个主机上应用程序的通信链路

      2.3.4 建立通信链路

      当客户端要与服务器端通信时,客户端首先创建一个Socket实例,OS为这个socket创建一个没有使用过的本地端口号,并创建一个包含本地地址、远程地址和端口号的套接字,在socket构造函数正确返回之前,要进行TCP三次握手,握手完成后,socket实例对象被创建完成。

      服务器端将创建一个ServerSocket实例,创建ServerSocket比较简单,只要指定的端口号没有被占用,一般都成功(同时会bind指定端口号,开始listen)。之后调用accept()方法时, 进入阻塞状态,等待客户端请求。当有新的请求到来时,将为这个连接建立一个新的套接字数据结构,该套接字数据结构包含地址和端口信息。注意:这时服务端与之对应的socket实例并没有完成创建,要等到3次握手完成后,这个服务器端的socket才会返回。

      2.3.5 数据传输

      当连接创建完后,服务端和客户端都会拥有一个socket实例,每个socket实例都有一个inputstreamf和outputstream, 用这两个对象交换数据。

      网路传输的是字节流,当创建socket对象时,OS会为inputstream和outputstream分配缓冲区,数据读写通过这个缓冲区来完成的。

    2.4 NIO的工作方式

    对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞 与非阻塞 。所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止。而对于非阻塞状态, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待

      2.4.1 BIO带来的挑战

      BIO就是阻塞式I/O,无论是磁盘I/O还是网络I/O,数据在写入OutputStream或者从InputStream读取时都可能会阻塞,一旦有阻塞,线程会失去CPU的使用权,这在当前大规模访问量和有性能要求的情况下是不能接受的。虽然当前网络I/O有一些解决办法,如一个客户端对应一个处理线程,出现阻塞时只是影响一个线程,不影响其他线程,还有为了减少系统线程的开销,采用线程池的办法减少线程创建和回收的成本。

      但是在一些场景下仍然是无法解决的,比如一些需要大量HTTP长连接的情况,像淘宝现在使用的Web旺旺,服务端需要同时保持几百万HTTP连接,并不是每时每刻这些连接都在传输数据,在这种情况下不可能创建那么多的线程保持连接。

      就算线程数量不是问题,也存在如下问题:比如想给一些客户更高的优先级时,很难通过设计线程的优先级来完成;另外每个客户端的请求在服务端可能需要访问一些竞争资源,因为客户端在不同的线程,因此需要同步。

      2.4.2 NIO的工作机制

      2.4.3 Buffer的工作方式

      2.4.4 NIO的数据访问方式

        1.FileChannel.transferXXX

        2. FileChannel.map

    2.5 I/O调优

      2.5.1 磁盘I/O优化

        1. 性能检测

                   iostat :  linux 下查看 iO的一些参数,I/O wait参数不应该操过25%。

         iops:  每秒IO的读写次数。 是磁盘的性能指标。

               raid: 磁盘冗余阵列。 

        2. 提升I/O性能

          增加缓存,减少磁盘访问次数;

          优化磁盘管理系统,设计最优的磁盘方式策略,以及磁盘的寻址策略,这是在底层OS层面考虑的;

          设计合理的磁盘存储数据块,以及访问这些数据块的策略,这是从应用层面考虑。例如 我们可以给存放的数据设计索引,通过寻址索引加快和减少磁盘的访问量,还可以采用异步和非                         阻塞的方式加快磁盘的访问速度(具体方法比如,给数据库建索引,减少IO; NIO技术);

           应用合理的RAID策略提升磁盘IO;

       2.5.2 TCP网络参数调优  

           增大端口数目,  减少TIME_WAIT(可以快速释放请求) , 除此之外,还可以让TCP复用等。

         echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range设置向外连接可用端口范围 表示可以使用的端口为65535-1024个(0~1024为受保护的)

      echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse 设置time_wait连接重用 默认0

      echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle 设置快速回收time_wait连接 默认0

      echo 180000 > /proc/sys/net/ipv4/tcp_max_tw_buckets 设置最大time_wait连接长度 默认262144

      echo 1 > /proc/sys/net/ipv4/tcp_timestamps  设置是否启用比超时重发更精确的方法来启用对RTT的计算 默认0

      echo 1 > /proc/sys/net/ipv4/tcp_window_scaling 设置TCP/IP会话的滑动窗口大小是否可变 默认1

      echo 20000 > /proc/sys/net/ipv4/tcp_max_syn_backlog 设置最大处于等待客户端没有应答的连接数 默认2048

      echo 15 > /proc/sys/net/ipv4/tcp_fin_timeout  设置FIN-WAIT状态等待回收时间 默认60

      echo "4096 87380 16777216" > /proc/sys/net/ipv4/tcp_rmem  设置最大TCP数据发送缓冲大小,分别为最小、默认和最大值  默认4096    87380   4194304

      echo "4096 65536 16777216" > /proc/sys/net/ipv4/tcp_wmem 设置最大TCP数据 接受缓冲大小,分别为最小、默认和最大值  默认4096    87380   4194304

      echo 10000 > /proc/sys/net/core/somaxconn  设置每一个处于监听状态的端口的监听队列的长度 默认128

      echo 10000 > /proc/sys/net/core/netdev_max_backlog 设置最大等待cpu处理的包的数目 默认1000

      echo 16777216 > /proc/sys/net/core/rmem_max 设置最大的系统套接字数据接受缓冲大小 默认124928

      echo 262144 > /proc/sys/net/core/rmem_default  设置默认的系统套接字数据接受缓冲大小 默认124928

      echo 16777216 > /proc/sys/net/core/wmem_max  设置最大的系统套接字数据发送缓冲大小 默认124928

      echo 262144 > /proc/sys/net/core/wmem_default  设置默认的系统套接字数据发送缓冲大小 默认124928

      echo 2000000 > /proc/sys/fs/file-max 设置最大打开文件数 默认385583

      结合ab命令来压测机器优化网络

      设置完记得保存

       2.5.3 网络I/O优化

        减少网络交互的次数:减少交互次数通常是在网络两端设置缓存,如Oracle的JDBC驱动程序就提供了对SQL结果的缓存,有效减少对数据库的访问。

                                                        还有一招,合并访问请求,比如查十回,每次查一个;也可以一次查10个;

                   访问的静态资源比如JS,CSS,可已经多个JS文件名称合并在一个HTTP请求中,发送web后端,后端根据URL 把JS文件打包一并返回给前端;

        减少网络传输数据量的大小:数据压缩在传输;

                                                             尽量通过读取协议头来获取有用信息,尽量避免读取通信数据body来获得需要的信息。           

        尽量减少编码:减少字节和字符的转。

        1. 同步与异步: 异步无法保证依赖,需要在可靠性和性能之间做平衡(做五星的时候,遇到过这个问题,加购物车,扣库存。)

        2. 阻塞与非阻塞:主要从CPU消耗上来说的,阻塞就是CPU停下来等待一个慢的操作;非阻塞就是慢操作执行时,CPU去做其他事;

                 非阻塞的方式虽然提高CPU利用率  但是线程切换增加;

        3. 两种方式的组合:

    2.6 设计模式解析之适配器模式

      2.6.1 适配器模式的结构

      2.6.2 Java I/O中的适配器模式

    2.7 设计模式之装饰器模式

      2.7.1 装饰器模式的结构

      2.7.2 Java  I/O中的装饰器模式

    2.8 适配器模式与装饰器模式的区别

      

       

  • 相关阅读:
    JAVA for(i = 0; i<a.length; i++) 解析
    3.2.2多维数组 3.3 排序
    3.2数组
    字符串和数组
    2.7.3与程序转移有关的跳转语句
    2.7.2 循环语句
    读书共享 Primer Plus C-part 4
    Linux 批量修改文件名
    关于/usr/local/lib/libz.a(zutil.o): relocation R_X86_64_32 against `.rodata.str1.1' can not be used when making a shared object; recompile with -fPIC解决办法
    做一个有深度的程序猿
  • 原文地址:https://www.cnblogs.com/liufei1983/p/7280054.html
Copyright © 2020-2023  润新知