天天看點

Java I/O 操作及優化建議

java i/o

i/o,即 input/output(輸入/輸出) 的簡稱。就 i/o 而言,概念上有 5 種模型:blocking

i/o,nonblocking i/o,i/o multiplexing (select and poll),signal driven i/o

(sigio),asynchronous i/o (the posix

aio_functions)。不同的作業系統對上述模型支援不同,unix 支援 io 多路複用。不同系統叫法不同,freebsd 裡面叫

kqueue,linux 叫 epoll。而 windows2000 的時候就誕生了 iocp 用以支援 asynchronous i/o。

java 是一種跨平台語言,為了支援異步 i/o,誕生了 nio,java1.4 引入的 nio1.0 是基于 i/o 複用的,它在各個平台上會選擇不同的複用方式。linux 用的 epoll,bsd 上用 kqueue,windows 上是重疊 i/o。

java i/o 的相關方法如下所述:

同步并阻塞 (i/o 方法):伺服器實作模式為一個連接配接啟動一個線程,每個線程親自處理 i/o 并且一直等待 i/o

直到完成,即用戶端有連接配接請求時伺服器端就需要啟動一個線程進行處理。但是如果這個連接配接不做任何事情就會造成不必要的線程開銷,當然可以通過線程池機制改

善這個缺點。i/o 的局限是它是面向流的、阻塞式的、串行的一個過程。對每一個用戶端的 socket 連接配接 i/o

都需要一個線程來處理,而且在此期間,這個線程一直被占用,直到 socket 關閉。在這期間,tcp

的連接配接、資料的讀取、資料的傳回都是被阻塞的。也就是說這期間大量浪費了 cpu 的時間片和線程占用的記憶體資源。此外,每建立一個 socket

連接配接時,同時建立一個新線程對該 socket 進行單獨通信

(采用阻塞的方式通信)。這種方式具有很快的響應速度,并且控制起來也很簡單。在連接配接數較少的時候非常有效,但是如果對每一個連接配接都産生一個線程無疑是對

系統資源的一種浪費,如果連接配接數較多将會出現資源不足的情況;

同步非阻塞 (nio 方法):伺服器實作模式為一個請求啟動一個線程,每個線程親自處理 i/o,但是另外的線程輪詢檢查是否 i/o

準備完畢,不必等待 i/o 完成,即用戶端發送的連接配接請求都會注冊到多路複用器上,多路複用器輪詢到連接配接有 i/o

請求時才啟動一個線程進行處理。nio 則是面向緩沖區,非阻塞式的,基于選擇器的,用一個線程來輪詢監控多個資料傳輸通道,哪個通道準備好了

(即有一組可以處理的資料) 就處理哪個通道。伺服器端儲存一個 socket 連接配接清單,然後對這個清單進行輪詢,如果發現某個 socket

端口上有資料可讀時,則調用該 socket 連接配接的相應讀操作;如果發現某個 socket 端口上有資料可寫時,則調用該 socket

連接配接的相應寫操作;如果某個端口的 socket 連接配接已經中斷,則調用相應的析構方法關閉該端口。這樣能充分利用伺服器資源,效率得到大幅度提高;

異步非阻塞 (aio 方法,jdk7 釋出):伺服器實作模式為一個有效請求啟動一個線程,用戶端的 i/o

請求都是由作業系統先完成了再通知伺服器應用去啟動線程進行處理,每個線程不必親自處理 i/o,而是委派作業系統來處理,并且也不需要等待 i/o

完成,如果完成了作業系統會另行通知的。該模式采用了 linux 的 epoll 模型。

在連接配接數不多的情況下,傳統 i/o 模式編寫較為容易,使用上也較為簡單。但是随着連接配接數的不斷增多,傳統 i/o

處理每個連接配接都需要消耗一個線程,而程式的效率,當線程數不多時是随着線程數的增加而增加,但是到一定的數量之後,是随着線程數的增加而減少的。是以傳統

