Java IO机制

Java IO有各种各样的类,纷繁复制,但是如果将他们的设计原理弄明白,就不会感觉非常混乱了。不过还是感觉很难用。。。。

Java的IO机制可以大概分为两个部分,一部分是基于流的IO, 一部分是NIO。

基于流的
IO

基于流的IO又分为两个部分,一部分是基于字节的IO,另一部分是基于字符的IO。基于字符的IO是在基于字节的IO基础上,又进行了解码,将 字节转化为字符。

无论这个流是什么类,总要解决下面几个问题。

  • 数据从哪里读取,写到哪里
  • 类有什么功能

字节流

数据来源及去向有很多种,例如:

  • 字节数组:ByteArrayInputStream/ByteArrayOutputStream
  • 字符串:StringBufferInputStream
  • 文件:FileInputStream/FileOutputStream
  • 管道:PipedInputStream/PipedOutputStream
  • Socket: socket.getInputStream()/socket.getOutputStream()

像文件读取这样的操作,最终需要调用操作系统的 API 实现,所以,你如果在JDK的源代码中跟踪下去,会发现,最终读写操作 调用的是 native 函数。

IO类通过继承FilterInputStream,使用装饰器模式,为流添加功能:

  • 按基本数据类型读取:DataInputStream
  • 使用缓冲区读取:BufferedInputStream
  • 跟踪行号:LineNumberInputStream
  • 弹出一个字节缓冲区:PushbackInputStream
  • 压缩:ZipInputStream/ZipOutputStream/GZIPInputStream/GZIPOutputStream

IO类通过 InputStreamReader/InputStreamWriter 将字节流转化成字符流,实际上就是添加了解码的功能。

字符流

数据来源与去向

  • 字符数组:CharArrayReader/CharArrayWriter
  • 字符串:StringReader/StringWriter
  • 文件:FileReader/FileWriter
  • 管道:PipedReader/PipedWriter

字符流新添加功能

  • 缓冲:BufferedReader/BufferedWriter
  • 行号:LineNumberReader
  • 打印:PrintWriter
  • 一字符缓冲:PushbackReader


IO

从 JDK1.4 开始,引入了 java.nio 包,在新IO中,引入了几个新的概念。

  • 通道(Channel)
  • 缓冲区(Buffer)
  • 选择器(Selector)

新 IO 中,通过 File, Socket 可以得到 Channel,然后读数据时,将数据从 Channel 读取到 Buffer 中,写数据时,将数据从 Buffer 中 写入到 Channel。我们通过操作 Buffer 来读取,写入数据。

例如,将一个文件中写入一个字节的数据:

    public static void testNIO() throws IOException {
        File file = new File("testNIO.txt");
        FileOutputStream outputStream = new FileOutputStream(file);
        FileChannel channel = outputStream.getChannel();
        ByteBuffer src = ByteBuffer.allocate(2);
        src.put((byte) 65);
        src.flip();
        channel.write(src);
        channel.close();
    }

注意如何获取文件通道,如何操作 Buffer,如何通过 Buffer 和 Channel 向文件中写入数据。

Buffer
结构与原理

另外,要明白 nio 的原理,掌握 Buffer 的结构是很有必要的,Buffer 有三个关键的数据

    private int position = 0;
    private int limit;
    private int capacity;

capacity 是 Buffer 的大小,limit 是当前position可以到达的最大位置,position 是当前要操作的数据的位置。

初始状态

Buffer 的初始状态:

+---------------------------------------+
|   |   |   |   |   |   |   |   |   |   |
+---------------------------------------+
                                    capacity
position                              limit

写入数据后

向 Buffer 中写入数据后,position 的位置会移动:

    public ByteBuffer put(byte x) {
        hb[ix(nextPutIndex())] = x;
        return this;
    }
    final int nextPutIndex() {                          // package-private
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }

写入数据后,buffer状态

+---------------------------------------+
| * | * | * | * | * |   |   |   |   |   |
+---------------------------------------+

                                     capacity
                  position             limit

flip

这时候,我们需要调用 flip 函数,将 limit 设置为 position 当前位置设置, 同时,将 position 清0。

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

调用 flip 后 Buffer 状态

+---------------------------------------+
| * | * | * | * | * |   |   |   |   |   |
+---------------------------------------+

                   limit                capacity
position

读取数据后

这样,从 Buffer 中读取数据时,就可以从 position 开始,一直读取到 limit。

读取数据时, position 的位置也要变化。

    public byte get() {
        return hb[ix(nextGetIndex())];
    }
    final int nextGetIndex() {                          // package-private
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }

读取数据后,Buffer 状态

+---------------------------------------+
| * | * | * | * | * |   |   |   |   |   |
+---------------------------------------+

                   limit                capacity
                   position

