`
dengqsintyt
  • 浏览: 291270 次
  • 性别: Icon_minigender_1
社区版块
存档分类
最新评论

JAVA I/O性能

 
阅读更多

在应用程序中,通常会涉及到两种类型的计算:CPU计算和I/O计算。对于大多数应用来说,花费在等待I/O上的时间是占较大比重的。通常需要等待速度较 慢的磁盘或是网络连接完成I/O请求,才能继续后面的CPU计算任务。因此提高I/O操作的效率对应用的性能有较大的帮助。本文将介绍Java语言中与I /O操作相关的内容,包括基本的Java I/OJava NIO,着重于基本概念和最佳实践。

Java语言提供了多个层次不同的概念来对I/O操作进行抽象。Java I/O中最早的概念是流,包括输入流和输出流,早在JDK 1.0中就存在了。简单的来说,流是一个连续的字节的序列。输入流是用来读取这个序列,而输出流则构建这个序列。InputStream OutputStream 所操纵的基本单元就是字节。每次读取和写入单个字节或是字节数组。如果从字节的层次来处理数据类型的话,操作会非常繁琐。可以用更易使用的流实现来包装基本的字节流。如果想读取或输出Java的基本数据类型,可以使用DataInputStream DataOutputStream 。它们所提供的类似readFloatwriteDouble这样的方法,会让处理基本数据类型变得很简单。如果希望读取或写入的是Java中的对象的话,可以使用ObjectInputStream ObjectOutputStream 。它们与对象的序列化 机制一起,可以实现Java对象状态的持久化和数据传递。基本流所提供的对于输入和输出的控制比较弱。InputStream只提供了顺序读取、跳过部分字节和标记/重置的支持,而OutputStream则只能顺序输出。

流的使用

由于I/O操作所对应的实体在系统中都是有限的资源,需要妥善的进行管理。每个打开的流都需要被正确的关闭以释放资源。所遵循的原则是谁打开谁释放。如果一个流只在某个方法体内使用,则通过finally语句或是JDK 7中的try-with-resources 语 句来确保在方法返回之前,流被正确的关闭。如果一个方法只是作为流的使用者,就不需要考虑流的关闭问题。典型的情况是在servlet实现中并不需要关闭 HttpServletResponse中的输出流。如果你的代码需要负责打开一个流,并且需要在不同的对象之间进行传递的话,可以考虑使用Execute Around Method 模式。如下面的代码所示:

 

Java代码 

             

public void use(StreamUser user) {
    InputStream input = null;
    try {
        input = open();
        user.use(input);
    } catch(IOException e) {
        user.onError(e);
    } finally {
        if (input != null) {
            try { 
                input.close();
            } catch (IOException e) {
                user.onError(e);
            }
        }
    }
 }

 

 

 

如上述代码中所看到的一样,由专门的类负责流的打开和关闭。流的使用者StreamUser并不需要关心资源释放的细节,只需要对流进行操作即可。

在使用输入流的过程中,经常会遇到需要复用一个输入流的情况,即多次读取一个输入流中的内容。比如通过URL.openConnection方法打 开了一个远端站点连接的输入流,希望对其中的内容进行多次处理。这就需要把一个InputStream对象在多个对象中传递。为了保证每个使用流的对象都 能获取到正确的内容,需要对流进行一定的处理。通常有两种解决的办法,一种是利用InputStream的标记支持。如果一个流支持标记的话(通过markSupported 方法判断),就可以在流开始的地方通过mark 方法添加一个标记,当完成一次对流的使用之后,通过reset 方法就可以把流的读取位置重置到上次标记的位置,即流开始的地方。如此反复,就可以复用这个输入流。大部分输入流的实现是不支持标记的。可以通过BufferedInputStream 进行包装来支持标记。

 

 

Java代码 

            

 

private InputStream prepareStream(InputStream ins) {
    BufferedInputStream buffered = new BufferedInputStream(ins);
    buffered.mark(Integer.MAX_VALUE);
    return buffered;
} 
private void resetStream(InputStream ins) throws IOException {
    ins.reset();
    ins.mark(Integer.MAX_VALUE);
}  
 

如上面的代码所示,通过prepareStream方法可以用一个BufferedInputStream来包装基本的InputStream。通 过 mark方法在流开始的时候添加一个标记,允许读入Integer.MAX_VALUE个字节。每次流使用完成之后,通过resetStream方法重置 即可。

另外一种做法是把输入流的内容转换成字节数组,进而转换成输入流的另外一个实现ByteArrayInputStream 。 这样做的好处是使用字节数组作为参数传递的格式要比输入流简单很多,可以不需要考虑资源相关的问题。另外也可以尽早的关闭原始的输入流,而无需等待所有使 用流的操作完成。这两种做法的思路其实是相似的。BufferedInputStream在内部也创建了一个字节数组来保存从原始输入流中读入的内容。

 

 

Java代码 

 

private byte[] saveStream(InputStream input) throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    ReadableByteChannel readChannel = Channels.newChannel(input);
    ByteArrayOutputStream output = new ByteArrayOutputStream(32 * 1024);
    WritableByteChannel writeChannel = Channels.newChannel(output);
    while ((readChannel.read(buffer)) > 0 || buffer.position() != 0) {
        buffer.flip();
        writeChannel.write(buffer);
        buffer.compact();
    }
    return output.toByteArray();
}
 上面的代码中saveStream方法把一个InputStream保存为字节数组。

 

缓冲区

由于流背后的数据有可能比较大,在实际的操作中,通常会使用缓冲区来提高性能。传统的缓冲区的实现是使用数组来完成。比如经典的从InputStreamOutputStream的复制的实现 ,就是使用一个字节数组作为中间的缓冲区。NIO中引入的Buffer 类及其子类,可以很方便的用来创建各种基本数据类型的缓冲区。相对于数组而言,Buffer类及其子类提供了更加丰富的方法来对其中的数据进行操作。后面会提到的通道也使用Buffer类进行数据传递。

Buffer上进行的元素添加和删除操作,都围绕3个属性position limit capacity 展 开,分别表示Buffer当前的读写位置、可用的读写范围和容量限制。容量限制是在创建的时候指定的。Buffer提供的get/put方法都有相对和绝 对两种形式。相对读写时的位置是相对于position的值,而绝对读写则需要指定起始的序号。在使用Buffer的常见错误就是在读写操作时没有考虑到 这3个元素的值,因为大多数时候都是使用的是相对读写操作,而position的值可能早就发生了变化。一些应该注意的地方包括:将数据读入缓冲区之前, 需要调用clear 方法;将缓冲区中的数据输出之前,需要调用flip 方法。

 

Java代码 

 

ByteBuffer buffer = ByteBuffer.allocate(32);
CharBuffer charBuffer = buffer.asCharBuffer();
String content = charBuffer.put("Hello").put("World").flip().toString();
System.out.println(content);
 

 

  

 

上面的代码展示了Buffer子类的使用。首先可以在已有的ByteBuffer上面创建出其它数据类型的缓冲区视图,其次Buffer子类的很多方法是可以级联的,最后是要注意flip方法的使用。

字符与编码

在程序中,总是免不了与字符打交道,毕竟字符是用户直接可见的信息。而与字符处理直接相关的就是编码。相信不少人都曾经为了程序中的乱码问题而困 扰。要弄清楚这个问题,就需要理解字符集和编码的概念。字符集,顾名思义,就是字符的集合。一个字符集中所包含的字符通常与地区和语言有关。字符集中的每 个字符通常会有一个整数编码与其对应。常见的字符集有ASCIIISO-8859-1Unicode等。对于字符集中的每个字符,为了在计算机中表 示,都需要转换某种字节的序列,即该字符的编码。同一个字符集可以有不同的编码方式。如果某种编码格式产生的字节序列,用另外一种编码格式来解码的话,就 可能会得到错误的字符,从而产生乱码的情况。所以将一个字节序列转换成字符串的时候,需要知道正确的编码格式。

NIO中的java.nio.charset 包提供了与字符集相关的类,可以用来进行编码和解码。其中的CharsetEncoder CharsetDecoder 允 许对编码和解码过程进行精细的控制,如处理非法的输入以及字符集中无法识别的字符等。通过这两个类可以实现字符内容的过滤。比如应用程序在设计的时候就只 支持某种字符集,如果用户输入了其它字符集中的内容,在界面显示的时候就是乱码。对于这种情况,可以在解码的时候忽略掉无法识别的内容。

 

Java代码 

 

String input = "你123好";
Charset charset = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = charset.newEncoder();
encoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
CharsetDecoder decoder = charset.newDecoder();
CharBuffer buffer = CharBuffer.allocate(32);
buffer.put(input);
buffer.flip();
try {
    ByteBuffer byteBuffer = encoder.encode(buffer);
    CharBuffer cbuf = decoder.decode(byteBuffer);
    System.out.println(cbuf);  //输出123
} catch (CharacterCodingException e) {
    e.printStackTrace();
} 
 

 

 

上面的代码中,通过使用ISO-8859-1字符集的编码和解码器,就可以过滤掉字符串中不在此字符集中的字符。

Java I/O在处理字节流字之外,还提供了处理字符流的类,即Reader /Writer 类 及其子类,它们所操纵的基本单位是char类型。在字节和字符之间的桥梁就是编码格式。通过编码器来完成这两者之间的转换。在创建 Reader/Writer子类实例的时候,总是应该使用两个参数的构造方法,即显式指定使用的字符集或编码解码器。如果不显式指定,使用的是JVM的默 认字符集,有可能在其它平台上产生错误。

通道

通道作为NIO中的核心概念,在设计上比之前的流要好不少。通道相关的很多实现都是接口而不是抽象类。通道本身的抽象层次也更加合理。通道表示的是 对支持I/O操作的实体的一个连接。一旦通道被打开之后,就可以执行读取和写入操作,而不需要像流那样由输入流或输出流来分别进行处理。与流相比,通道的 操作使用的是Buffer而不是数组,使用更加方便灵活。通道的引入提升了I/O操作的灵活性和性能,主要体现在文件操作和网络操作上。

文件通道

对文件操作方面,文件通道FileChannel 提供了与其它通道之间高效传输数据的能力,比传统的基于流和字节数组作为缓冲区的做法,要来得简单和快速。比如下面的把一个网页的内容保存到本地文件的实现。

 

Java代码   

FileOutputStream output = new FileOutputStream("baidu.txt");
	FileChannel channel = output.getChannel();
	URL url = new URL("http://www.baidu.com");
	InputStream input = url.openStream();
	ReadableByteChannel readChannel = Channels.newChannel(input);
	channel.transferFrom(readChannel, 0, Integer.MAX_VALUE);

 

 

文件通道的另外一个功能是对文件的部分片段进行加锁。当在一个文件上的某个片段加上了排它锁之后,其它进程必须等待这个锁释放之后,才能访问该文件 的这个片段。文件通道上的锁是由JVM所持有的,因此适合于与其它应用程序协同时使用。比如当多个应用程序共享某个配置文件的时候,如果Java程序需要 更新此文件,则可以首先获取该文件上的一个排它锁,接着进行更新操作,再释放锁即可。这样可以保证文件更新过程中不会受到其它程序的影响。

另外一个在性能方面有很大提升的功能是内存映射文件 的支持。通过FileChannelmap 方法可以创建出一个MappedByteBuffer 对象,对这个缓冲区的操作都会直接反映到文件内容上。这点尤其适合对大文件进行读写操作。

套接字通道

在套接字通道方面的改进是提供了对非阻塞I/O和多路复用I/O的支持。传统的流的I/O操作是阻塞式的。在进行I/O操作的时候,线程会处于阻塞状态等待操作完成。NIO中引入了非阻塞I/O的支持,不过只限于套接字I/O操作。所有继承自SelectableChannel 的通道类都可以通过configureBlocking 方法来设置是否采用非阻塞模式。在非阻塞模式下,程序可以在适当的时候查询是否有数据可供读取。一般是通过定期的轮询来实现的。

多路复用I/O是一种新的I/O编程模型。传统的套接字服务器的处理方式是对于每一个客户端套接字连接,都新创建一个线程来进行处理。创建线程是很 耗时的操作,而有的实现会采用线程池。不过一个请求一个线程的处理模型并不是很理想。原因在于耗费时间创建的线程,在大部分时间可能处于等待的状态。而多 路复用I/O的基本做法是由一个线程来管理多个套接字连接。该线程会负责根据连接的状态,来进行相应的处理。多路复用I/O依靠操作系统提供的select 或相似系统调用的支持,选择那些已经就绪的套接字连接来处理。可以把多个非阻塞I/O通道注册在某个Selector 上,并声明所感兴趣的操作类型。每次调用Selectorselect 方法,就可以选择到某些感兴趣的操作已经就绪的通道的集合,从而可以进行相应的处理。如果要执行的处理比较复杂,可以把处理转发给其它的线程来执行。

下面是一个简单的使用多路复用I/O的服务器实现。当有客户端连接上的时候,服务器会返回一个Hello World作为响应。

 

Java代码 

private static class IOWorker implements Runnable {
	public void run() {
		try {
			Selector selector = Selector.open();
			ServerSocketChannel channel = ServerSocketChannel.open();
			channel.configureBlocking(false);
			ServerSocket socket = channel.socket();
			socket.bind(new InetSocketAddress("localhost", 10800));
			channel.register(selector, channel.validOps());
			while (true) {
				selector.select();
				Iterator iterator = selector.selectedKeys().iterator();
				while (iterator.hasNext()) {
					SelectionKey key = iterator.next();
					iterator.remove();
					if (!key.isValid()) {
						continue;
					}
					if (key.isAcceptable()) {
						ServerSocketChannel ssc = (ServerSocketChannel) key
								.channel();
						SocketChannel sc = ssc.accept();
						sc.configureBlocking(false);
						sc.register(selector, sc.validOps());
					}
					if (key.isWritable()) {
						SocketChannel client = (SocketChannel) key.channel();
						Charset charset = Charset.forName("UTF-8");
						CharsetEncoder encoder = charset.newEncoder();
						CharBuffer charBuffer = CharBuffer.allocate(32);
						charBuffer.put("Hello World");
						charBuffer.flip();
						ByteBuffer content = encoder.encode(charBuffer);
						client.write(content);
						key.cancel();
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

  

上面的代码给出的只是非常简单的示例程序,只是展示了多路复用I/O的基本使用方式。在开发复杂网络应用程序的时候,使用一些Java NIO网络应用框架会让你事半功倍。目前来说最流行的两个框架是Apache MINANetty。在使用了Netty之后,Twitter的搜索功能速度提升达到了3倍之多。网络应用开发人员都可以使用这两个开源的优秀框架。

 

 

分享到:
评论

相关推荐

    Java I/O, 2nd Edition

    7. **性能优化**:书中还涉及了如何通过缓冲、管道、异步I/O等技术来提高Java I/O的性能。 8. **案例研究**:提供了一些实际应用示例,帮助读者理解如何在实际项目中应用Java I/O技术。 9. **错误处理和调试**:...

    Java 新I/O

    Java 新I/O,也称为NIO(New Input/Output),是Java平台中对传统I/O模型的一种改进。在Java 1.4版本中引入的NIO库为开发人员提供了更高效、非阻塞的数据处理方式,特别适用于高并发、低延迟的系统。NIO的核心在于...

    Java I/O, NIO and NIO.2

    Java I/O, NIO, 和 NIO.2 是Java平台中处理输入/输出操作的核心组件,对于任何Java开发者来说,理解和掌握这些概念至关重要。本文将深入探讨这些技术,旨在提供一个全面而详尽的概述。 Java I/O(Input/Output)是...

    Java I/O 过滤流-带格式的读写操作

    在Java编程语言中,输入/输出(I/O)是处理数据传输的核心部分。过滤流(Filter Stream)是Java I/O框架中的一个重要概念,它提供了一种优雅的方式来进行数据的读写操作,同时允许我们添加额外的功能,如字符编码...

    深入分析 Java I/O 的工作机制(转载)

    Java I/O(输入/输出)系统是Java编程语言中用于处理数据流的重要组成部分,它允许程序与外部资源如文件、网络、硬件设备等进行交互。深入理解Java I/O的工作机制对于开发高效、可靠的系统至关重要。以下是对Java I/...

    java阻塞i/o与非阻塞i/o控制

    了解并熟练掌握这两种I/O模型,有助于开发者编写出高效、可扩展的Java应用程序,特别是在网络编程和服务器开发中,非阻塞I/O能够带来显著的性能提升。因此,深入学习和实践Java中的阻塞I/O与非阻塞I/O控制是非常必要...

    Java I/O系统

    此外,Java NIO(New Input/Output)是Java 1.4引入的一个重要改进,它提供了非阻塞I/O和选择器,能够同时处理多个输入输出通道,大大提升了并发性能。`Selector`类允许程序监控多个`SocketChannel`,一旦有数据可读...

    java对I/O流的处理

    Java中的I/O流处理是程序与外部设备交互数据的关键机制,包括从文件、网络、内存等数据源读取数据和向这些目标写入数据。I/O流系统在Java的`java.io`包中被实现,提供了丰富的类和接口来支持各种类型的流操作。 **I...

    Java I/O层次结构详解

    Java I/O层次结构详解 Java I/O系统是Java平台中不可或缺的一部分,它为开发者提供了处理输入和输出的强大工具。在Java中,I/O操作主要基于流(Stream)的概念,流可以被视为数据的有序序列,既可以代表从源读取...

    java i/0习题

    Java I/O(输入/输出)是Java编程语言中不可或缺的一部分,它允许程序与外部资源进行数据交换,如文件、网络连接、系统硬件等。在Java I/O中,我们使用流(Stream)的概念来处理数据,流是数据传输的通道。本套习题...

    Java I/O学习笔记: 磁盘操作 字节操作 字符操作 对象操作 网络操作 NIO & AIO Java I/O

    Java I/O学习笔记: 磁盘操作 字节操作 字符操作 对象操作 网络操作 NIO & AIO Java I/O Java是一种面向对象的编程语言,由Sun Microsystems于1995年推出。它是一种跨平台的语言,意味着可以在不同的操作系统上运行...

    Java I/O总结

    ### Java I/O总结 #### 一、从`new BufferedReader(new InputStreamReader(conn.getInputStream()))`想到的 在Java编程中,处理输入输出(I/O)是一项常见的任务。这段代码`new BufferedReader(new ...

    java基础之I/O流

    Java中的I/O流是程序与外部数据交互的重要机制,它允许数据在程序、文件、网络等之间流动。I/O流分为两大类:字符流(Character Stream)和字节流(Byte Stream),每类又分为输入流(Input Stream)和输出流...

    MaglevIO,一个易于使用和高效的Java I/O库。基于Java NATEVEIO。.zip

    3. **面向现代应用**:随着微服务、云计算和大数据的普及,对I/O性能的要求日益增长。MaglevIO作为一款现代化的I/O库,能够满足这些场景的需求,为分布式系统和高并发应用提供强大的支持。 4. **开源社区支持**:...

    Java I/O文件读写/删除/复制等

    自Java 1.4起,Java引入了NIO(New IO)库,提供了非阻塞I/O和通道的概念,提高了I/O性能。`java.nio.file` 包提供了 `Path`、`Files` 等类,可以更方便地进行文件操作。 9. **文件监控** Java 7引入了文件系统...

    Java I/O 使用和最佳实践

    Java I/O系统是Java编程语言中的一个核心组成部分,它提供了处理输入输出操作的类和接口。这个系统的设计目的是为了使得应用程序能够与外部世界交互,包括读取和写入文件、网络数据、标准输入输出流等。在Java中,I/...

    探索Java I/O 模型的演进

    2. **NIO(Non-blocking I/O)**:Java 1.4引入了NIO(New I/O),提供了选择器(Selector)和通道(Channel)等机制,支持I/O多路复用,可以实现非阻塞I/O,提高了服务器的并发性能。 3. **NIO 2(AIO或NIO 2.0)**...

    android 快速的搜索手机文件引擎 java I/O的应用

    总之,构建Android上的快速文件搜索引擎涉及多方面的技术,包括Java I/O的熟练运用、高效的搜索算法、性能优化、权限管理和用户界面设计。理解这些知识点并结合实际需求,能帮助我们创建出强大且易用的文件搜索工具...

    异步I/O处理

    此外,许多高级编程框架,如Node.js的Event Loop,Python的asyncio,Java的NIO,都提供了异步I/O的支持。 异步I/O处理的关键在于非阻塞和事件驱动。非阻塞I/O意味着即使没有数据可读,读取操作也不会挂起;事件驱动...

    java中I/O的t经典ppt

    Java中的I/O(输入/输出)是编程中一个至关重要的概念,特别是在处理文件操作、网络通信和数据交换时。在Java中,I/O是通过一系列流(Stream)类来实现的,这些类允许数据从一个源头(如键盘、文件、网络连接)传输...

Global site tag (gtag.js) - Google Analytics