深入分析Java Web技术内幕-2.深入分析Java I/O的工作机制

1 Java的I/O类库的基本架构

1.基于字节操作的I/O接口:InputStream和OutputStream
2.基于字符操作的I/O接口:Reader和Writer
3.基于磁盘操作的I/O接口:File
4.基于网络操作的I/O接口:Socket
前两者是数据传输格式,后两者是数据传输方式,数据传输格式与数据传输方式是影响效率的最关键因素。

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

InputStream和OutPutStream的类层次结构:
在这里插入图片描述
在这里插入图片描述
有两点注意事项:
1.操作数据的方式是可以组合使用的,即装饰者模式:

OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream("fileName")));

2.必须要指定流最终写到什么地方,比如:磁盘、网络等。

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

在这里插入图片描述
在这里插入图片描述
注意:不管是Writer还是Reader类,都只是定义了读取或写入的数据字符的方式,但是没有规定数据要写到哪,这些后面会具体讨论。

字节和字符的转换接口

从字节到字符的解码:
在这里插入图片描述
InputStreamReader:实现从字节到字符的转化,从InputStram到Reader的过程中要指定编码字符集,而这一工作由StreamDecoder来实现。
写入通过OutPutStreamWriter来完成从字符到字节的编码过程。
在这里插入图片描述

2 磁盘I/O工作机制

几种访问文件的方式

I/O操作需要用到操作系统提供的接口,因为磁盘设备是操作系统管理的。操作系统为了安全将内核地址空间用户地址空间隔离开,如此以来,必然存在数据可能需要从磁盘复制到内核地址空间,再从内核地址空间向用户空间复制的问题,这将非常缓慢。操作系统为了加速I/O访问,在内核空间使用缓存机制——将从磁盘读取的文件按照一定的组织方式进行缓存,如果与用户程序访问的是同一段磁盘地址的空间数据,那么操作系统从内缓存中直接取出返回给用户程序,以减少I/O的响应时间。(操作系统引入缓存机制加速磁盘IO的访问)

标准访问文件的方式

读:程序调用操作系统的read()接口,若内核空间中有相应的缓存数据,则直接从缓存中返回,若没有,则从磁盘中读取并缓存在操作系统缓存中。
写:程序调用write()接口将数据写入内核地址空间的缓存中,写操作完成。操作系统决定什么时候写到磁盘中。除非显式的调用了sync同步命令。
在这里插入图片描述

直接I/O的方式

应用程序直接访问磁盘数据,不经过内核数据缓冲区。
使用:数据库管理系统,系统明确地知道应该加载哪些数据、失效哪些数据、对热点数据预加载(操作系统却不知道)。
优点:减少一次数据从内核空间到应用空间的访问,提高访问效率
缺点:若访问的数据不在应用缓存中,那么数据会从磁盘直接加载,非常缓慢。
通常将直接I/O与异步I/O结合使用
在这里插入图片描述

同步访问文件的方式

同步访问就是数据的读写都是同步的,数据成功被写入磁盘时才返回给应用程序成功的标志。而标准访问方式读是同步的,写是异步的,数据写入内核地址空间缓冲区就已经算成功。
特点:性能比较差,对数据安全性比较高的场景中使用,硬件都是定制的。
在这里插入图片描述

异步访问文件的方式

当处理数据的线程发出请求时,线程接着去处理其他事情而不是阻塞等待,请求的数据返回后接着进行下面的操作。
特点:提高应用程序的效率,但不会提高访问文件的效率。
在这里插入图片描述

内存映射的方式

内存映射的方式是指操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要求访问内存中的一段数据时,转换为访问文件的某一段数据。这种方式的目的同样是减少数据从内核空间缓存到用户空间缓存的数据复制操作,因为这两个空间的数据是共享的。
在这里插入图片描述

Java访问磁盘文件

有两点注意:
1、数据在磁盘中的唯一最小描述就是文件,文件也是操作系统和磁盘驱动器交互的最小单元。
2、File代表一个虚拟的对象,而不是真实的文件对象,真正使用到这个文件时,才会检查这个文件存在不存在。

