第二章 深入分析java I/O的工作机制

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

四种I/O接口:

基于字节操作的I/O接口:InputStream、OutpeStream

基于字符操作的I/O接口:Writer、Reader

基于磁盘操作的I/O接口:File

基于网络操作的I/O接口:Socket

字节与字符的转化接口:数据持久化或网络传输都是以字节进行的

package cn.javaweb.twocharacter;
import java.io.FileReader;
import java.io.IOException;
/**  
* @Description: TODO(读文件:字节到字符)  
*/
public class test213 {
    public static void main(String[] args) {
        try {
        StringBuffer str = new StringBuffer();//append
        char[] buf = new char[1024];//字符
            FileReader f = new FileReader("file");
            while (f.read(buf)>0) {
                str.append(buf);
            }
            str.toString();
        } catch (IOException e) {
        }
    }
}    
  • 2.2 磁盘I/O工作机制

访问文件的方式:

1、标准访问文件的方式:先磁盘读取,再缓存到操作系统的缓存中

2、直接I/O的方式:应用程序直接访问磁盘数据

3、同步访问文件的方式:数据的写入和读取都是同步的

4、异步访问文件的方式:数据的线程发出请求后,线程会主动处理其他事情,而不是阻塞等待,当请求的数据返回后继续处理下面的操作

5、内存映射的方式:操作系统将内存的某一块区域与磁盘中的文件关联起来,当要访问内存中的一段数据时,转换为访问文件的一段数据

 

  • java访问磁盘文件

1、传入文件路径,将会根据路径创建一个File对象标识文件

2、根据File对象创建真正读取文件的操作对象(byte),这时会创建一个真实存在的磁盘文件描述符FileDescriptor(可直接控制文件)

3、 StreamDecoder将byte解码成char

 

  • java序列化技术

java序列化:将一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的