阻塞式 i/o 的瓶頸在于不能處理過多的連接配接。非阻塞式 i/o 出現的目的就是為了解決這個瓶頸。非阻塞 io

處理連接配接的線程數和連接配接數沒有聯系,例如系統處理 10000 個連接配接,非阻塞 i/o 不需要啟動 10000 個線程,你可以用 1000

個,也可以用 2000 個線程來處理。因為非阻塞 io

處理連接配接是異步的,當某個連接配接發送請求到伺服器,伺服器把這個連接配接請求當作一個請求“事件”,并把這個“事件”配置設定給相應的函數處理。我們可以把這個處理

函數放到線程中去執行,執行完就把線程歸還,這樣一個線程就可以異步的處理多個事件。而阻塞式 i/o 的線程的大部分時間都被浪費在等待請求上了。

java nio

java.nio 包是 java 在 1.4 版本之後新增加的包,專門用來提高 i/o 操作的效率。

表 1 所示是 i/o 與 nio 之間的對比内容。

表 1. i/o vs nio

i/o

nio

面向流

面向緩沖

阻塞 io

非阻塞 io

選擇器

nio 是基于塊 (block) 的,它以塊為基本機關處理資料。在 nio 中,最為重要的兩個元件是緩沖 buffer 和通道

channel。緩沖是一塊連續的記憶體塊,是 nio

讀寫資料的中轉地。通道辨別緩沖資料的源頭或者目的地,它用于向緩沖讀取或者寫入資料,是通路緩沖的接口。channel

是一個雙向通道,即可讀,也可寫。stream 是單向的。應用程式不能直接對 channel 進行讀寫操作,而必須通過 buffer 來進行,即

channel 是通過 buffer 來讀寫資料的。

使用 buffer 讀寫資料一般遵循以下四個步驟:

寫入資料到 buffer;

調用 flip() 方法;

從 buffer 中讀取資料;

調用 clear() 方法或者 compact() 方法。

當向 buffer 寫入資料時,buffer 會記錄下寫了多少資料。一旦要讀取資料,需要通過 flip() 方法将 buffer 從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到 buffer 的所有資料。

一旦讀完了所有的資料,就需要清空緩沖區,讓它可以再次被寫入。有兩種方式能清空緩沖區:調用 clear() 或 compact()

方法。clear() 方法會清空整個緩沖區。compact()

方法隻會清除已經讀過的資料。任何未讀的資料都被移到緩沖區的起始處,新寫入的資料将放到緩沖區未讀資料的後面。

buffer 有多種類型,不同的 buffer 提供不同的方式操作 buffer 中的資料。

圖 1 buffer 接口層次圖

Java I/O 操作及優化建議

buffer 寫資料有兩種情況:

從 channel 寫到 buffer,如例子中 channel 從檔案中讀取資料,寫到 channel;

直接調用 put 方法,往裡面寫資料。

從 buffer 中讀取資料有兩種方式:

從 buffer 讀取資料到 channel;

使用 get() 方法從 buffer 中讀取資料。

buffer 的 rewin 方法将 position 設回 0,是以你可以重讀 buffer 中的所有資料。limit 保持不變,仍然表示能從 buffer 中讀取多少個元素(byte、char 等)。

clear() 和 compact() 方法

一旦讀完 buffer 中的資料,需要讓 buffer 準備好再次被寫入。可以通過 clear() 或 compact() 方法來完成。

如果調用的是 clear() 方法,position 将被設回 0,limit 被設定成 capacity 的值。換句話說,buffer 被清空了。buffer 中的資料并未清除,隻是這些标記告訴我們可以從哪裡開始往 buffer 裡寫資料。

如果 buffer 中有一些未讀的資料,調用 clear()

方法,資料将“被遺忘”,意味着不再有任何标記會告訴你哪些資料被讀過,哪些還沒有。如果 buffer

