BufferedInputStream和GZIPInputStream是在讀取文件數據中經常使用到的兩個類(至少後者在Linux系統中被廣泛使用)。一般來說,緩衝輸入數據是一種很好的想法,這在許多關於Java性能的書籍中都有描述。對於這些流,仍然有許多問題值得我們瞭解。
何時不需要緩衝
緩衝是用來減少來自輸入設備的單獨讀取操作數的數量,許多開發者往往忽視這一點,並經常將InputStream包含進BufferedInputStream中,如
1
|
final
InputStream is
= new
BufferedInputStream( new
FileInputStream( file ) );
|
是否使用緩衝的簡略規則如下:當你的數據塊足夠大的時候(100K+),你不需要使用緩衝,你可以處理任何長度的塊(不需要保證在緩衝前緩衝區中至少有N bytes可用字節)。在所有的其他情況下,你都需要緩衝輸入數據。最簡單的不需要緩衝的例子就是手動複製文件的過程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public
static
void
copyFile( final
File from, final
File to ) throws IOException {
final
InputStream is
= new
FileInputStream( from );
try
{
final
OutputStream os = new
FileOutputStream( to );
try
{
final
byte[] buf = new
byte[ 8192
];
int
read = 0 ;
while
( ( read = is .read(
buf ) ) != - 1
)
{
os.write(
buf, 0 ,
read );
}
}
finally
{
os.close();
}
}
finally
{
is .close();
}
}
|
注1:衡量文件複製的性能是非常困難的,因爲這很大程度上收到操作系統寫入緩存的影響。在我的機器上覆制一個4.5G的文件到相同的硬盤所花費的時間在68至107s之間變化。
注2:文件複製經常通過Java NIO實現,使用FileChannel.transferTo或者transferFrom的方法。使用這些方法不需要再在內核和用戶態之間頻繁的轉換(在用戶的java程序中將讀入數據轉換爲字節緩存,再通過內核調用將它複製回輸出文件中)。相反它們在內核模式中傳輸儘可能多的數據(直達231-1字節),儘可能做到不返回用戶的代碼中。因此,Java
NIO會使用較少的CPU週期,並騰出留給其他程序。然而,只有高負荷的環境下才能看到這當中的差異(在我的機器中,NIO模式的CPU總佔用率爲4%,而舊的流模式CPU總佔用率則爲8-9%)。以下是一個可能的Java
NIO實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
private
static
void
copyFileNio( final
File from, final
File to ) throws IOException {
final
RandomAccessFile inFile = new
RandomAccessFile( from, "r"
);
try
{
final
RandomAccessFile outFile = new
RandomAccessFile( to, "rw"
);
try
{
final
FileChannel inChannel = inFile.getChannel();
final
FileChannel outChannel = outFile.getChannel();
long
pos = 0 ;
long
toCopy = inFile.length();
while
( toCopy > 0
)
{
final
long bytes = inChannel.transferTo( pos, toCopy, outChannel );
pos
+= bytes;
toCopy
-= bytes;
}
}
finally
{
outFile.close();
}
}
finally
{
inFile.close();
}
}
|
緩衝大小
BufferedInputStream中默認的緩衝大小是8192個字節。緩衝大小實際上是從輸入設備中準備讀取的塊的平均大小。這就是爲什麼它經常值得精確地提高至64K(65536), 萬一有非常大的輸入文件 — 那些在512K和2M的文件,爲了更深一層地減少磁盤讀入的數量。許多專家也建議將此值設置爲4096的整數倍
— 一個普通磁盤扇區的大小。所以,不要將緩衝區的大小設置爲,像125000這樣的大小,取而代之的應該是像131072(128K)這樣的大小。
java.util.zip.GZIPInputStream 是一個能夠很好處理gzip文件輸入的輸入流。它經常被用來做這樣的事情:
1
|
final
InputStream is = new
GZIPInputStream( new
BufferedInputStream( new
FileInputStream( file ) ) );
|
這樣的初始化已經足夠好了,不過BufferedInputStream在此處是多餘的,因爲GZIPInputStream已經擁有了自己的內建緩衝區,它裏面有一個字節緩衝區(實際上,它是InflaterInputStream的成員),被用做此從底層流中讀取壓縮的數據,並且將其傳遞給一個inflater。這個緩衝區默認的大小是512字節,所以它必須被設置成一個更高的數值。一個更理想地使用GZIPInputStream的方式如下:
1
|
final
InputStream is = new
GZIPInputStream( new
FileInputStream( file ), 65536
);
|
BufferedInputStream.available
BufferedInputStream.available 方法有一個可能的性能問題,取決於你真正想要接收到的東西。它的標準實現將返回BufferedInputStream自身的內部緩衝區可用字節數和底層輸入流調用avalibale()結果的總和。所以,它將儘可能地返回一個精確的數值。但是在很多案例中,用戶想知道的僅僅是緩衝區中是否還有空間可用(
available() > 0). 在這種情況下,即使BufferedInputStream的緩衝區中只有一個字節餘留,我們都不需要去查詢底層的輸入流。這顯得非常重要,如果我們有一個FileInputStream包含在BufferedInputStream中–這樣的優化會節省我們在FileInputStream.available()中的磁盤訪問時間。
幸運的是,我們可以簡單地解決這樣的問題。BufferedInputStream不是一個final類,所以我們可以繼承並且重載available方法。我們可以看看JDK的源代碼着手準備。從這裏我們還可以發現Java
6中的實現有一個bug — 如果BufferedInputStream可用的字節數和底層流available()調用結果的總和大於Integer.MaxVALUE,這樣就會因爲溢出而返回一個負數結果,不過這在Java
7中已經得到了解決。
以下是我們改進的實現,它將返回BufferedInputStream的內置緩衝區中可用的字節數,又或者是,如果它裏面沒有剩餘的字節數,底層流中的available()方法會被調用,並且返回調用後的結果。在大多數情況下,這種實現會極少調用到底層流中的available()方法,因爲當BufferedInputStream緩衝區被讀取到最後,這個類會讀取從底層流中讀取更多的數據,所以,我們只會在輸入文件的末尾中調用底層流的available()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
class
FasterBufferedInputStream extends
BufferedInputStream
{
public
FasterBufferedInputStream(InputStream in, int
size) {
super (in,
size);
}
public
int
available() throws
IOException {
if
(in == null )
throw
new
IOException( "Stream
closed"
);
final
int
n = count - pos;
return
n > 0
? n : in.available();
}
}
|
爲了測試這個實現,我嘗試使用標準版的和改進版的BufferedInputStream去讀取4.5G的文件,它們都有64K的緩衝區大小,並且每讀取512或者1024字節的時候就調用一次available()方法。乾淨的測試需要操作系統在每一次測試之後重啓以清除磁盤緩存。於是我決定在熱身階段讀取文件,當文件已經在磁盤緩存時就用兩種方法測試性能。測試顯示,標準類的運行時間與available()調用的數量呈線性關係。而改進的方法運行時間看起來卻與調用的次數無關。
|
standard, once per 512 bytes |
improved, once per 512 bytes |
standard, once per 1024 bytes |
improved, once per 1024 bytes |
Java 6 |
17.062 sec |
2.11 sec |
9.592 sec |
2.047 sec |
Java 7 |
17.337 sec |
2.125 sec |
9.748 sec |
2.044 sec |
這裏是測試的源代碼:
1
2
3
4
5
6
7
8
9
10
11
|
private
static
void
testRead( final
InputStream is ) throws
IOException {
final
long
start = System.currentTimeMillis();
final
byte []
buf = new
byte [
512
];
while
( true
)
{
if
( is.available() == 0
) break ;
is.read(
buf );
}
final
long
time = System.currentTimeMillis() - start;
System.out.println(
"Impl:
"
+ is.getClass().getCanonicalName() + "
time = "
+ time / 1000.0
+ "
sec" );
}
|
1
2
3
4
|
使用以下的聲明變量調用以上方法:
final
InputStream is1 = new
BufferedInputStream( new
FileInputStream( file ), 65536
);
and
final
InputStream is2 = new
FasterBufferedInputStream( new
FileInputStream( file ), 65536
);
|
總結
-
BufferedInputStream和GZIPInputStream 都有內建的緩衝區。前者默認的緩衝大小是8192字節,後者則爲512字節。一般而言,它值得增加任何它們的整數倍大小到至少65536。
-
不要使用BufferedInputStream作爲GZIPInputStream的輸入,相反,顯示地在構造器中設置GZIPInputStream的緩存大小。雖然,保持BufferedInputStream仍然是安全的。
-
如果你有一個new BufferedInputStream( new FileInputStream( file ) )對象,並且需要頻繁地調用它的available方法(例如,每輸入一次信息都需要調用一次或者兩次),考慮重載 BufferedInputStream.available方法,它將極大地提高文件讀取的速度。
|