public class JavaWebTest {
    @Test
    public void readFileTest(){
        //创建File,也就是指定文件的路径跟名称,但是不会去检查是否存在
        File file = new File("C:/file.txt");
        try {
            //这里读取文件内容,如果文件不存在,会抛出异常
            FileReader fileReader = new FileReader(file);
            StringBuffer stringBuffer = new StringBuffer();
            char[] chars = new char[1024];
            int len = 0;
            //这里就将读取到的文件内容存入chars缓存区中
            while ((len = fileReader.read(chars))>0){
                stringBuffer.append(chars,0,len);
            }
            System.out.println("文件中的内容是:"+stringBuffer.toString());
        } catch (FileNotFoundException e) {
            System.out.println("文件不存在");
        } catch (IOException e) {
            System.out.println("文件读取错误");
        }
    }
}

Java序列化技术

Java序列化是将一个对象转换成一串二进制表示的字节数组,通过保存或转移这些数据来达到持久化的目的。
Java的反序列化使用序列化生成的.dat文件以及一个类模板将字节数组重新构造成对象。
下面描述一些复杂情况:
1.当父类继承Serializable接口时,所有子类都可以被序列化。
2.子类实现了Serializable接口,父类没有时,父类中的属性不能序列化。
3.序列化的属性是对象,那么这个对象也必须实现Serializable接口。
4.反序列化时,若对象属性修改或删减,那么修改的部分丢失,不报错。
5.反序列化时,若serialVersionUID被修改,则反序列化失败。
多语言情况使用通用数据结构:
Java序列化在Java环境下可以很好的工作,但在多语言环境下,用Java序列化存储后,很难用其他语言还原出结果,这种情况下要尽量存储通用的数据结构,如JSON或者XML结构。

3 网络I/O工作机制

两台主机交互时,首先要有相互沟通的意向,其次要有沟通的渠道(物理链路),再次要有一个通讯协议。

TCP状态转化

三次握手

所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:
在这里插入图片描述
第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。 从客户端的视角来看,我接到了服务端发送过来的响应数据包,说明服务端接收到了我在第一次握手时发送的网络包,并且成功发送了响应数据包,这就说明,服务端的接收、发送能力正常。而另一方面,我收到了服务端的响应数据包,说明我第一次发送的网络包成功到达服务端,这样,我自己的发送和接收能力也是正常的。
第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力,服务端的发送、接收能力是正常的。 第一、二次握手后,服务端并不知道客户端的接收能力以及自己的发送能力是否正常。而在第三次握手时,服务端收到了客户端对第二次握手作的回应。从服务端的角度,我在第二次握手时的响应数据发送出去了,客户端接收到了。所以,我的发送能力是正常的。而客户端的接收能力也是正常的。
经历了上面的三次握手过程,客户端和服务端都确认了自己的接收、发送能力是正常的。之后就可以正常通信了。

四次挥手

在这里插入图片描述
第一次挥手:A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据,主动关闭 TCP 连接。A 把连接释放报文段首部的终止控制位 FIN 置 1,其序号 seq = u(等于前面已传送过的数据的最后一个字节的序号加 1),这时 A 进入 FIN-WAIT-1(终止等待1)状态,等待 B 的确认。请注意:TCP 规定,FIN 报文段即使不携带数据,也将消耗掉一个序号。
第二次挥手:B 收到连接释放报文段后立即发出确认,确认号是 ack = u + 1,而这个报文段自己的序号是 v(等于 B 前面已经传送过的数据的最后一个字节的序号加1),然后 B 就进入 CLOSE-WAIT(关闭等待)状态。TCP 服务端进程这时应通知高层应用进程,因而从 A 到 B 这个方向的连接就释放了,这时的 TCP 连接处于半关闭(half-close)状态,即 A 已经没有数据要发送了,但 B 若发送数据,A 仍要接收。也就是说,从 B 到 A 这个方向的连接并未关闭,这个状态可能会持续一段时间。A 收到来自 B 的确认后,就进入 FIN-WAIT-2(终止等待2)状态,等待 B 发出的连接释放报文段。
第三次挥手:若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接。这时 B 发出的连接释放报文段必须使 FIN = 1。假定 B 的序号为 w(在半关闭状态,B 可能又发送了一些数据)。B 还必须重复上次已发送过的确认号 ack = u + 1。这时 B 就进入 LAST-ACK(最后确认)状态,等待 A 的确认。
第四次挥手:A 在收到 B 的连接释放报文后,必须对此发出确认。在确认报文段中把 ACK 置 1,确认号 ack = w + 1,而自己的序号 seq = u + 1(前面发送的 FIN 报文段要消耗一个序号)。然后进入 TIME-WAIT(时间等待) 状态。请注意,现在 TCP 连接还没有释放掉。必须经过时间等待计时器设置的时间 2MSL(MSL:最长报文段寿命)后,A 才能进入到 CLOSED 状态,然后撤销传输控制块,结束这次 TCP 连接。 B一收到 A 的确认就进入 CLOSED 状态,然后撤销传输控制块。所以在释放连接时,B 结束 TCP 连接的时间要早于 A。