clear

此后,如果想再向 Buffer 中写入数据,可以调用 clear 函数,将 position 清 0,limit 设置为 capacity。使得 Buffer 回到初态。

+---------------------------------------+
| * | * | * | * | * |   |   |   |   |   |
+---------------------------------------+
                                    capacity
position                              limit
    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

总之,需要时刻明白,读取与写入都是从当前位置 position 开始,一直移动到 limit。同时,需要明白自己调用函数之前和之后, position 与 limit 的位置是如何变化的。

另外,这里的读取与写入一定要明白是指什么,我们对 Buffer 进行 put 时,是对它写,进行 get 时,是对它读。而 channel.write(buf) 时,是对 buf 读,再写入 channel。 channel.read(buf) 时,是从 channel 读,再写入 buf。需要弄清楚读写的主体是什么。

Selector
与 Channel

在传统IO中,由于读写操作是阻塞的,当无法读取或者写入数据时,程序就会阻塞住。为了同时为多个客户端服务,服务器端需要多个 线程,一方面可以使多个客户端得到响应,另一方面,也使得一个线程被阻塞时,整个程序不会停下来。

而在新IO中,通过通道,可以实现非阻塞IO,同时,可以通过一个Selector来管理多个Channel。这样,使用一个线程就可以管理多个 客户端连接。

先创建一个 Selector,

 Selector selector = Selector.open();

然后,将每个Channel设置成非阻塞的。然后调用Channel的register函数,将对应的selector及一个事件代码传入,意思是当这个事件发生 时,我们可以通过 selector 来获取这个 channel。

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress address = new InetSocketAddress(
                InetAddress.getLocalHost(), 10000);
        serverSocketChannel.socket().bind(address);
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

假如已经将多个channel 注册到 selector,那么当其中有几个 channel 已经发生了我们所关心的事件时,如何获取这些 channel呢?

可以通过 select 函数获取现在已经触发事件的 channel 个数。

selector.select()

然后,通过 selectedKeys 函数获取所有发生的事件

Set<SelectionKey> selectedKeys = selector.selectedKeys();

并通过迭代器,访问每个事件,检测他们的事件类型,并作出相应的处理。

Iterator<SelectionKey> it = selectedKeys.iterator(); // 依次进行处理
while (it.hasNext()) {
    SelectionKey selectionKey = it.next();
    if (selectionKey.isAcceptable()) { // 如果是Accept事件
    // 获取注册的ServerSocketChannel
        serverSocketChannel = ((ServerSocketChannel) selectionKey
        .channel());
        SocketChannel socketChannel = serverSocketChannel.accept(); // 建立连接
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ); // 注册该连接的Read事件

    } else if (selectionKey.isReadable()) { // 如果是Read事件
    // 获取注册的SocketChannel
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (socketChannel.read(buffer) > 0) { // 读取接收到的数据
            buffer.flip();
            byte[] dst = new byte[buffer.limit()];
            buffer.get(dst);
        }
    }
    it.remove(); // 需要将处理过的事件移除
}

注意,当 ACCEPT 事件发生时,我们获取了新的 channel ,并将 selector 及 OP_READ 事件注册过去,这样,当这个 channel 的 READ 事件发生时,我们就可以通过 selectorKeys 来获取这个事件。

(上面的代码来自参考资料1)

通过上面的例子,可以看出,我们使用了一个线程, 一个selector,多个channel,就管理了多个来自客户端的 socket 连接。

除了上面的 Selector, Channel, Buffer 外,新 IO 还提供了内存映射文件,文件锁功能。

内存映射文件

操作系统可以利用虚拟内存实现将文件或者文件的一部分映射到内存中,然后这个文件就可以当作是内存数组一样随机访问。通过 内存映射文件,可以大大提升文件访问的性能,同时可以支持对大文件的访问。我们可以通过 Channel 和 Buffer 来访问内存映射文件的内容。

FileChannel channel = FileChannel.open(Paths.get("test.txt"));
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0,
                channel.size());
for (int i = 0; i < channel.size(); i++) {
    buffer.get(i);
}

文件锁

可以通过给文件加锁,来控制对文件访问的同步。

FileChannel channel = FileChannel.open(Paths.get("test.txt"));
FileLock lock = channel.lock();

// Operation on channel

lock.release();

参考资料

Java NIO (三) 非阻塞模式与Selector

时间: 2024-10-11 23:10:44

Java IO机制的相关文章

Java IO工作机制分析

Java的IO类都在java.io包下,这些类大致可分为以下4种: 基于字节操作的 I/O 接口:InputStream 和 OutputStream 基于字符操作的 I/O 接口:Writer 和 Reader 基于磁盘操作的 I/O 接口:File 基于网络操作的 I/O 接口:Socket 1 IO类库的基本结构 1.1 基于字节操作的IO接口 基于字节操作的IO接口分别是InputStream和OutputStream,InputStream的类结构图如下所示: 同InputStream

