摘要:摘要本文大多技術圍繞調整磁盤文件但是有些內容也同樣適合網絡和窗口輸出。語言采取兩種截然不同的磁盤文件結構。一個是基于字節流,另一個是字符序列。但是在有些情況下軟件高速緩存能被用于加速。串行化串行化以標準格式將任意的數據結構轉換為字節流。
摘要
本文大多技術圍繞調整磁盤文件 I/O,但是有些內容也同樣適合網絡 I/O 和窗口輸出。
第一部分技術討論底層的I/O問題,然后討論諸如壓縮,格式化和串行化等高級I/O問題。然而這個討論沒有包含應用設計問題,例如搜索算法和數據結構,也沒有討論系統級的問題,例如文件高速緩沖。
Java語言采取兩種截然不同的磁盤文件結構。一個是基于字節流,另一個是字符序列。在Java 語言中一個字符由兩個字節表示,而不是像通常的語言如c語言那樣是一個字節。因此,從一個文件讀取字符時需要進行轉換。這個不同在某些情況下是很重要的, 就像下面的幾個例子將要展示的那樣。
低級I/O相關的問題:
緩沖
讀寫文本文件
格式化的代價
隨機訪問
高級I/O問題
壓縮
高速緩沖
分解
串行化
獲取文件信息
更多信息
加速I/O的基本規則
避免訪問磁盤
避免訪問底層的操作系統
避免方法調用
避免個別的處理字節和字符
很明顯這些規則不能在所有的問題上避免,因為如果能夠的話就沒有實際的I/O被執行。
使用緩沖加速文件讀取的示例:
對于一個1 MB的輸入文件,以秒為單位的執行時間是: | |
---|---|
FileInputStream的read方法,每次讀取一個字節,不用緩沖 | 6.9秒 |
BufferedInputStream的read方法使用BufferedInputStream | 0.9秒 |
FileInputStream的read方法讀取數據到直接緩沖 | 0.4秒 |
或者說在最慢的方法和最快的方法間是17比1的不同。
這個巨大的加速并不能證明你應該總是使用第三種方法,即自己做緩沖。這可能是一個錯誤的傾向特別是在處理文件結束事件時沒有仔細的實現。在可讀性上它也沒有其它方法好。但是記住時間花費在哪兒了以及在必要的時候如何矯正是很有用。方法2 或許是對于大多應用的 "正確" 方法.
方法 2 和 3 使用了緩沖技術, 大塊文件被從磁盤讀取,然后每次訪問一個字節或字符。緩沖是一個基本而重要的加速I/O 的技術,而且有幾個類支持緩沖(BufferedInputStream 用于字節, BufferedReader 用于字符)。
緩沖區越大I/O越快嗎?典型的Java緩沖區長1024 或者 2048 字節,一個更大的緩沖區有可能加速 I/O但比重很小,大約5 到10%。
第一個方法簡單的使用FileInputStream的read方法:
FileInputStream的read方法每次讀取文件的下一個字節,觸發了大量的底層運行時系統調用
優點:編碼簡單,適用于小文件
缺點:讀寫頻繁,不適用于大文件
</>復制代碼
import java.io.*;
public class intro1 {
public static void main(String args[]) {
if (args.length != 1) {
System.err.println("missing filename");
System.exit(1);
}
try {
FileInputStream fis = new FileInputStream(args[0]); // 建立指向文件的讀寫流
int cnt = 0;
int b;
while ((b = fis.read()) != -1 ) { // FileInputStream 的read方法每次 讀取文件一個字節
if (b == "
")
cnt++;
}
fis.close();
System.out.println(cnt);
}
catch (IOException e) {
System.err.println(e);
}
}
}
方法 2: 使用大緩沖區
第二種方法使用大緩沖區避免了上面的問題:
BufferedInputStream的read方法 把文件的字節塊讀入緩沖區,然后每次讀取一個字節,每次填充緩沖只需要訪問一次底層存儲接口
優點:避免每個字節的底層讀取,編碼相對不復雜
缺點:緩存占用了小量內存
</>復制代碼
import java.io.*;
public class intro2 {
public static void main(String args[]) {
if (args.length != 1) {
System.err.println("missing filename");
System.exit(1);
}
try {
FileInputStream fis = new FileInputStream(args[0]);
BufferedInputStream bis = new BufferedInputStream(fis); // 把文件讀取流指向緩沖區
int cnt = 0;
int b;
while ((b = bis.read()) != -1 ) { //BufferedInputStream 的read方法 把文件的字節塊獨
//入緩沖區BufferedInputStream,然后每次讀取一個字節
if (b == "
")
cnt++;
}
bis.close();
System.out.println(cnt);
}
catch (IOException e) {
System.err.println(e);
}
}
}
方法 3: 直接緩沖
FileInputStream的read方法直接讀入字節塊到直接緩沖buf,然后每次讀取一個字節。
優點:速度最快,
缺點:編碼稍微復雜,可讀性差,占用了小量內存,
</>復制代碼
import java.io.*;
public class intro3 {
public static void main(String args[]) {
if (args.length != 1) {
System.err.println("missing filename");
System.exit(1);
}
try {
FileInputStream fis = new FileInputStream(args[0]);
byte buf[] = new byte[2048];
int cnt = 0;
int n;
while ((n = fis.read(buf)) != -1 ) { // FileInputStream 的read方法直接讀入字節塊到
//直接緩沖buf,然后每次讀取一個字節
for (int i = 0; i < n; i++) {
if (buf[i] == "
")
cnt++;
}
}
fis.close();
System.out.println(cnt);
}
catch (IOException e) {
System.err.println(e);
}
}
}
方法4: 緩沖整個文件
緩沖的極端情況是事先決定整個文件的長度,然后讀取整個文件。
優點:把文件底層讀取降到最少,一次,
缺點:大文件會耗盡內存。
</>復制代碼
import java.io.*;
public class readfile {
public static void main(String args[]) {
if (args.length != 1) {
System.err.println("missing filename");
System.exit(1);
}
try {
int len = (int)(new File(args[0]).length());
FileInputStream fis = new FileInputStream(args[0]);
byte buf[] = new byte[len]; //建立直接緩沖
fis.read(buf); // 讀取整個文件
fis.close();
int cnt = 0;
for (int i = 0; i < len; i++) {
if (buf[i] == "
")
cnt++;
}
System.out.println(cnt);
}
catch (IOException e) {
System.err.println(e);
}
}
}
這個方法很方便,在這里文件被當作一個字節數組。但是有一個明顯得問題是有可能沒有讀取一個巨大的文件的足夠的內存。
緩沖的另一個方面是向窗口終端的文本輸出。缺省情況下, System.out ( 一個PrintStream) 是行緩沖的,這意味著在遇到一個新行符后輸出緩沖區被提交。
實際上向文件寫數據只是輸出代價的一部分。另一個可觀的代價是數據格式化。
考慮下面的字符輸出程序
性能對比結果為:這些程序產生同樣的輸出。運行時間是:
格式化方法 | 示例語句 | 運行時間 |
---|---|---|
簡單的輸出一個固定字符 | System.out.print(s); | 1.3秒 |
使用簡單格式"+" | String s = 字符+字符, System.out.print(s); | 1.8秒 |
使用java.text包中的 MessageFormat 類的對象方法 | String s = fmt.format(values); System.out.print(s); | 7.8秒 |
使用java.text包中的 MessageFormat 類的靜態方法 | 7.8*1.3秒 |
最慢的和最快的大約是6比1。如果格式沒有預編譯第三種方法將更慢,使用靜態的方法代替:
第三個方法比前兩種方法慢很多的事實并不意味著你不應該使用它,而是你要意識到時間上的開銷。
在國際化的情況下信息格式化是很重要的,關心這個問題的應用程序通常從一個綁定的資源中讀取格式然后使用它。
</>復制代碼
public class format1 {
public static void main(String args[]) {
final int COUNT = 25000;
for (int i = 1; i <= COUNT; i++) {
String s = "The square of 5 is 25
";
System.out.print(s);
}
}
}
方法2,使用簡單格式"+":
</>復制代碼
public class format2 {
public static void main(String args[]) {
int n = 5;
final int COUNT = 25000;
for (int i = 1; i <= COUNT; i++) {
String s = "The square of " + n + " is " +
n * n + "
";
System.out.print(s);
}
}
}
方法 3,第三種方法使用java.text包中的 MessageFormat 類的對象方法:
</>復制代碼
import java.text.*;
public class format3 {
public static void main(String args[]) {
MessageFormat fmt =
new MessageFormat("The square of {0} is {1}
");
Object values[] = new Object[2];
int n = 5;
values[0] = new Integer(n);
values[1] = new Integer(n * n);
final int COUNT = 25000;
for (int i = 1; i <= COUNT; i++) {
String s = fmt.format(values);
System.out.print(s);
}
}
}
方法 4,使用MessageFormat.format(String, Object[]) 類的靜態方法
</>復制代碼
import java.text.*;
public class format4 {
public static void main(String args[]) {
String fmt = "The square of {0} is {1}
";
Object values[] = new Object[2];
int n = 5;
values[0] = new Integer(n);
values[1] = new Integer(n * n);
final int COUNT = 25000;
for (int i = 1; i <= COUNT; i++) {
String s =
MessageFormat.format(fmt, values);
System.out.print(s);
}
}
}
這比前一個例子多花費1/3的時間。
隨機訪問的性能開銷RandomAccessFile 是一個進行隨機文件I/O(在字節層次上)的類。這個類提供一個seek方法,和 C/C++ 中的相似,移動文件指針到任意的位置,然后從那個位置字節可以被讀取或寫入。
seek方法訪問底層的運行時系統因此往往是消耗巨大的。一個更好的代替是在RandomAccessFile上建立你自己的緩沖,并實現一個直接的字節read方法。read方法的參數是字節偏移量(>= 0)。這樣的一個例子是:
這個程序簡單的讀取字節序列然后輸出它們。
適用的情況:如果有訪問位置,這個技術是很有用的,文件中的附近字節幾乎在同時被讀取。例如,如果你在一個排序的文件上實現二分法查找,這個方法可能很有用。
不適用的情況:如果你在一個巨大的文件上的任意點做隨機訪問的話就沒有太大價值。
</>復制代碼
import java.io.*;
public class ReadRandom {
private static final int DEFAULT_BUFSIZE = 4096;
private RandomAccessFile raf;
private byte inbuf[];
private long startpos = -1;
private long endpos = -1;
private int bufsize;
public ReadRandom(String name)
throws FileNotFoundException {
this(name, DEFAULT_BUFSIZE);
}
public ReadRandom(String name, int b)
throws FileNotFoundException {
raf = new RandomAccessFile(name, "r");
bufsize = b;
inbuf = new byte[bufsize];
}
public int read(long pos) {
if (pos < startpos || pos > endpos) {
long blockstart = (pos / bufsize) * bufsize;
int n;
try {
raf.seek(blockstart);
n = raf.read(inbuf);
}
catch (IOException e) {
return -1;
}
startpos = blockstart;
endpos = blockstart + n - 1;
if (pos < startpos || pos > endpos)
return -1;
}
return inbuf[(int)(pos - startpos)] & 0xffff;
}
public void close() throws IOException {
raf.close();
}
public static void main(String args[]) {
if (args.length != 1) {
System.err.println("missing filename");
System.exit(1);
}
try {
ReadRandom rr = new ReadRandom(args[0]);
long pos = 0;
int c;
byte buf[] = new byte[1];
while ((c = rr.read(pos)) != -1) {
pos++;
buf[0] = (byte)c;
System.out.write(buf, 0, 1);
}
rr.close();
}
catch (IOException e) {
System.err.println(e);
}
}
}
壓縮的性能開銷
Java提供用于壓縮和解壓字節流的類,這些類包含在java.util.zip 包里面,這些類也作為 Jar 文件的服務基礎 ( Jar 文件是帶有附加文件列表的 Zip 文件)。
壓縮的目的是減少存儲空間,同時被壓縮的文件在IO速度不變的情況下會減少傳輸時間。
壓縮時候要消耗CPU時間,占用內存。
壓縮時間=數據量/壓縮速度。
IO傳輸時間=數據容量/ IO速度。
傳輸數據的總時間=壓縮時間+I/O傳輸時間
壓縮是提高還是損害I/O性能很大程度依賴你的硬件配置,特別是和處理器和磁盤驅動器的速度相關。使用Zip技術的壓縮通常意味著在數據大小上減少50%,但是代價是壓縮和解壓的時間。一個巨大(5到10 MB)的壓縮文本文件,使用帶有IDE硬盤驅動器的300-MHz Pentium PC從硬盤上讀取可以比不壓縮少用大約1/3的時間。
壓縮的一個有用的范例是向非常慢的媒介例如軟盤寫數據。使用高速處理器(300 MHz Pentium)和低速軟驅(PC上的普通軟驅)的一個測試顯示壓縮一個巨大的文本文件然后在寫入軟盤比直接寫入軟盤快大約50% 。
下面的程序接收一個輸入文件并將之寫入一個只有一項的壓縮的 Zip 文件:
</>復制代碼
import java.io.*;
import java.util.zip.*;
public class compress {
public static void doit(String filein, String fileout) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(filein);
fos = new FileOutputStream(fileout);
ZipOutputStream zos = new ZipOutputStream(fos);
ZipEntry ze = new ZipEntry(filein);
zos.putNextEntry(ze);
final int BUFSIZ = 4096;
byte inbuf[] = new byte[BUFSIZ];
int n;
while ((n = fis.read(inbuf)) != -1)
zos.write(inbuf, 0, n);
fis.close();
fis = null;
zos.close();
fos = null;
}
catch (IOException e) {
System.err.println(e);
}
finally {
try {
if (fis != null)
fis.close();
if (fos != null)
fos.close();
}
catch (IOException e) {
}
}
}
public static void main(String args[]) {
if (args.length != 2) {
System.err.println("missing filenames");
System.exit(1);
}
if (args[0].equals(args[1])) {
System.err.println("filenames are identical");
System.exit(1);
}
doit(args[0], args[1]);
}
}
下一個程序執行相反的過程,將一個假設只有一項的Zip文件作為輸入然后將之解壓到輸出文件:
</>復制代碼
import java.io.*;
import java.util.zip.*;
public class uncompress {
public static void doit(String filein, String fileout) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(filein);
fos = new FileOutputStream(fileout);
ZipInputStream zis = new ZipInputStream(fis);
ZipEntry ze = zis.getNextEntry();
final int BUFSIZ = 4096;
byte inbuf[] = new byte[BUFSIZ];
int n;
while ((n = zis.read(inbuf, 0, BUFSIZ)) != -1)
fos.write(inbuf, 0, n);
zis.close();
fis = null;
fos.close();
fos = null;
}
catch (IOException e) {
System.err.println(e);
}
finally {
try {
if (fis != null)
fis.close();
if (fos != null)
fos.close();
}
catch (IOException e) {
}
}
}
public static void main(String args[]) {
if (args.length != 2) {
System.err.println("missing filenames");
System.exit(1);
}
if (args[0].equals(args[1])) {
System.err.println("filenames are identical");
System.exit(1);
}
doit(args[0], args[1]);
}
}
高速緩存
關于硬件的高速緩存的詳細討論超出了本文的討論范圍。但是在有些情況下軟件高速緩存能被用于加速I/O??紤]從一個文本文件里面以隨機順序讀取一行的情況,這樣做的一個方法是讀取所有的行,然后把它們存入一個ArrayList (一個類似Vector的集合類):
</>復制代碼
import java.io.*;
import java.util.ArrayList;
public class LineCache {
private ArrayList list = new ArrayList();
public LineCache(String fn) throws IOException {
FileReader fr = new FileReader(fn);
BufferedReader br = new BufferedReader(fr);
String ln;
while ((ln = br.readLine()) != null)
list.add(ln);
br.close();
}
public String getLine(int n) {
if (n < 0)
throw new IllegalArgumentException();
return (n < list.size() ? (String)list.get(n) : null);
}
public static void main(String args[]) {
if (args.length != 1) {
System.err.println("missing filename");
System.exit(1);
}
try {
LineCache lc = new LineCache(args[0]);
int i = 0;
String ln;
while ((ln = lc.getLine(i++)) != null)
System.out.println(ln);
}
catch (IOException e) {
System.err.println(e);
}
}
}
getLine 方法被用來獲取任意行。這個技術是很有用的,但是很明顯對一個大文件使用了太多的內存,因此有局限性。一個代替的方法是簡單的記住被請求的行最近的100 行,其它的請求直接從磁盤讀取。這個安排在局域性的訪問時很有用,但是在真正的隨機訪問時沒有太大作用?
分解
分解 是指將字節或字符序列分割為像單詞這樣的邏輯塊的過程。Java 提供StreamTokenizer 類, 像下面這樣操作:
</>復制代碼
import java.io.*;
public class token1 {
public static void main(String args[]) {
if (args.length != 1) {
System.err.println("missing filename");
System.exit(1);
}
try {
FileReader fr = new FileReader(args[0]);
BufferedReader br = new BufferedReader(fr);
StreamTokenizer st = new StreamTokenizer(br);
st.resetSyntax();
st.wordChars("a", "z");
int tok;
while ((tok = st.nextToken()) !=
StreamTokenizer.TT_EOF) {
if (tok == StreamTokenizer.TT_WORD)
;// st.sval has token
}
br.close();
}
catch (IOException e) {
System.err.println(e);
}
}
}
這個例子分解小寫單詞 (字母a-z)。如果你自己實現同等地功能,它可能像這樣:
</>復制代碼
import java.io.*;
public class token2 {
public static void main(String args[]) {
if (args.length != 1) {
System.err.println("missing filename");
System.exit(1);
}
try {
FileReader fr = new FileReader(args[0]);
BufferedReader br = new BufferedReader(fr);
int maxlen = 256;
int currlen = 0;
char wordbuf[] = new char[maxlen];
int c;
do {
c = br.read();
if (c >= "a" && c <= "z") {
if (currlen == maxlen) {
maxlen *= 1.5;
char xbuf[] =
new char[maxlen];
System.arraycopy(
wordbuf, 0,
xbuf, 0, currlen);
wordbuf = xbuf;
}
wordbuf[currlen++] = (char)c;
}
else if (currlen > 0) {
String s = new String(wordbuf,
0, currlen);
// do something with s
currlen = 0;
}
} while (c != -1);
br.close();
}
catch (IOException e) {
System.err.println(e);
}
}
}
第二個程序比前一個運行快大約 20%,代價是寫一些微妙的底層代碼。
StreamTokenizer 是一種混合類,它從字符流(例如 BufferedReader)讀取, 但是同時以字節的形式操作,將所有的字符當作雙字節(大于 0xff) ,即使它們是字母字符。
串行化 以標準格式將任意的Java數據結構轉換為字節流。例如,下面的程序輸出隨機整數數組:
</>復制代碼
import java.io.*;
import java.util.*;
public class serial1 {
public static void main(String args[]) {
ArrayList al = new ArrayList();
Random rn = new Random();
final int N = 100000;
for (int i = 1; i <= N; i++)
al.add(new Integer(rn.nextInt()));
try {
FileOutputStream fos = new FileOutputStream("test.ser");
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(al);
oos.close();
}
catch (Throwable e) {
System.err.println(e);
}
}
}
而下面的程序讀回數組:
</>復制代碼
import java.io.*;
import java.util.*;
public class serial2 {
public static void main(String args[]) {
ArrayList al = null;
try {
FileInputStream fis = new FileInputStream("test.ser");
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream ois = new ObjectInputStream(bis);
al = (ArrayList)ois.readObject();
ois.close();
}
catch (Throwable e) {
System.err.println(e);
}
}
}
注意我們使用緩沖提高I/O操作的速度。
有比串行化更快的輸出大量數據然后讀回的方法嗎?可能沒有,除非在特殊的情況下。例如,假設你決定將文本輸出為64位的整數而不是一組8字節。作為文本的 長整數的最大長度是大約20個字符,或者說二進制表示的2.5倍長。這種格式看起來不會快。然而,在某些情況下,例如位圖,一個特殊的格式可能是一個改 進。然而使用你自己的方案而不是串行化的標準方案將使你卷入一些權衡。
除了串行化實際的I/O和格式化開銷外(使用DataInputStream和 DataOutputStream), 還有其他的開銷,例如在串行化恢復時的創建新對象的需要。
注意DataOutputStream 方法也可以用于開發半自定義數據格式,例如:
</>復制代碼
import java.io.*;
import java.util.*;
public class binary1 {
public static void main(String args[]) {
try {
FileOutputStream fos = new FileOutputStream("outdata");
BufferedOutputStream bos = new BufferedOutputStream(fos);
DataOutputStream dos = new DataOutputStream(bos);
Random rn = new Random();
final int N = 10;
dos.writeInt(N);
for (int i = 1; i <= N; i++) {
int r = rn.nextInt();
System.out.println(r);
dos.writeInt(r);
}
dos.close();
}
catch (IOException e) {
System.err.println(e);
}
}
}
和:
</>復制代碼
import java.io.*;
public class binary2 {
public static void main(String args[]) {
try {
FileInputStream fis = new FileInputStream("outdata");
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);
int N = dis.readInt();
for (int i = 1; i <= N; i++) {
int r = dis.readInt();
System.out.println(r);
}
dis.close();
}
catch (IOException e) {
System.err.println(e);
}
}
}
這些程序將10個整數寫入文件然后讀回它們
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/67227.html
摘要:采用標準的傳輸格式,就能進行請求響應了某些特定的框架,可能會有自定義的通信格式。對于這種情況,采用多線程的模型再合適不過。只啟動固定的線程數來進行處理,既利用了多線程的處理,又控制了系統的資源消耗。在的包中,提供了相應的實現。 JAVA 中原生的 socket 通信機制 摘要:本文屬于原創,歡迎轉載,轉載請保留出處:https://github.com/jasonGeng88/blog...
摘要:請欣賞語法清單后端掘金語法清單翻譯自的,從屬于筆者的入門與實踐系列。這篇一篇框架整合友好的文章三后端掘金一理論它始終是圍繞數據模型頁面進行開發的。 RxJava 常用操作符 - Android - 掘金 原文地址 http://reactivex.io/documenta... ... RxJava 和 Retrofit 結合使用完成基本的登錄和注冊功能 - Android - 掘...
摘要:在中一般來說通過來創建所需要的線程池,如高并發原理初探后端掘金閱前熱身為了更加形象的說明同步異步阻塞非阻塞,我們以小明去買奶茶為例。 AbstractQueuedSynchronizer 超詳細原理解析 - 后端 - 掘金今天我們來研究學習一下AbstractQueuedSynchronizer類的相關原理,java.util.concurrent包中很多類都依賴于這個類所提供的隊列式...
摘要:在中一般來說通過來創建所需要的線程池,如高并發原理初探后端掘金閱前熱身為了更加形象的說明同步異步阻塞非阻塞,我們以小明去買奶茶為例。 AbstractQueuedSynchronizer 超詳細原理解析 - 后端 - 掘金今天我們來研究學習一下AbstractQueuedSynchronizer類的相關原理,java.util.concurrent包中很多類都依賴于這個類所提供的隊列式...
摘要:兩個序號,三個標志位含義表示所傳數據的序號。正常通信時為,第一次發起請求時因為沒有需要確認接收的數據所以為。終止位,用來在數據傳輸完畢后釋放連接。手機網站如,填寫。中的用法普通的用法分為和兩大類。 網站架構及其演變過程 基礎結構 網絡傳輸分解方式: 標準的 OSI 參考模型 TCP/IP 參考模型 showImg(https://segmentfault.com/img/remot...
閱讀 3672·2021-09-07 09:59
閱讀 728·2019-08-29 15:12
閱讀 815·2019-08-29 11:14
閱讀 1321·2019-08-26 13:27
閱讀 2675·2019-08-26 10:38
閱讀 3144·2019-08-23 18:07
閱讀 1284·2019-08-23 14:40
閱讀 1934·2019-08-23 12:38
极致性价比!云服务器续费无忧!
Tesla A100/A800、Tesla V100S等多种GPU云主机特惠2折起,不限台数,续费同价。
NVIDIA RTX 40系,高性价比推理显卡,满足AI应用场景需要。
乌兰察布+上海青浦,满足东推西训AI场景需要