为什么 TIME-WAIT 状态必须等待 2MSL 的时间呢?
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,它是任何报文在网络上存活的最长时间,超过这个时间报文将被丢弃。而2MSL的意思就是2倍的MSL的意思。
假设第四次挥手时数据丢失,那么服务器就会一直收不到客户端的回应,因此服务器会重传第三次挥手的报文段,所以客户端不能直接进入CLOSE,而是要保持TIME_WAIT。

影响网络传输的因素

  • 网络带宽:一条物理链路在1s内能够传输的最大比特数。
  • 传输距离:数据在光纤中走的距离,光的传播速度很快,但数据在光纤中不是走直线的,速度大概是光的2/3。这个时间是我们通常说的网络延时。
  • TCP拥塞控制:TCP通过设置一个窗口的大小来保证传输方和接收方的步调一致,窗口大小由带宽和响应时间决定。计算公式是带宽*响应时间,通过这个值可以得到理论最优的TCP缓冲区大小。Linux系统已经可以自动调整这个大小。

Java Socket的工作机制

Socket描述的是计算机之间完成相互通信的一种抽象功能。
在这里插入图片描述

建立通信链路

public class MyServer {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(7777, 10);
            while (true){
                //这里底层就在进行三次握手,如果握手失败就会抛出异常
                Socket socket = serverSocket.accept();

                InputStream inputStream = socket.getInputStream();
                DataInputStream dataInputStream = new DataInputStream(inputStream);
                Thread.sleep(1000);
                String recvString = dataInputStream.readUTF();
                System.out.println("客户端接收到:"+ recvString);
                //发送
                DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
                String sendString = "服务端向客户端发起了一条对话 ";
                dataOutputStream.writeUTF(sendString);
                System.out.println(sendString);
                socket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class MyClient {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            createSocket(i);
        }
    }

    private static void createSocket(int num) {
        try {
            //这里底层就在进行三次握手,如果握手失败就会抛出异常
            Socket socket = new Socket("localhost", 7777);
            OutputStream outputStream = socket.getOutputStream();
            DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
            String sendString = "客户端"+num+"向服务器发送一条消息";
            dataOutputStream.writeUTF(sendString);
            System.out.println("发送给服务器:"+sendString);
            //接收
            InputStream inputStream = socket.getInputStream();
            DataInputStream dataInputStream = new DataInputStream(inputStream);
            String recvString = dataInputStream.readUTF();
            System.out.println("从服务器收到"+recvString);
            socket.close();

        } catch (IOException e) {
            System.out.println("连接失败,地址错误或服务器拒绝连接");
        }
    }
}

先运行服务端,然后运行客户端,可以看到两者之间通信建立。
在这里插入图片描述

问题:

高并发
比如,当成千上万个客户端同时发起连接请求,而服务端一次只能处理一个请求,这样会造成客户端排队时间过长,带来不良的访问体验。虽然可以让服务器派发不同的线程来处理Socket,但是又会导致线程一直被连接占用,如果发送的内容又不多时,就会造成线程资源的浪费,还是不能满足连接数过多的要求,因为线程可能被占用而无法释放,为其他的连接服务。当然,可以考虑使用线程池来减少线程创建和回收的成本,这是我们所说的伪异步IO,但当连接时长连接的时候仍然无法从根本上解决问题。