package cn.javaweb.twocharacter;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**  
* @Description: TODO(序列化)  
*/
public class Serialize implements Serializable{
    public static void main(String[] args) {
        try {
            FileOutputStream fos = new FileOutputStream("d:/serialize.date");//创建文件输出流
            ObjectOutputStream oos = new ObjectOutputStream(fos);//有什么用
            Serialize serialize = new Serialize();//创建序列化对象
            oos.writeObject(serialize);//Write the specified object to the ObjectOutputStream
            oos.flush();//刷出缓存
            oos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

序列化后得到的二进制字节数据

一下几种情况需要注意

1、当父类继承Serializable接口时,所有子类都可以被序列化

2、子类实现了Serializable接口,父类没有实现,则子类中属性仍可被序列化

3、如果序列化属性是对象,则这个对象必须实现Serializable接口

4、反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失

5、反序列化时,如果serialVersionUID(序列化ID,8字节)被修改,反序列化时会失败

 

  • 2.3网络I/O机制
  • TCP状态转化

TIME-WAIT:

1、FIN-WAIT2转换到TIME-WAIT:双发不同时发起FIN,主动关闭的一方在完成自身发起的关闭请求后,接收到被动一方的FIN后进入此状态

2、CLOSING转换到TIME-WAIT:双方同时发起关闭,都做了FIN请求,同时接收到FIN并做了ACK的情况下,这是就由CLOSING进入到TIME-WAIT

3、FIN-WAIT1转换到TIME-WAIT:同时收到FIN(对方发起)和ACK(本身发起的FIN回应),与CLOSING的区别:本身发起FIN回应的ACK先于对方的FIN请求到达

  • 影响网络传输的因素
  • java Socket的工作机制
  • 建立通信链路

客户端:

1、创建Socket实例,操作系统为此Socket实例分配一个没有被使用的本地端口号,并创建一个包含本地地址、远程地址和端口号的套接字数据结构,直至关闭连接

2、TCP三次握手,建立连接

3、Socket实例对象创建完成

服务端:

1、创建ServerSocket实例,同时操作系统创建一个底层数据结构(包含监听端口和监听地址的通配符“*”,监听所有地址)

2、调用accept(),进入阻塞状态,等待客户端的请求

3、新的请求到来时,为这个连接创建一个新的套接字数据结构(包含请求源地址和端口),会关联到ServerSocket实例的一个未完成的连接数据结构列表中

4、TCP三次握手,建立连接

5、ServerSocket实例对象创建完成

  • 数据传输

1、成功建立连接,服务端、客户端都有Socket实例

2、每个Socket都有一个InputStream和OutputStream,通过这两个对象来交换数据(byte),并为这两个对象分配一定大小的缓存

3、写入端将数据写到OutputStream对应的SendQ()队列中,队列满时移动到另一段的OutputStream的RecvQ队列中,如果此时RecvQ满时,OutputStream的write方法就会阻塞,直至有容量

 

  • 2.4 NIO工作方式

阻塞I/O(BI/O)带来的挑战

NIO的工作机制

1、Channel交通工具

2、Buffer座位

3、Selector调度系统

调用Selector的静态工厂创建一个选择器,创建一个服务端的Channel,绑定到一个Socket对象,并把这个通信信道注册到选择器上,把这个通信信道设置为非阻塞模式。然后调用Selector的selectedKeys方法来检查已经注册在这个选择器上的所有通信信道是否有需要的事件发生,如果有事件发生,将会返回所有的SelectionKey,通过这个对象的Channel方法就可以取得这个通信信道对象,从而读取通信的数据,这里读取的数据是Buffer

package cn.javaweb.twocharacter;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**  
* @ClassName: test241  
* @Description: TODO(NIO)  
* @author rocky  
* @date 2016年3月2日 上午9:48:03    
*/
public class test241 {
    
    public static void selector() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);//创建缓冲区=>座位
        Selector selector = Selector.open();//创建Selector=>车辆运行调度系统,检测车的运行状态
        ServerSocketChannel ssc = ServerSocketChannel.open();//接收连接Opens a server-socket channel=>交通工具
        ssc.configureBlocking(false);//设置非阻塞方式
        ssc.socket().bind(new InetSocketAddress(8080));//将ServerSocketChannel绑定到给定的端口
        ssc.register(selector, SelectionKey.OP_ACCEPT);//注册监听的事件:把ServerSocketChannel注册到selector上
        // 这时候selector就开始管理channel了当有ACCEPT事件来,通知channel,OP_ACCEPT有数据可读的通道可以说是“读就绪”
        while (true) {
            //我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合 。
            //我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。
            //对于每一个 SelectionKey,必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
            Set selectedKeys = selector.selectedKeys();
            Iterator it = selectedKeys.iterator();
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                //用“位与”操作 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在该 集合中。
                if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
                    //知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞:
                    ServerSocketChannel ssChanel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = ssChanel.accept();//Accepts a connection made to this channel's socket. 
                    sc.configureBlocking(false);//设置非阻塞方式
                    sc.register(selector, SelectionKey.OP_READ);//接受这个连接的目的是为了读取来自套接字的数据OP_READ
                    it.remove();//Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。
                                //下次该通道变成就绪时,Selector会再次将其放入已选择键集中。
                } else if((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ)
                {
                    SocketChannel sc = (SocketChannel) key.channel();
                    while (true) {
                        buffer.clear();//clear() 方法重设缓冲区,使它可以接受读入的数据
                        int n = sc.read(buffer);//读取数据
                        if (n <= 0) {
                            break;
                        }
                        buffer.flip();//flip() 方法让缓冲区可以将新读入的数据写入另一个通道
                    }
                    it.remove();
                }
            }
        }
    }
    public static void main(String[] args) throws IOException {
        selector();
    }
}

Buffer的工作方式

NIO的数据访问方式

1、FileChannel.tranceferXXX:数据直接在内核空间中移动

2、FileChannel.,map:将文件按照一定的大小块映射为内存区域,当程序访问这个内存区域时将直接操作这个文件的数据,这种方式省去了数据从内核空间到用户空间复制的损耗,适合大文件的只读性操作