中仍有未讀的資料,且後續還需要這些資料,但是此時想要先寫些資料,那麼使用 compact() 方法。compact()

方法将所有未讀的資料拷貝到 buffer 起始處。然後将 position 設到最後一個未讀元素正後面。limit 屬性依然像 clear()

方法一樣,設定成 capacity。現在 buffer 準備好寫資料了,但是不會覆寫未讀的資料。

buffer 參數

buffer 有 3 個重要的參數:位置 (position)、容量 (capacity) 和上限 (limit)。

capacity 是指 buffer 的大小,在 buffer 建立的時候已經确定。

limit 當 buffer 處于寫模式,指還可以寫入多少資料;處于讀模式,指還有多少資料可以讀。

position 當 buffer

處于寫模式,指下一個寫資料的位置;處于讀模式,目前将要讀取的資料的位置。每讀寫一個資料,position+1,也就是 limit 和

position 在 buffer 的讀/寫時的含義不一樣。當調用 buffer 的 flip

方法,由寫模式變為讀模式時,limit(讀)=position(寫),position(讀) =0。

散射&聚集

nio 提供了處理結構化資料的方法,稱之為散射 (scattering) 和聚集 (gathering)。散射是指将資料讀入一組

buffer 中,而不僅僅是一個。聚集與之相反,指将資料寫入一組 buffer 中。散射和聚集的基本使用方法和對單個 buffer

操作時的使用方法相當類似。在散射讀取中,通道依次填充每個緩沖區。填滿一個緩沖區後,它就開始填充下一個,在某種意義上,緩沖區數組就像一個大緩沖區。

在已知檔案具體結構的情況下,可以構造若幹個符合檔案結構的 buffer,使得各個 buffer

的大小恰好符合檔案各段結構的大小。此時,通過散射讀的方式可以一次将内容裝配到各個對應的 buffer

中,進而簡化操作。如果需要建立指定格式的檔案,隻要先構造好大小合适的 buffer 對象,使用聚集寫的方式,便可以很快地建立出檔案。清單 1 以

filechannel 為例,展示如何使用散射和聚集讀寫結構化檔案。

清單 1. 使用散射和聚集讀寫結構化檔案

import java.io.file;

import java.io.fileinputstream;

import java.io.filenotfoundexception;

import java.io.fileoutputstream;

import java.io.ioexception;

import java.io.unsupportedencodingexception;

import java.nio.bytebuffer;

import java.nio.channels.filechannel;

