認識 NIO 類
從 JDK 1.4 開始,Java 有提供一個位於 java.nio
Package 的 IO 類別庫,其目的是為了提高 IO 操作效率,其關鍵類型有四個
NIO 相關類 | 功能概述 |
---|---|
Buffer | 可控制的緩衝區,存放臨時資料 |
Charset | 具有將 JVM 預設的 Unicode 編碼轉換為其他編碼的類(或反轉為 Unicode) |
Channel | 資料傳輸通道,能將多個不同 Buffer 的資料彙聚(讀取、寫入) |
Selector | 支援 非同步 IO,也就是「Not Blocking IO」,在撰寫伺服器程式時常被使用到 |
●
Selector
使用的範例請看另一篇「Socket 通訊 - TCP、UDP 應用 / NIO Socket」
Buffer 概述:提高 IO 效率
以往資料的輸出、輸入往往比較耗時,如果有配合使用緩衝區 Buffer,就可提高 I/O 的操作效率,其功能有:
A. 減少實際的物理讀寫次數
B. 緩衝區的區塊記憶體被重復使用,減少動態分配、回收的次數
● 它與傳統的
BufferedInputStream
、BufferedOutputStream
... 等等(Buffered 開頭的類)相比起來差在哪?以往的緩衝類無法讓使用者直接操縱 Buffer(傳統類別綁定類、Buffer 較不利於重用),而新的 Buffer 可以讓使用者直接操縱,解耦了資料、資料操控
Buffer 類關係:屬性、方法、創建
● Buffer 類關係圖如下
● Buffer 類的共通屬性、方法如下
● 共通屬性
屬性 | 功能概述 |
---|---|
capacity | 標明該 緩衝區的大小 |
limit | 該緩衝區的 終點(可以不與 capacity 相同),該值是可以修改變動的 |
position | 該 緩衝區的指標,指向下一個讀寫單元的位置 |
三者關係:
capacity
>=limit
>=position
>= 0
● 共通方法
方法名 | 影響 position | 影響 limit | 影響 capacity |
---|---|---|---|
clear | position 修改為 0 | limit 改為 capacity | - |
flip | position 修改為 0 | limit 改為 position | - |
rewind | position 修改為 0 | - | - |
其中還有幾個是緩衝類都具有的方法
方法名 | 功能概述 |
---|---|
get | 可以用來相對讀取、絕對讀取 |
put | 可以用來相對寫入、絕對寫入 |
● 而 Buffer 並未公開建構函數,必須 透過指定靜態函數取得 Buffer 物件
靜態方法 | 特色 |
---|---|
allocate | 產生指定容量的 Buffer |
directAllocate | 同上,不過它的緩衝區屬於「直接緩衝區」,與作業系統有更好的耦合,可進一步提供 IO 速度 |
● 但相對的
directAllocate
的代價是較高的,通常只有在 緩衝區大、並長期存在、不斷重用 的狀況下會去使用代價如下
A. 消耗 Native 內存
B. 不受 JVM 管控
C. 銷毀、創建需要與 Native 作業系統通訊
Channel 概述:通道
通道是用來連接緩衝區(Buffer
)與資料來源、資料輸出終點的通道
Channel 類關係:方法
● Channel
界面指宣告兩個方法
Channel 方法 | 說明 |
---|---|
close | 關閉通道 |
isOpen | 判斷通道是否打開 |
● JDK 在實作時,通道會在建立時開啟,一但關閉通道,就不會開啟
● Channel
類關係如下
Channel 拓展介面 | 功能概述 |
---|---|
ReadableByteChannel | 唯讀的方法;read(ByteBuffer) |
WritableByteChannel | 唯寫的方法;write(ByteBuffer) |
ByteChannel | 支持讀、寫的類並擴充以上介面 |
ScatteringByteChannel | 可以有多個 ByteBuffer,將資料源頭讀取到的資料依序填充到指定的 ByteBuffer;read(ByteBuffer[]) |
GatheringByteChannel | 可以有多個 ByteBuffer,將多個 ByteBuffer 輸出到最終檔案中;write(ByteBuffer[]) |
FileChannel | 與「檔案」產生相關聯的類,代表一個與檔案相連的通道 |
●
FileChannel
類也不公開建構函數,所以需要透過以往 IO 類獲得
FileInputStream
、FileOutputStream
、RandomAcessFile
類別中的getChannel
方法來取得實體物件
Charset 概述:指定編碼
Charset
類別的每個實體代表「特定的字元編碼類型」,它是個抽象類,JVM 支持的字元編碼都會繼承該類
Charset 轉換編碼
● 使用 Charset
轉換編碼的方式如下,它會透過 Charset 類返回一個轉換過得 ByteBuffer
A. 轉換 JVM 的 Unicode 為特定字元編碼
// 使用 UTF-8 編碼
ByteBuffer utf8Encode = StandardCharsets.UTF_8.encode("123");
// 使用 ASCII 編碼
CharBuffer asciiDecoded = StandardCharsets.US_ASCII.decode("123");
StandardCharsets 提供簡易的標準編碼,它們都是 Charset 的實作類
B. 將特定編碼轉為 JVM 的 Unicode 編碼
// 使用 UTF-8 編碼
ByteBuffer utf8Encode = StandardCharsets.UTF_8.encode(originalText);
// 使用 UTF-8 解碼
CharBuffer utf8Decode = StandardCharsets.UTF_8.decode(utf8Encode);
// 使用 ASCII 編碼
ByteBuffer asciiEncode = StandardCharsets.US_ASCII.encode(originalText);
// 使用 ASCII 解碼
CharBuffer asciiDecode = StandardCharsets.UTF_8.decode(asciiEncode);
NIO 使用介紹
字元編碼轉換 Charset
● 首先先來看 Buffer、Charset 編碼的關係;ByteBuffer 可以存放 Byte 資料,但它並不關心放入的數據是怎樣被編碼,所以取出數據時要注意編碼轉換
Charset 範例如下
A. 將資料以 UTF-8 編碼放入 ByteBuffer,列印會變成亂碼
static void bufferPutUtf8() {
// 將資料以 UTF-8 編碼放入 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.wrap("安安"
.getBytes(StandardCharsets.UTF_8));
// 轉換 CharBuffer,CharBuffer 會轉換錯誤
CharBuffer charBuffer = byteBuffer.asCharBuffer();
System.out.println(charBuffer);
}
● 正確方式
如果 ByteBuffer 中存放的是 Unicode 字元,在透過
asCharBuffer
方法轉換時,就可以轉換為正確字元;錯誤(亂碼)原因: 當前範例存放的卻是
UTF-8
編碼,所以會出錯
B. 使用 Unicode 編碼
由於 Java String 採用 Unicode 編碼,所以可以正確打印並顯示
static void bufferPutUnicode() {
ByteBuffer byteBuffer = ByteBuffer.wrap("安安"
.getBytes(StandardCharsets.UTF_16BE)); // UTF_16BE 就是 Unicode
CharBuffer charBuffer = byteBuffer.asCharBuffer();
System.out.println(charBuffer);
}
C. Charset 使用 decode
使用 Charset 指定方式解碼後,取得的 CharBuffer 就會是正確的
static void bufferPutUtf8UseDecode() {
ByteBuffer byteBuffer = ByteBuffer.wrap("安安"
.getBytes(StandardCharsets.UTF_8));
Charset charset = Charset.forName("UTF-8");
CharBuffer charBuffer = charset.decode(byteBuffer);
System.out.println(charBuffer);
}
● 使用 Charset 手動設定編碼(decode
、encode
)範例:
static void useCharset() {
// 取得 Big5 編碼方起是
Charset charset = Charset.forName("Big5");
// 使用 Big5 編碼
ByteBuffer byteBuffer = charset.encode("安安你好呀!");
// 轉換 Big5 編碼為
CharBuffer charBuffer = charset.decode(byteBuffer);
System.out.println(charBuffer);
}
FileChannel 讀寫文件
● 接下來我們會使用 FileChannel 來操控文件,跟以往的 IO 比較起來會多出 Channel、Buffer、Charset 這些角色
● 寫入資料
A. FileChannel
類:
使用 FileOutputStream
、RandomAccessFile
取得 FileChannel
類
B. ByteBuffer
類:
由於對 Channel
寫入資料需要使用 ByteBuffer
類,所以使用 ByteBuffer#wrap
取得 ByteBuffer
● 讀取資料
A. FileChannel
類:
使用 FileInputStream
取得 FileChannel
類
B. ByteBuffer
類:
由於對 Channel
讀取資料需要使用 ByteBuffer
類,所以使用 ByteBuffer#allocate
創建一塊 ByteBuffer
區域
C. Charset
類:
由於從檔案中讀取的資料為 Byte,並非系統可認得的數據,所以使用 Charset
#defaultCharset
取得當前平台的編碼;並使用 decode
解碼 Byte 數據
static void readFile(String fileName) {
try(FileInputStream fis = new FileInputStream(fileName)) {
FileChannel fc = fis.getChannel();
// 創建 ByteBuffer 區塊 (1024 Byte 大小)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
fc.read(byteBuffer);
byteBuffer.flip(); // limit 改為 position
Charset cs = Charset.defaultCharset();
System.out.println(cs.decode(byteBuffer));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
● 使用範例、結果:
class FileChannelUsage {
public static void main(String[] args) {
String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello5.txt";
writeFile(targetFile);
readFile(targetFile);
}
}
控制 Buffer 緩衝區
● 我們前面有說到 Buffer 與傳統 IO 的差異在於,可以讓使用者手動操控 Buffer;以下來個範例,並觀察如何使用 Buffer,並觀察 Buffer 的狀態
class BufferUsage {
static void showBufferInfo(ByteBuffer byteBuffer) {
// 讀取 position, limit, capacity 資訊
System.out.println("position: " + byteBuffer.position()
+ ", limit: " + byteBuffer.limit()
+ ", capacity: " + byteBuffer.capacity());
}
public static void main(String[] args) {
String existFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello5.txt";
String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello6.txt";
try(FileInputStream fis = new FileInputStream(existFile);
FileOutputStream fos = new FileOutputStream(targetFile)) {
FileChannel inputChannel = fis.getChannel();
FileChannel outputChannel = fos.getChannel();
int limitVal = 5;
// 設定 Buffer 大小為 10 Byte
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
while (inputChannel.read(byteBuffer) != -1) {
// 設定上限值
byteBuffer.limit(limitVal++);
System.out.print("Start - ");
showBufferInfo(byteBuffer);
byteBuffer.flip(); // position 改為 0, limit 改為 position
System.out.print("After flip - ");
showBufferInfo(byteBuffer);
int written = outputChannel.write(byteBuffer);
System.out.println("The byte be written=(" + written + ")");
byteBuffer.clear(); // position 改為 0, limit 改為 capacity
System.out.print("After clear - ");
showBufferInfo(byteBuffer);
System.out.println("--------------------------");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
從結果,可以發現幾件事
A. Buffer 的 limit 值預設與 capacity 相同
B. 每次 read 都會自動填滿 position 到 limit 之間的數據空間,並且 不會超出 limit!
C. 每次 write 也只會將 position 到 limit 之間的數據寫入,並且 不會超出 limit!
FileInputStream、FileOutputStream 快速複製
● Channel
的子類 FileChannel
有提供兩個方便的方法,可以透過 Channel 快速複製檔案
A. transferTo
方法:
將 WritableByteChannel
資料寫入到預計要輸出的 Channel 中
B. transferFrom
方法:
將 ReadableByteChannel
資料寫入到預計要輸出的 Channel 中
class FastCopy {
public static void main(String[] args) {
String existFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello5.txt";
String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello6.txt";
try(FileInputStream fis = new FileInputStream(existFile);
FileOutputStream fos = new FileOutputStream(targetFile)) {
FileChannel inputChannel = fis.getChannel();
FileChannel outputChannel = fos.getChannel();
// 將 inputChannel 資料傳遞到 outputChannel
long copySize = inputChannel.transferTo(0, inputChannel.size(), outputChannel);
System.out.println("Copy to outputChannel=(" + copySize + ")");
// 將 inputChannel 資料傳遞到 outputChannel
copySize = outputChannel.transferFrom(inputChannel, 0, inputChannel.size());
System.out.println("Copy from inputChannel=(" + copySize + ")");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
常見的 NIO 操作
ByteBuffer 產生:緩衝區視圖
● ByteBuffer 提供 asCharBuffer
、asIntBuffer
、asFloatBuffer
... 等等方法,這些方法可以用來生成緩衝區視圖
● 緩衝區視圖是什麼?(簡單來說,就是讓我看見記憶體的數據)
緩衝區視圖 允許程式設計師以抽象的方式查看記憶體中的數據,而不需要直接操作內存,從而提高了程式碼的 可讀性 和 可維護性
這種方法通常用於低級別的編程,如網絡編程、文件I/O和編碼/解碼操作,但在高級別的應用程式中也可以有用
範例如下
A. ByteBuffer 轉換後的視圖緩衝區,也會重新分配每格數據的大小
class BufferView {
static void showBufferInfo(Buffer buffer) {
System.out.println("position: " + buffer.position()
+ ", limit: " + buffer.limit()
+ ", capacity: " + buffer.capacity());
}
public static void main(String[] args) {
// 空間為 8 Byte
ByteBuffer byteBuffer = ByteBuffer.allocate(4);
// hasRemaining: 檢查當前 position ~ limit 之間是否還有數據
while (byteBuffer.hasRemaining()) {
System.out.println(byteBuffer.get());
}
System.out.println("--- Byte After get ---");
showBufferInfo(byteBuffer);
// 將 Position 設置為 0
byteBuffer.rewind();
System.out.println("--- AByte fter rewind ---");
showBufferInfo(byteBuffer);
// 轉換為 char 緩衝視圖
CharBuffer charBuffer = byteBuffer.asCharBuffer();
System.out.println("--- Byte After asCharBuffer ---");
showBufferInfo(charBuffer);
// 放入數據
charBuffer.put("你好");
while (byteBuffer.hasRemaining()) {
System.out.println(byteBuffer.get());
}
System.out.println("--- Char After get ---");
showBufferInfo(charBuffer);
}
}
從結果可以發現,對於 4 byte 空間數據,不同緩衝區會有不同解釋
● ByteBuffer 解釋為 4 capacity,代表一個數據擁有 1 Byte 空間
● CharBuffer 則解釋為 2 capacity,代表一個數據擁有 2 Byte 空間
B. ByteBuffer 可以取出指定數據的格式
class BufferView2 {
public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.asCharBuffer().put("安");
System.out.println(byteBuffer.getChar());
byteBuffer.rewind();
byteBuffer.asIntBuffer().put(666);
System.out.println(byteBuffer.getInt());
byteBuffer.rewind();
byteBuffer.asFloatBuffer().put(222F);
System.out.println(byteBuffer.getFloat());
}
}
文件應設緩衝區:MappedByteBuffer
● MappedByteBuffer
用來建立、修改那些因「太大不能放入記憶體的檔案」!(類似於 mmap 的功能)
MappedByteBuffer
是透過檔案映射記憶體的方式來達到這個操作,也就是說該檔案會與某塊物理記憶體產生關聯
● 而 MappedByteBuffer
也不能直接創建,必須透過 FileChannel#map
方法取得實例
其中的 MapMode
參數會對應到該緩衝區的相關權限
MapMode | 概述 |
---|---|
READ_ONLY | 只能讀取該映射記憶體區塊 |
READ_WRITE | 可對該映射記憶體區塊進行讀寫 |
PRIVATE | 這次修改不會被保存到檔案中、並且其他程式不可見(也就是操作不會影響原文件) |
●
PRIVATE
時,會使用 Copy-on-Write 技術,對於文件的存取速度會提高許多
使用範例如下
class MappedByteBufferUsage {
public static void main(String[] args) {
String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello7.txt";
try(RandomAccessFile raf = new RandomAccessFile(targetFile, "rw")) {
int capacity = 0x7A12000; // 128MB
MappedByteBuffer mbb = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, capacity);
mbb.put("安安".getBytes(StandardCharsets.UTF_8));
// limit fix to current position
mbb.flip();
System.out.println(StandardCharsets.UTF_8.decode(mbb));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
檔案大小也確實是 128M
文件同步:分區加鎖 🔒
● 在多執行序(線程)中如果只是單單讀取文件,那是安全操作,但如果 有涉及到文件的「寫」,那就是不安全操作
● NIO 庫則有引入檔案加鎖 🔒 機制,允許程式 同步存取共享檔案
● Java 使用的檔案鎖,對於作業系統的其他執行序(
Thread
)是可見的操作因為這種檔案鎖直接映射到本地作業系統的加鎖 🔒 工具,這個鎖由作業系提供,並非 Java 自身提供
其中,關於鎖有提供幾個方法,如下表
方法 | 說明 |
---|---|
tryLock | 嘗試獲取鎖,如果無法獲取,則直接返回 |
lock | 同樣是獲取鎖,但是如果無法獲取它會一直等待(該線程會被掛起 Blocking) |
並且這兩個方法都有以下參數,這些參數較為特別
參數 | 說明 |
---|---|
position、size | 可以對同一個文件的不同區塊進行鎖定,不會全部鎖住;範圍是 position ~ position+size |
shared: Boolean | 決定是否使用共享鎖 |
● 共享鎖、排他鎖?
一個檔案中有兩把不同的鎖
graph TD; subgraph 檔案 共享鎖 排他鎖 end● 共享鎖:
當一個執行序獲得該檔案的共享鎖時,其他執行序仍可獲得該檔案的共享鎖,但不能獲得該檔案的排他鎖
● 排他鎖:
當一個執行序已經獲得檔案的排他鎖時,其他執行不允許獲得 共享鎖、排他鎖
● 分區加鎖 🔒 範例如下
詳細說明請看註解
class SynAccessFile {
static class Modifier extends Thread {
private final ByteBuffer byteBuffer;
private final FileChannel fileChannel;
private final int lockStart, lockEnd;
Modifier(FileChannel fileChannel, ByteBuffer byteBuffer, int start, int end) {
this.fileChannel = fileChannel;
this.lockStart = start;
this.lockEnd = end;
// 設定 Buffer
byteBuffer.limit(end);
byteBuffer.position(start);
// 獲得處理區域緩衝區,該緩衝區對應檔案的映射區
this.byteBuffer = byteBuffer.slice();
// Start thread
start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ", Lock range=(" + lockStart + "~" + lockEnd + ")");
try {
// 這裡記得鎖定區塊
FileLock fileLock = fileChannel.lock(lockStart, lockEnd, false);
while (byteBuffer.position() < byteBuffer.limit() - 1) {
// ByteBuffer#put, ByteBuffer#get
// 都會移動 position
byteBuffer.put((byte) (byteBuffer.get() + 1));
}
fileLock.release();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
String targetFile = "/var/folders/qt/468b6d6j6xxffzc4tydgnj800000gq/T/Hello8.txt";
// 這裡不能使用 try-with-resouces,
// 否則會造成檔案還沒寫入就被 close 掉
// try(RandomAccessFile raf = new RandomAccessFile(targetFile, "rw")) {
try {
RandomAccessFile raf = new RandomAccessFile(targetFile, "rw");
FileChannel fileChannel = raf.getChannel();
int capacity = 0x2710; // 10KB
MappedByteBuffer mbb = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, capacity);
// 一半寫 A
for (int i = 0; i < capacity / 2; i++) {
mbb.put((byte) 'A');
}
// 一半寫 B
for (int i = capacity / 2; i < capacity; i++) {
mbb.put((byte) 'B');
}
// Create Thread, 訪問上半
new Modifier(fileChannel, mbb, 0, capacity / 2 - 1);
// Create Thread, 訪問下半
new Modifier(fileChannel, mbb, capacity / 2, capacity);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
更多的 Java 語言相關文章
Java 語言深入
● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識
此外,我們還涵蓋了物件創建、函數式編程、註解應用以及泛型的深入分析,幫助您提升在實際開發中的技能和效率
點擊以下連結,開始你的學習之旅~
● 深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節
● 深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構
● 深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較
● 深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用
● 認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用
Java IO 相關文章
● 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
深入 Java 多執行緒
● 這一系列文章將帶你深入了解 Java 多執行緒技術的各個方面,從基礎知識到進階應用,涵蓋了多執行緒編程的核心概念與實踐
深入 Java 物件導向
● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!