package cn.javaweb.twocharacter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class Filechannel {
    public static void map(String[] args) {
        int BUFFER_SIZE = 1024;
        String filename = "test.db";
        long fileLength = new File(filename).length();
        int bufferCount = (int) (1 + (fileLength / BUFFER_SIZE));
        MappedByteBuffer[] buffers = new MappedByteBuffer[bufferCount];
        long remaining = fileLength;
        for (int i = 0; i < bufferCount; i++) {
            RandomAccessFile file;//Instances of this class support both reading and writing to a random access file
            try {
                file = new RandomAccessFile(filename, "r");
                //Maps a region of this channel's file directly into memory. 
                buffers[i] = file.getChannel().map(FileChannel.MapMode.READ_ONLY, i * BUFFER_SIZE, Math.min(remaining, BUFFER_SIZE));
            } catch (IOException e) {
                e.printStackTrace();
            }
            remaining -= BUFFER_SIZE;
        }
    }
    public static void main(String[] args) {
        map(args);
    }
}
  • I/O调优

磁盘I/O优化

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

2、优化磁盘管理系统,设计最优的磁盘方式的策略,以及寻址方式(线性寻址、三维地址模式)

3、磁盘存储数据块

4、RAID策略

RAID策略及说明
磁盘阵列 说明
RAID 0 数据平均输入到多个磁盘,且写数据和读数据都是并行的,IOPS提高1倍
RAID 1 提高数据安全,一份数据复制到多个磁盘,不提升IOPS
RAID 5 前两种折中,数据平均写到磁盘阵列总数的减一的磁盘中,剩余一个用来写入数据的奇偶校验信息
RAID 0+1 一个数据多组备份且同时写入

 

TCP网络参数调优

调优参数
echo "1024 65535">/proc/sys/net/ipv4/ip_local_port_range 设置向外连接可用端口
echo 1>/proc/sys/net/ipv4/tcp_tw_reuse 设置time_wait连接重用
echo 1>/proc/sys/net/ipv4/tcp_tw_recycle 设置快速回收time_wait连接
echo 180000>/proc/sys/net/ipv4/tcp_max_tw_buckets 设置最大time_wait连接长度
echo 0>/proc/sys/net/ipv4/tcp_timestamps 表示是否启用一种比超时重发更精确的方法来启用RTT的计算
echo 1>/proc/sys/net/ipv4/tcp_window_scaling 设置TCP/IP会话的滑动窗口是否可变
echo 20000>/proc/sys/net/ipv4/tcp_max_syn_backlog 设置最大等待处于客户端还没有应答回来的连接数
echo 10000>/proc/sys/net/core/somaxconn 设置每一个处于监听状态的端口的监听队列的长度
echo 10000>/proc/sys/net/core/netdev_max_backlog 设置最大等待CPU处理的包的数目
echo 2000000>/proc/sys/fs/file-max 设置最大打开文件数
echo 15>/proc/sys/net/ipv4/tcp_fin_timeout 设置FIN-WAIT-2状态等待回收的时间
echo 16777216>/proc/sys/net/core/rmem_max 设置最大的系统套接字数据接收缓冲大小
echo 262144>/proc/sys/net/core/rmem_default 设置默认的系统套接字数据接收缓冲大小
echo 16777216>/proc/sys/net/core/wmem_max 设置最大的系统套接字数据发送缓冲大小
echo 262144>/proc/sys/net/core/wmem_default 设置默认的系统套接字数据发送缓冲大小
echo "4096 87380 16777216">/proc/sys/net/ipv4/tcp_rmem 设置最大的TCP数据发送缓冲大小,三个值分别是最小、默认、最大
echo "4096 87380 16777216">/proc/sys/net/ipv4/tcp_wmem 设置最大的TCP数据接收缓冲大小,三个值分别是最小、默认、最大
   

 

网络I/O调优

1、减少网络交互次数

2、减少网络传输数据量的大小

3、减少编码

交互方式:同步异步、阻塞非阻塞

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

适配模式的结构

Java I/O的适配器模式

  • 2.7 设计模式解析之装饰模式

装饰模式的结构

Java I/O的装饰模式

 

posted @ 2016-02-29 01:07  水底的土豆  阅读(265)  评论(1)    收藏  举报