public class nioscatteringandgathering {

public void createfiles(string tpath){

try {

bytebuffer bookbuf = bytebuffer.wrap("java 性能優化技巧".getbytes("utf-8"));

bytebuffer autbuf = bytebuffer.wrap("test".getbytes("utf-8"));

int booklen = bookbuf.limit();

int autlen = autbuf.limit();

bytebuffer[] bufs = new bytebuffer[]{bookbuf,autbuf};

file file = new file(tpath);

if(!file.exists()){

file.createnewfile();

} catch (ioexception e) {

// todo auto-generated catch block

e.printstacktrace();

}

fileoutputstream fos = new fileoutputstream(file);

filechannel fc = fos.getchannel();

fc.write(bufs);

fos.close();

} catch (filenotfoundexception e) {

bytebuffer b1 = bytebuffer.allocate(booklen);

bytebuffer b2 = bytebuffer.allocate(autlen);

bytebuffer[] bufs1 = new bytebuffer[]{b1,b2};

file file1 = new file(tpath);

fileinputstream fis = new fileinputstream(file);

filechannel fc = fis.getchannel();

fc.read(bufs1);

string bookname = new string(bufs1[0].array(),"utf-8");

string autname = new string(bufs1[1].array(),"utf-8");

system.out.println(bookname+" "+autname);

} catch (unsupportedencodingexception e) {

public static void main(string[] args){

nioscatteringandgathering nio = new nioscatteringandgathering();

nio.createfiles("c://1.txt");

輸出如下清單 2 所示。

清單 2. 運作結果

java 性能優化技巧 test

清單 3 所示代碼對傳統 i/o、基于 byte 的 nio、基于記憶體映射的 nio 三種方式進行了性能上的對比,使用一個有 400 萬資料的檔案的讀、寫操作耗時作為評測依據。

清單 3. i/o 的三種方式對比試驗

import java.io.bufferedinputstream;

import java.io.bufferedoutputstream;

import java.io.datainputstream;

import java.io.dataoutputstream;

import java.io.randomaccessfile;

import java.nio.intbuffer;

import java.nio.mappedbytebuffer;

public class niocomparator {

public void iomethod(string tpath){

long start = system.currenttimemillis();

dataoutputstream dos = new dataoutputstream(

new bufferedoutputstream(new fileoutputstream(new file(tpath))));

for(int i=0;i<4000000;i++){

dos.writeint(i);//寫入 4000000 個整數

if(dos!=null){

dos.close();

long end = system.currenttimemillis();

system.out.println(end - start);

start = system.currenttimemillis();

datainputstream dis = new datainputstream(

new bufferedinputstream(new fileinputstream(new file(tpath))));

dis.readint();

if(dis!=null){

dis.close();

end = system.currenttimemillis();

public void bytemethod(string tpath){

fileoutputstream fout = new fileoutputstream(new file(tpath));

filechannel fc = fout.getchannel();//得到檔案通道

bytebuffer bytebuffer = bytebuffer.allocate(4000000*4);//配置設定 buffer

bytebuffer.put(int2byte(i));//将整數轉為數組

bytebuffer.flip();//準備寫

fc.write(bytebuffer);

fileinputstream fin;

fin = new fileinputstream(new file(tpath));

filechannel fc = fin.getchannel();//取得檔案通道

fc.read(bytebuffer);//讀取檔案資料

fc.close();

bytebuffer.flip();//準備讀取資料

while(bytebuffer.hasremaining()){

byte2int(bytebuffer.get(),bytebuffer.get(),bytebuffer.get(),bytebuffer.get());//将 byte 轉為整數

public void mapmethod(string tpath){

//将檔案直接映射到記憶體的方法

filechannel fc = new randomaccessfile(tpath,"rw").getchannel();

intbuffer ib = fc.map(filechannel.mapmode.read_write, 0, 4000000*4).asintbuffer();

ib.put(i);

if(fc!=null){

filechannel fc = new fileinputstream(tpath).getchannel();

mappedbytebuffer lib = fc.map(filechannel.mapmode.read_only, 0, fc.size());

lib.asintbuffer();

while(lib.hasremaining()){

lib.get();

public static byte[] int2byte(int res){

byte[] targets = new byte[4];

targets[3] = (byte)(res & 0xff);//最低位

targets[2] = (byte)((res>>8)&0xff);//次低位

targets[1] = (byte)((res>>16)&0xff);//次高位

targets[0] = (byte)((res>>>24));//最高位,無符号右移

return targets;

public static int byte2int(byte b1,byte b2,byte b3,byte b4){

return ((b1 & 0xff)<<24)|((b2 & 0xff)<<16)|((b3 & 0xff)<<8)|(b4 & 0xff);

niocomparator nio = new niocomparator();

nio.iomethod("c://1.txt");

nio.bytemethod("c://2.txt");

nio.bytemethod("c://3.txt");

清單 3 運作輸出如清單 4 所示。

清單 4. 運作輸出

1139

906

296

157

234

125

除上述描述及清單 3 所示代碼以外,nio 的 buffer 還提供了一個可以直接通路系統實體記憶體的類

directbuffer。directbuffer 繼承自 bytebuffer,但和普通的 bytebuffer 不同。普通的

bytebuffer 仍然在 jvm 堆上配置設定空間,其最大記憶體受到最大堆的限制,而 directbuffer

直接配置設定在實體記憶體上,并不占用堆空間。在對普通的 bytebuffer 通路時,系統總是會使用一個“核心緩沖區”進行間接的操作。而

directrbuffer 所處的位置,相當于這個“核心緩沖區”。是以,使用 directbuffer

是一種更加接近系統底層的方法,是以,它的速度比普通的 bytebuffer 更快。directbuffer 相對于 bytebuffer

而言,讀寫通路速度快很多,但是建立和銷毀 directrbuffer 的花費卻比 bytebuffer 高。directbuffer 與

bytebuffer 相比較的代碼如清單 5 所示。

清單 5. directbuffer vs bytebuffer

public class directbuffervsbytebuffer {

public void directbufferperform(){

bytebuffer bb = bytebuffer.allocatedirect(500);//配置設定 directbuffer

for(int i=0;i<100000;i++){

for(int j=0;j<99;j++){

bb.putint(j);

bb.flip();

bb.getint(j);

bb.clear();

system.out.println(end-start);

for(int i=0;i<20000;i++){

bytebuffer b = bytebuffer.allocatedirect(10000);//建立 directbuffer

public void bytebufferperform(){

bytebuffer bb = bytebuffer.allocate(500);//配置設定 directbuffer

bytebuffer b = bytebuffer.allocate(10000);//建立 bytebuffer

directbuffervsbytebuffer db = new directbuffervsbytebuffer();

db.bytebufferperform();

db.directbufferperform();

運作輸出如清單 6 所示。

清單 6. 運作輸出

920

110

531

390

由清單 6 可知,頻繁建立和銷毀 directbuffer

的代價遠遠大于在堆上配置設定記憶體空間。使用參數-xx:maxdirectmemorysize=200m –xmx200m 在 vm

arguments 裡面配置最大 directbuffer 和最大堆空間,代碼中分别請求了 200m 的空間,如果設定的堆空間過小,例如設定

1m,會抛出錯誤如清單 7 所示。

清單 7. 運作錯誤

error occurred during initialization of vm

too small initial heap for new size specified

directbuffer 的資訊不會列印在 gc 裡面,因為 gc 隻記錄了堆空間的記憶體回收。可以看到,由于 bytebuffer

在堆上配置設定空間,是以其 gc 數組相對非常頻繁,在需要頻繁建立 buffer 的場合,由于建立和銷毀 directbuffer

的代碼比較高昂,不宜使用 directbuffer。但是如果能将 directbuffer 進行複用,可以大幅改善系統性能。清單 8 是一段對

directbuffer 進行監控代碼。

清單 8. 對 directbuffer 監控代碼

import java.lang.reflect.field;

public class mondirectbuffer {

class c = class.forname("java.nio.bits");//通過反射取得私有資料

field maxmemory = c.getdeclaredfield("maxmemory");

maxmemory.setaccessible(true);

field reservedmemory = c.getdeclaredfield("reservedmemory");

reservedmemory.setaccessible(true);

synchronized(c){

long maxmemoryvalue = (long)maxmemory.get(null);

long reservedmemoryvalue = (long)reservedmemory.get(null);

system.out.println("maxmemoryvalue="+maxmemoryvalue);

system.out.println("reservedmemoryvalue="+reservedmemoryvalue);

} catch (classnotfoundexception e) {

} catch (securityexception e) {

} catch (nosuchfieldexception e) {

} catch (illegalargumentexception e) {

} catch (illegalaccessexception e) {

運作輸出如清單 9 所示。

清單 9. 運作輸出

maxmemoryvalue=67108864

reservedmemoryvalue=0

java aio

aio 相關的類和接口:

java.nio.channels.asynchronouschannel:标記一個 channel 支援異步 io 操作;

java.nio.channels.asynchronousserversocketchannel:serversocket 的 aio 版本,建立 tcp 服務端,綁定位址,監聽端口等;

java.nio.channels.asynchronoussocketchannel:面向流的異步 socket channel,表示一個連接配接;

java.nio.channels.asynchronouschannelgroup:異步 channel

的分組管理,目的是為了資源共享。一個 asynchronouschannelgroup 綁定一個線程池,這個線程池執行兩個任務:處理 io

事件和派發 completionhandler。asynchronousserversocketchannel 建立的時候可以傳入一個

asynchronouschannelgroup,那麼通過 asynchronousserversocketchannel 建立的

asynchronoussocketchannel 将同屬于一個組,共享資源;

java.nio.channels.completionhandler:異步 io 操作結果的回調接口,用于定義在 io

操作完成後所作的回調工作。aio 的 api 允許兩種方式來處理異步操作的結果:傳回的 future 模式或者注冊

completionhandler,推薦用 completionhandler 的方式,這些 handler 的調用是由

asynchronouschannelgroup 的線程池派發的。這裡線程池的大小是性能的關鍵因素。

這裡舉一個程式範例,簡單介紹一下 aio 如何運作。

清單 10. 服務端程式

import java.net.inetsocketaddress;

import java.nio.channels.asynchronousserversocketchannel;

import java.nio.channels.asynchronoussocketchannel;

import java.nio.channels.completionhandler;

import java.util.concurrent.executionexception;

public class simpleserver {

public simpleserver(int port) throws ioexception {

final asynchronousserversocketchannel listener =

asynchronousserversocketchannel.open().bind(new inetsocketaddress(port));

//監聽消息,收到後啟動 handle 處理子產品

listener.accept(null, new completionhandler<asynchronoussocketchannel, void>() {

public void completed(asynchronoussocketchannel ch, void att) {

listener.accept(null, this);// 接受下一個連接配接

handle(ch);// 處理目前連接配接

@override

public void failed(throwable exc, void attachment) {

// todo auto-generated method stub

});

public void handle(asynchronoussocketchannel ch) {

bytebuffer bytebuffer = bytebuffer.allocate(32);//開一個 buffer

ch.read(bytebuffer).get();//讀取輸入

} catch (interruptedexception e) {

} catch (executionexception e) {

bytebuffer.flip();

system.out.println(bytebuffer.get());

// do something

清單 11. 用戶端程式

import java.util.concurrent.future;

public class simpleclientclass {

private asynchronoussocketchannel client;

public simpleclientclass(string host, int port) throws ioexception,

                                    interruptedexception, executionexception {

this.client = asynchronoussocketchannel.open();

future<?> future = client.connect(new inetsocketaddress(host, port));

future.get();

public void write(byte b) {

bytebuffer bytebuffer = bytebuffer.allocate(32);

system.out.println("bytebuffer="+bytebuffer);

bytebuffer.put(b);//向 buffer 寫入讀取到的字元

client.write(bytebuffer);

清單 12.main 函數

import org.junit.test;

public class aiodemotest {

@test

public void testserver() throws ioexception, interruptedexception {

simpleserver server = new simpleserver(9021);

thread.sleep(10000);//由于是異步操作,是以睡眠一定時間,以免程式很快結束

public void testclient() throws ioexception, interruptedexception, executionexception {

simpleclientclass client = new simpleclientclass("localhost", 9021);

client.write((byte) 11);

aiodemotest demotest = new aiodemotest();

demotest.testserver();

demotest.testclient();

結束語

i/o 與 nio 一個比較重要的差別是我們使用 i/o 的時候往往會引入多線程,每個連接配接使用一個單獨的線程,而 nio

則是使用單線程或者隻使用少量的多線程,每個連接配接共用一個線程。而由于 nio 的非阻塞需要一直輪詢,比較消耗系統資源,是以異步非阻塞模式 aio

就誕生了。本文對 i/o、nio、aio 等三種輸入輸出操作方式進行一一介紹,力求通過簡單的描述和執行個體讓讀者能夠掌握基本的操作、優化方法。

來源:51cto

下一篇: Hadoop