Java IO流 探险

Java的IO流使用了一种装饰器设计模式,它将IO流分为底层节点流和上层处理流.本篇重点在如何访问文件与目录.如何以二进制格式和文本格式来读写数据.对象序列化机制.还有Java7的"NIO.2". 装饰设计模式:当想要对已有的对象进行功能增强时,可以定义类,将已有对象传入,基于已有的功能,并提供加强功能.那么自定义的该类称为装饰类. 装饰类通常会通过构造方法接收被装饰的对象.并基于被装饰的对象的功能,提供更强的功能. IO的方式通常分为:BIO(同步阻塞).NIO(同步非阻塞).AIO

Java 类加载机制详解

一.类加载器 类加载器(ClassLoader),顾名思义,即加载类的东西.在我们使用一个类之前,JVM需要先将该类的字节码文件(.class文件)从磁盘.网络或其他来源加载到内存中,并对字节码进行解析生成对应的Class对象,这就是类加载器的功能.我们可以利用类加载器,实现类的动态加载. 二.类的加载机制 在Java中,采用双亲委派机制来实现类的加载.那什么是双亲委派机制?在Java Doc中有这样一段描述: The ClassLoader class uses a delegation mo

Java IO详解(转)

IO是Java及众多编程语言很重要的一块,同时很多程序的瓶颈和耗时操作也都在IO这块. 一.简介 IO操作面临很多问题,信息量的巨大,网络的环境等等,因为IO不仅仅是对本地文件.目录的操作,有时对二进制流.还有一部分是网络方面的资源,所以多种原因直接造成IO操作无疑是耗时且复杂多变的.Java对IO的支持是个不断的演变过程,经过了很多的优化,直到JDK1.4以后,才趋于稳定,在JDK1.4中,加入了nio类,解决了很多性能问题,虽然我们有足够的理由不去了解关于Java IO以前的情况,但是为了学

java IO 包源码解析

本文参考连接:http://blog.csdn.net/class281/article/details/24849275                         http://zhhphappy.iteye.com/blog/1562427 一.IO包简要类图 Java I/O流部分分为两个模块,即Java1.0中就有的面向字节的流(Stream),以及Java1.1中大幅改动添加的面向字符的流(Reader & Writer).添加面向字符的流主要是为了支持国际化,旧的I/O流仅支持

java运行机制详细

JVM(Java虚拟机)一种用于计算设备的规范,可用不同的方式(软件或硬件)加以实现.编译虚拟机的指令集与编译微处理器的指令集非常类似.Java虚拟机包括一套字节码指令集.一组寄存器.一个栈.一个垃圾回收堆和一个存储方法域. Java虚拟机(JVM)是可运行Java代码的假想计算机.只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行. 1.为什么要使用Java虚拟机 Java语言的一个非常重要的特点就是与平台的无关性.而使用Java虚拟机是实

装饰器模式及JAVA IO流例子★★★☆☆

一.什么是装饰模式 通过关联机制给类增加行为,其行为的扩展由修饰对象来决定: 二.补充说明 与继承相似,不同点在于继承是在编译期间扩展父类,而装饰器模式在运行期间动态扩展原有对象: 或者说,继承是对类进行扩展,装饰模式是对对象进行扩展: 三.角色 抽象构件 具体构件 抽象装饰类 具体装饰类 说明:具体构件.抽象装饰类.具体装饰类的共同父类是抽象构件,具体装饰类继承抽象装饰类并在运行期间装饰具体构件: 四.例子 例子说明: 画家接口Painter,为抽象构件,有两个方法,获取画家描述信息及绘画:

java反射机制分析

本文转自:http://www.cnblogs.com/gulvzhe/archive/2012/01/27/2330001.html 浅显易懂,值得收藏 Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象, 都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制.反射的概念是由Smith在1982年 首次提出的,主要是指程序可以访问.检测和修改它本身状态或行为的一种能力.这一概念的提出很快引发了

利用java反射机制实现读取excel表格中的数据

如果直接把excel表格中的数据导入数据库,首先应该将excel中的数据读取出来. 为了实现代码重用,所以使用了Object,而最终的结果是要获取一个list如List<User>.List<Book>等,所以需要使用泛型机制去实现.下面会给出代码,可能会稍微复杂一点,但注释很清晰,希望大家耐心阅读. 在上代码之前简单说一下思路: 1.excel表格必须有表头,且表头中各列的值要与实体类的属性相同: 2.先读取表头信息,然后获取表头列数,接着确定需要使用的set方法的名称,并存到数