阻塞和死锁
每个Socekt都有一个InputStream和一个OutputStream。当创建Socket对象时,系统会为这两个流创建缓冲区SendQ队列和RecvQ队列。数据的读写都是通过缓冲区完成。当发送消息时,写入端数据通过OutoutStream写到sendQ中,队列满时数据被转移到recvQ中,recvQ如果满了,那么不能继续向sendQ中写入,直到RecvQ有足够空间接收sendQ的数据,因此发生阻塞。缓冲区的大小以及读写端的速度非常影响数据传输效率。因为阻塞的存在,所以同时发送数据时可能出现死锁。

NIO

Channel和Selector它们是NIO的两个核心概念,Channel要比Socket更加具体,它代表每一个通信信道,Selector它可以轮询每个Channel的状态,还有一个Buffer类,我们可以通过Buffer来控制数据的传输。

public void selector() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Selector selector = Selector.open();//调用Selector的静态工厂创建一个选择器
        ServerSocketChannel ssc = ServerSocketChannel.open();//创建一个服务端的Channel
        ssc.configureBlocking(false);//设置为非阻塞方式
        ssc.socket().bind(new InetSocketAddress(8080));//将服务端的Channel绑定到一个Socket对象
        ssc.register(selector, SelectionKey.OP_ACCEPT);//注册监听的事件,将Channel注册到选择器上
        while (true) {//无限循环,保持监听状态
            Set<SelectionKey> keys = selector.keys();//取得所有的key集合
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = (SelectionKey) iterator.next();
                if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
                    ServerSocketChannel ssc2 = (ServerSocketChannel) key.channel();//获取这个key所代表的通信信道对象
                    SocketChannel sc = ssc2.accept();//服务端接受请求
                    sc.configureBlocking(false);
                    sc.register(selector, SelectionKey.OP_READ);
                    iterator.remove();
                } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
                    SocketChannel sc = (SocketChannel) key.channel();
                    while (true) {
                        buffer.clear();//将缓冲区的索引状态重置为初始位置
                        int a = sc.read(buffer);//读取数据到buffer
                        if (a <= 0) {//数据读取完毕,跳出循环
                            break;
                        }
                        //将缓存字节数组的指针设置为数组的开始序列即数组下标0,这样就可以从buffer开头,
                        //对该buffer进行读取了,最多只能读取之前写入的数据长度,而不是整个缓冲的容量大小,
                        //如果没有这个方法,就是从buffer最后开始读取,读出来的都是byte=0时候的字符。
                        buffer.flip();
                    }
                    iterator.remove();
                }
            }
        }
    }

注意:这里我们是将Server端的监听连接请求的事件和处理请求的事件放在一个线程中,但是在应用中,我们通常会把它们放在两个线程中,一个线程专门负责监听客户端的连接请求,而且是以阻塞方式执行的;另外一个线程专门负责处理请求,这个专门负责处理请求的线程才会真正采用NIO的方式。
Selector可以监听一组Channel上的I/O状态,前提是这些Channel已经注册到Selector中,Selector可以调用select()检查已经注册的通信信道上I/O是否已经准备好,如果没有通信信道状态发生变化,那么select方法会阻塞等待或在超时后返回0,如果多个信道有数据,那么它将会把这些数据分配到对应的Buffer中。所以NIO的关键是有一个线程来处理所有连接的数据交互,而每个连接的数据交互都不是阻塞方式,因此可以同时处理大量的连接请求。

Buffer的工作方式

可以把Buffer简单地理解为一组基本数据类型的元素列表,它通过几个变量来保存这个数据的当前位置状态:capacity, position, limit, mark:

索引说明
capacity缓冲区数组的总长度
position下一个要操作的数据元素的位置
position下一个要操作的数据元素的位置
mark用于记录当前position的前一个位置或者默认是-1

在这里插入图片描述
举例:我们通过ByteBuffer.allocate(11)方法创建了一个11个byte的数组的缓冲区,初始状态如上图,position的位置为0,capacity和limit默认都是数组长度。当我们写入5个字节时,变化如下图:
在这里插入图片描述
这时我们需要将缓冲区中的5个字节数据写入Channel的通信信道,所以我们调用ByteBuffer.flip()方法,变化如下图所示(position设回0,并将limit设成之前的position的值):
在这里插入图片描述
这时底层操作系统就可以从缓冲区中正确读取这个5个字节数据并发送出去了。在下一次写数据之前我们再调用clear()方法,缓冲区的索引位置又回到了初始位置。

调用clear()方法:position将被设回0,limit设置成capacity,这些标记告诉我们可以从哪里开始往Buffer里写数据。如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
调用mark()方法:它将记录当前position的上一次位置,之后可以通过调用reset()方法恢复到这个position。
调用rewind()方法:它可以将position设回0,所以你可以重读Buffer中的所有数据,limit保持不变,仍然表示能从Buffer中读取多少个元素。

NIO的数据访问方式

NIO提供了比传统的文件访问方式更好的方法,NIO有两个优化方法:一个是FileChannel.transferToFileChannel.transferFrom;另一个是FileChannel.map
① FileChannel.transferXXX与传统的访问文件方式相比可以减少数据从内核到用户空间的复制,数据直接在内核空间中移动,在Linux中使用sendfile系统调用。
② FileChannel.map将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时将直接操作这个文件数据,这种方式省去了数据从内核空间向用户空间复制的损耗。这种方式适合对大文件的只读性操作,如大文件的MD5校验。但是这个种方式是和操作系统底层I/O实现相关的。
在这里插入图片描述

I/O调优

磁盘I/O优化

① 增加缓存,减少磁盘访问次数;
② 优化磁盘的管理系统,设计最优的磁盘方式策略、磁盘寻址策略;
③ 设计合理的磁盘存储数据块,以及访问这些数据块的策略,比如我们可以给存放的数据设计索引,通过寻址索引来加快和减少磁盘的访问量,还可以采用异步和非阻塞的方式加快磁盘的访问速度;
④ 应用合理的RAID策略提升磁盘I/O【RAID:将不同的磁盘组合起来以提高I/O性能】

TCP网络参数调优

要能够建立一个TCP连接,必须知道对方的IP和一个未被使用的端口号,由于32位的操作系统的端口号通常由两个字节来表示,也就是只有2^16=65535个,所以一台主机能够同时建立的连接数是有限的。在Linux中可以通过查看/proc/sys/net/ipv4/ip_local_port_range文件来知道当前这个主机可以使用的端口范围。
在这里插入图片描述
如果可以分配的端口号偏少,遇到大量并发请求时就会成为瓶颈,由于端口有限导致大量请求等待建立链接,这样性能就压不上去。如果发现有大量的TIME_WAIT时,可以设置/proc/sys/net/ipv4/tcp_fin_timeout为更小的值来快速释放请求。可以使用netstat -n | awk '/^tcp/{++state[$NF]} END {for(key in state) print key,"\t",state[key]}'来查看网络连接情况。
在这里插入图片描述
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

注意,以上设置都是临时性的,系统重启后就会丢失。
另外,Linux还提供了一些工具用于查看当前的TCP统计信息:
▶ cat /proc/net/netstat:查看TCP的统计信息;
▶ cat /proc/net/snmp:查看当前系统的连接情况;
▶ netstat -s:查看网络的统计信息。

网络I/O优化

① 减少网络交互的次数。
通常需要在网络交互的两端设置缓存,如Oracle的JDBC就提供了对查询结果的缓存,在客户端和服务器端都有,可以有效减少对数据库的访问。除了设置缓存还可以合并访问请求,比如在查询数据库时,我们要查询10个ID,可以每次查一个ID,也可以一次查10个ID。再比如,在访问一个页面进通常会有多个JS和CSS文件,我们可以将多个JS文件合并在一个HTTP链接中,每个文件用逗号隔开,然后发送到后端的Web服务器。

② 减少网络传输数据量的大小。
通常的办法是将数据压缩后再传输,比如在HTTP请求中,通常Web服务器将请求的Web页面gzip压缩后再传输给浏览器。还有就是通过设计简单的协议,尽量通过读取有用的协议头来获取有价值的信息,比如在设计代理程序时,4层代理和7层代理都是在尽量避免读取整个通信数据来获取所需要的信息。

③ 尽量减少编码。
在网络传输中数据都是以字节形式进行传输的,但是我们要发送的数据都是字符形式的,从字符到字节必须编码,但是这个编码过程是比较费时的,所以在经过网络I/O传输时,尽量直接以字节形式发送。

④ 根据应用场景设计合适的交互方式。
a. 同步与异步
同步就是一个任务的完成需要依赖另一个任务时,只有等待被依赖的任务完成后,依赖的任务才能完成,这是一种可靠的任务序列,要成功都成功,要失败都失败。而异步不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,只要自己完成了整个任务就算完成了,所以它是不可靠的任务序列,比如打电话和发信息。同步能够保证程序的可靠性,而异步可以提升程序的性能。

b. 阻塞与非阻塞
阻塞就是CPU停下来等待一个慢的操作完成后,CPU才接着完成其它的工作,非阻塞就是在这个慢的操作执行时,CPU去做其它工作,等这个慢的操作完成时,CPU在完成后续的操作。虽然非阻塞的方式可以明显提高CPU的利用率,但是也可能有不好的效果,就是系统的线程切换会比较频繁。

c. 两种方式的组合
组合的方式有四种,分别是:同步阻塞、异步阻塞、同步非阻塞、异步非阻塞,这四种方式对I/O性能都有影响:
注意:虽然异步和非阻塞能够提升I/O的性能,但是也会带来一些额外的性能成本,比如会增加线程数量从而增加CPU的消耗,同时也会导致程序设计复杂度的上升,如果设计的不合理,反而会导致性能下降。

设计模式

适配器模式

适配器就是把一个类的接口变换成客户端所能接受的另一种接口,从而使两个接口不匹配而无法在一起工作的两个类能够在一起工作。通常被用于在一个项目需要引用一些开源框架在一起工作的情况下,这些框架的内部都有一些关于环境信息的接口,需要从外部传入,但是外部的接口不一定能够匹配,在这种情况下,就需要适配器模式来转换接口。
在这里插入图片描述
Target(目标接口):客户端期待的接口;
Adaptee(源接口):需要被适配的接口;
Adapter(适配器):将源接口适配成目标接口,继承源接口,实现目标接口。

Java I/O中的适配器模式
InputStreamReader和OutputStreamWriter类分别继承了Reader和Writer接口,但是要创建它们的对象必须在构造函数中传入一个InputStream和OutputStream的实例。InputStreamReader实现了Reader接口,并且持有了InputStream的引用,这里是通过StreamDecoder类间接持有的,因为从byte到char需要经过编码。很显然,适配器就是InputStreamReader类,源接口就是InputStream,目标接口就是Reader类。OutputStreamWriter也是类似的方式。

装饰器模式

装饰器模式,顾名思义,就是将某个类重新装扮一下,让它的功能变得更加强大,但是作为原来这个类的使用者,还不应该感受到装饰前和装饰后有什么不同,否则就破坏了原有类的结构,所以装饰器模式要做到对被装饰类的使用者透明,这是对装饰器模式的一个要求。
在这里插入图片描述
Component:抽象组件角色,定义一组抽象的接口,规定这个被装饰组件都有哪些功能;
ConcreteComponent:实现这个抽象组件的所有功能;
Decorator:装饰器角色,它持有一个Component对象实例的引用,定义一个与抽象组件一致的接口;
ConcreteDecorator:具体的装饰器实现者,负责实现装饰器角色定义的功能。

Java I/O中的装饰器模式
在这里插入图片描述
以FileInputStream为例,InputStream类就是以抽象组件存在的,而FileInputStream就是具体组件,它实现了抽象组件的所有功能。FilterInputStream类无疑就是装饰角色,它实现了InputStream类的所有功能,并且持有InputStream对象实例的引用。BufferedInputStream是具体的装饰器实现者,它给InputStream类附加了功能。这个装饰器类的作用就是使得InputStream读取的数据保存在内存中,从而提高读取的性能。与这个装饰器类有类似功能的是LineNumberInputStream类,它的作用就是提高按行读取数据的功能。

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

适配器模式和装饰器模式都有一个别名,就是包装模式,它们看似都是起到包装一个类或对象的作用,但是使用它们的目的很不一样。适配器模式是要将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的;而装饰器模式不是要改变被装饰对象的接口,而是恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方法从而提升性能。

参考文献:
1.https://blog.csdn.net/xilong_cheng/article/details/84199307
2.https://blog.csdn.net/Alexshi5/article/details/79481259?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-3

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值