NIO: Channels và Buffers
Sau bài này, bạn sẽ:
- Hiểu Channels (two-way I/O) và Buffers (memory containers) trong Java NIO
- Thành thạo ByteBuffer operations: position, limit, capacity, flip(), clear(), compact()
- Sử dụng FileChannel để đọc/ghi files với performance cao hơn streams
- Áp dụng memory-mapped files (MappedByteBuffer) cho large files và shared memory
- So sánh performance: Stream I/O vs Channel/Buffer vs Memory-mapped files
Bài trước: Streams, Readers và Writers — Đã học về stream hierarchy và decorator pattern. Bài này sẽ tìm hiểu NIO với Channels và Buffers - approach mới cho high-performance I/O.
NIO Overview
Java NIO (New I/O) được giới thiệu từ Java 1.4 với ba core components:
- Channels - Kênh I/O hai chiều (read + write)
- Buffers - Container cho dữ liệu trong memory
- Selectors - I/O multiplexing (sẽ học ở bài khác)
┌──────────────────────────────────────────────┐
│ NIO Architecture │
├──────────────────────────────────────────────┤
│ │
│ Application │
│ ↓ │
│ Buffer (in memory) │
│ ↓ │
│ Channel (connection to I/O) │
│ ↓ │
│ File / Socket / Pipe │
│ │
└──────────────────────────────────────────────┘
NIO vs IO - Khác biệt chính
| java.io | java.nio |
|---|---|
| Stream-oriented (dòng) | Buffer-oriented (bộ đệm) |
| Blocking (đồng bộ) | Non-blocking (bất đồng bộ) |
| Đọc tuần tự, 1 chiều | Có thể di chuyển qua lại trong buffer |
| Không có Selectors | Có Selectors (I/O multiplexing) |
- High-performance servers (xử lý nhiều connections)
- Large file processing (memory-mapped files)
- Cần non-blocking I/O
- Không cần dùng NIO cho simple file reading/writing!
Buffers - Container cho dữ liệu
Buffer hierarchy
Buffer (abstract)
├── ByteBuffer ⭐ Quan trọng nhất!
├── CharBuffer
├── ShortBuffer
├── IntBuffer
├── LongBuffer
├── FloatBuffer
└── DoubleBuffer
ByteBuffer - Core buffer class
ByteBuffer là buffer được dùng nhiều nhất, có thể chứa:
- Raw bytes
- Primitive types (int, long, float, etc.)
- Arrays
Tạo ByteBuffer:
import java.nio.ByteBuffer;
public class ByteBufferCreation {
public static void main(String[] args) {
// 1. allocate() - Heap buffer (trong Java heap)
ByteBuffer buffer1 = ByteBuffer.allocate(1024); // 1KB
System.out.println("Heap buffer created");
// 2. allocateDirect() - Direct buffer (ngoài heap, native memory)
ByteBuffer buffer2 = ByteBuffer.allocateDirect(1024);
System.out.println("Direct buffer created");
// 3. wrap() - Wrap existing byte array
byte[] array = new byte[1024];
ByteBuffer buffer3 = ByteBuffer.wrap(array);
System.out.println("Wrapped buffer created");
// wrap() với offset và length
ByteBuffer buffer4 = ByteBuffer.wrap(array, 10, 100);
// Sử dụng array[10..109]
}
}
Direct vs Heap ByteBuffer Comparison
Heap buffer vs Direct buffer:
| Heap Buffer | Direct Buffer |
|---|---|
| Trong Java heap | Ngoài heap (native memory) |
| GC quản lý | Không do GC quản lý |
| Allocate/deallocate nhanh | Allocate/deallocate chậm |
| I/O chậm hơn | I/O nhanh hơn (zero-copy) |
| Dùng cho: short-lived buffers | Dùng cho: long-lived, frequent I/O |
Heap Buffer (ByteBuffer.allocate()):
Application → Heap Buffer → JVM copies to OS buffer → I/O
↑
Extra copy step! (slower)
Direct Buffer (ByteBuffer.allocateDirect()):
Application → Direct Buffer (OS native memory) → I/O
↑
No copy! (faster)
Trade-offs:
import java.nio.ByteBuffer;
public class HeapVsDirectBuffer {
public static void main(String[] args) {
// Heap Buffer
long start1 = System.nanoTime();
ByteBuffer heapBuffer = ByteBuffer.allocate(1024 * 1024); // 1MB
long time1 = System.nanoTime() - start1;
System.out.println("Heap allocation: " + time1 + "ns");
// Output: ~1,000ns (FAST allocation!)
// Direct Buffer
long start2 = System.nanoTime();
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024);
long time2 = System.nanoTime() - start2;
System.out.println("Direct allocation: " + time2 + "ns");
// Output: ~500,000ns (SLOW allocation! 500x slower!)
// But I/O operations are faster with direct buffers!
}
}
When to use:
- Heap Buffer: Short-lived, temporary buffers, infrequent I/O
- Direct Buffer: Long-lived, frequent I/O operations (file channels, sockets)
import java.nio.ByteBuffer;
public class DirectBufferMemoryLeak {
public static void main(String[] args) {
// ❌ BAD: Allocate many direct buffers without cleanup
for (int i = 0; i < 10000; i++) {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB each
// NOT garbage collected normally!
// → OS native memory leak!
// Simulate work
if (i % 1000 == 0) {
System.out.println("Allocated " + i + " direct buffers");
}
}
// OutOfMemoryError: Direct buffer memory!
// ✅ GOOD: Reuse direct buffers
ByteBuffer reusableBuffer = ByteBuffer.allocateDirect(1024 * 1024);
for (int i = 0; i < 10000; i++) {
reusableBuffer.clear(); // Reuse same buffer!
// Use buffer...
}
// No memory leak!
}
}
Kết luận:
- Direct buffers are NOT managed by GC → can cause OS memory leaks!
- Always reuse direct buffers when possible
- Limit number of direct buffers (expensive to allocate/deallocate)
Buffer Properties: capacity, position, limit, mark
Mỗi buffer có 4 properties quan trọng:
0 <= mark <= position <= limit <= capacity
Buffer state:
┌───────────────────────────────────────────────┐
│ 0 capacity │
├───────────────────────────────────────────────┤
│ [ written data ][ available space ] │
│ 0 position limit │
└───────────────────────────────────────────────┘
- capacity: Kích thước tối đa của buffer (không đổi)
- position: Vị trí hiện tại để đọc/ghi (thay đổi)
- limit: Giới hạn - không được đọc/ghi vượt quá (thay đổi)
- mark: Bookmark - đánh dấu để quay lại sau (optional)
Ví dụ:
import java.nio.ByteBuffer;
public class BufferProperties {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("Initial state:");
printBufferState(buffer);
// capacity=10, position=0, limit=10
// Ghi 5 bytes
buffer.put((byte) 'H');
buffer.put((byte) 'e');
buffer.put((byte) 'l');
buffer.put((byte) 'l');
buffer.put((byte) 'o');
System.out.println("\nAfter writing 5 bytes:");
printBufferState(buffer);
// capacity=10, position=5, limit=10
}
static void printBufferState(ByteBuffer buffer) {
System.out.println("capacity=" + buffer.capacity() +
", position=" + buffer.position() +
", limit=" + buffer.limit() +
", remaining=" + buffer.remaining());
}
}
ByteBuffer Operations: put, get, flip, clear, compact
put() - Ghi dữ liệu vào buffer
import java.nio.ByteBuffer;
public class BufferPut {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(100);
// 1. put(byte b) - Ghi 1 byte
buffer.put((byte) 65); // 'A'
buffer.put((byte) 66); // 'B'
// 2. put(byte[] array) - Ghi cả array
byte[] data = {67, 68, 69}; // 'C', 'D', 'E'
buffer.put(data);
// 3. put(byte[] array, offset, length)
byte[] moreData = new byte[100];
buffer.put(moreData, 10, 20);
// 4. put(int index, byte b) - Ghi tại vị trí cụ thể (không di chuyển position)
buffer.put(0, (byte) 'X'); // Ghi đè byte đầu tiên
// 5. Ghi primitive types
buffer.putInt(42); // 4 bytes
buffer.putLong(1234567890L); // 8 bytes
buffer.putDouble(3.14159); // 8 bytes
buffer.putChar('€'); // 2 bytes
System.out.println("Position after puts: " + buffer.position());
}
}
get() - Đọc dữ liệu từ buffer
import java.nio.ByteBuffer;
public class BufferGet {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(100);
// Ghi một ít data
buffer.put((byte) 'H');
buffer.put((byte) 'e');
buffer.putInt(42);
buffer.putDouble(3.14);
// PHẢI flip() trước khi đọc!
buffer.flip();
// 1. get() - Đọc 1 byte
byte b1 = buffer.get(); // 'H'
byte b2 = buffer.get(); // 'e'
// 2. get(byte[] dst) - Đọc vào array
byte[] data = new byte[10];
buffer.get(data, 0, 4); // Đọc 4 bytes
// 3. get(int index) - Đọc tại vị trí cụ thể (không di chuyển position)
byte first = buffer.get(0);
// 4. Đọc primitive types
buffer.rewind(); // Quay về đầu
buffer.get(); // Skip 'H'
buffer.get(); // Skip 'e'
int num = buffer.getInt(); // 42
double pi = buffer.getDouble(); // 3.14
System.out.println("Read: " + num + ", " + pi);
}
}
flip() - Chuyển từ chế độ ghi sang chế độ đọc
flip() là method cực kỳ quan trọng trong NIO:
public Buffer flip() {
limit = position; // Giới hạn = vị trí hiện tại
position = 0; // Quay về đầu buffer
mark = -1; // Xóa mark
return this;
}
Ví dụ:
ByteBuffer buffer = ByteBuffer.allocate(10);
// Ghi 5 bytes
buffer.put((byte) 'H');
buffer.put((byte) 'e');
buffer.put((byte) 'l');
buffer.put((byte) 'l');
buffer.put((byte) 'o');
// State: position=5, limit=10
// flip() để chuyển sang chế độ đọc
buffer.flip();
// State: position=0, limit=5 (chỉ đọc được 5 bytes đã ghi)
// Đọc data
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
// Output: Hello
import java.nio.ByteBuffer;
public class FlipTrap {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
// Write 5 bytes
buffer.put((byte) 'H');
buffer.put((byte) 'e');
buffer.put((byte) 'l');
buffer.put((byte) 'l');
buffer.put((byte) 'o');
System.out.println("After write: position=" + buffer.position() +
", limit=" + buffer.limit());
// Output: position=5, limit=10
// ❌ FORGOT flip()! Try to read without flip()
System.out.print("Without flip(): ");
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println(" (nothing printed!)");
// Output: (nothing! because position=5, limit=10, hasRemaining()=false initially)
// ✅ CORRECT: Reset and flip before reading
buffer.rewind(); // Go back to start
buffer.flip(); // Set limit=5, position=0
System.out.print("With flip(): ");
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
// Output: Hello
}
}
Why flip() is necessary:
After write (before flip):
position=5, limit=10
[H][e][l][l][o][?][?][?][?][?]
↑ position here
hasRemaining() = true, but reads garbage!
After flip():
position=0, limit=5
[H][e][l][l][o][?][?][?][?][?]
↑ position ↑ limit
hasRemaining() = true, reads actual data!
clear() - Chuẩn bị để ghi lại từ đầu
clear() reset buffer về trạng thái ban đầu:
public Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
Lưu ý: clear() KHÔNG xóa dữ liệu, chỉ reset position/limit!
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put((byte) 'A');
buffer.put((byte) 'B');
buffer.put((byte) 'C');
// position=3
buffer.clear();
// position=0, limit=10 (ready to write again)
// Data cũ vẫn còn, nhưng sẽ bị ghi đè
compact() - Dọn dẹp phần đã đọc
compact() di chuyển phần chưa đọc về đầu buffer:
ByteBuffer buffer = ByteBuffer.allocate(10);
// Ghi 5 bytes
buffer.put("Hello".getBytes());
buffer.flip(); // position=0, limit=5
// Đọc 2 bytes
buffer.get(); // 'H'
buffer.get(); // 'e'
// position=2, limit=5 (còn 'l', 'l', 'o' chưa đọc)
// compact() - di chuyển 'llo' về đầu buffer
buffer.compact();
// position=3 (ready to write more)
// buffer content: ['l', 'l', 'o', ?, ?, ?, ?, ?, ?, ?]
// Ghi thêm
buffer.put("World".getBytes());
// buffer content: ['l', 'l', 'o', 'W', 'o', 'r', 'l', 'd', ?, ?]
rewind() - Quay lại đầu để đọc lại
buffer.rewind(); // position=0, limit không đổi
Tóm tắt các operations
| Operation | position | limit | Use case |
|---|---|---|---|
flip() | → 0 | → position | Chuyển ghi → đọc |
clear() | → 0 | → capacity | Chuẩn bị ghi lại từ đầu |
compact() | → remaining | → capacity | Giữ data chưa đọc, ghi thêm |
rewind() | → 0 | không đổi | Đọc lại từ đầu |
mark() | không đổi | không đổi | Đánh dấu vị trí |
reset() | → mark | không đổi | Quay về vị trí đã mark |
FileChannel - Đọc/ghi file qua channel
FileChannel là channel để đọc, ghi, map, và manipulate files.
Mở FileChannel
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class OpenFileChannel {
public static void main(String[] args) throws Exception {
// Cách 1: Từ RandomAccessFile
RandomAccessFile file = new RandomAccessFile("data.txt", "rw");
FileChannel channel1 = file.getChannel();
// Cách 2: Từ FileInputStream/FileOutputStream
// FileInputStream fis = new FileInputStream("data.txt");
// FileChannel channel2 = fis.getChannel();
// Cách 3: FileChannel.open() (Java 7+) - RECOMMENDED
Path path = Paths.get("data.txt");
FileChannel channel3 = FileChannel.open(path,
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
channel1.close();
channel3.close();
}
}
Đọc file với FileChannel
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class FileChannelRead {
public static void main(String[] args) throws Exception {
Path path = Paths.get("data.txt");
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
System.out.println("Bytes read: " + bytesRead);
// flip() để chuyển sang chế độ đọc
buffer.flip();
// Đọc data từ buffer
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear(); // Chuẩn bị cho lần đọc tiếp theo
}
}
}
Ghi file với FileChannel
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class FileChannelWrite {
public static void main(String[] args) throws Exception {
Path path = Paths.get("output.txt");
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
String data = "Hello, NIO!\nThis is FileChannel.";
ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
// Ghi toàn bộ buffer vào file
while (buffer.hasRemaining()) {
channel.write(buffer);
}
System.out.println("Data written successfully!");
}
}
}
Đọc file lớn với loop
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class ReadLargeFile {
public static void main(String[] args) throws Exception {
try (FileChannel channel = FileChannel.open(
Paths.get("large.txt"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024); // 1KB buffer
long totalBytes = 0;
while (channel.read(buffer) > 0) {
// flip() - chuyển sang chế độ đọc
buffer.flip();
// Xử lý data trong buffer
while (buffer.hasRemaining()) {
byte b = buffer.get();
totalBytes++;
// Process byte...
}
// clear() - chuẩn bị cho lần đọc tiếp theo
buffer.clear();
}
System.out.println("Total bytes read: " + totalBytes);
}
}
}
Channel-to-Channel Transfer: transferTo/transferFrom
FileChannel hỗ trợ zero-copy transfer giữa các channels - rất hiệu quả!
transferTo() - Copy từ channel này sang channel khác
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class TransferToExample {
public static void main(String[] args) throws Exception {
try (FileChannel source = FileChannel.open(
Paths.get("source.txt"), StandardOpenOption.READ);
FileChannel dest = FileChannel.open(
Paths.get("dest.txt"),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
long position = 0;
long size = source.size();
// transferTo() - zero-copy transfer!
source.transferTo(position, size, dest);
System.out.println("File copied: " + size + " bytes");
}
}
}
transferFrom() - Copy từ channel khác vào channel này
dest.transferFrom(source, 0, source.size());
Lợi ích của transferTo/transferFrom:
- Zero-copy: Dữ liệu không qua user space, chỉ trong kernel space
- Nhanh hơn nhiều so với đọc vào buffer rồi ghi ra
- Đơn giản, ít code hơn
Zero-copy transferTo Diagram
So sánh: Traditional copy vs transferTo
import java.io.*;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class CopyPerformance {
public static void main(String[] args) throws Exception {
String source = "large.dat"; // 100MB file
createTestFile(source, 100_000_000);
// Test 1: Traditional copy
long start1 = System.currentTimeMillis();
traditionalCopy(source, "copy1.dat");
long time1 = System.currentTimeMillis() - start1;
// Test 2: transferTo
long start2 = System.currentTimeMillis();
transferToCopy(source, "copy2.dat");
long time2 = System.currentTimeMillis() - start2;
System.out.println("Traditional copy: " + time1 + "ms");
System.out.println("transferTo copy: " + time2 + "ms");
System.out.println("Speedup: " + (time1 / (double) time2) + "x");
}
static void traditionalCopy(String src, String dest) throws IOException {
try (FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
}
static void transferToCopy(String src, String dest) throws IOException {
try (FileChannel source = FileChannel.open(
Paths.get(src), StandardOpenOption.READ);
FileChannel destination = FileChannel.open(
Paths.get(dest),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
source.transferTo(0, source.size(), destination);
}
}
static void createTestFile(String file, int size) throws IOException {
try (FileOutputStream fos = new FileOutputStream(file)) {
byte[] data = new byte[8192];
for (int i = 0; i < size / data.length; i++) {
fos.write(data);
}
}
}
}
Kết quả:
Traditional copy: 523ms
transferTo copy: 187ms
Speedup: 2.8x
Scatter/Gather I/O - Multiple Buffers
Scatter/Gather I/O cho phép đọc/ghi vào nhiều buffers trong một operation - hữu ích cho protocol parsing!
Scatter Read Diagram
Gather Write Diagram
Scatter Read - Đọc vào nhiều buffers
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
public class ScatterReadExample {
public static void main(String[] args) throws Exception {
// Giả sử file có cấu trúc:
// [Header: 10 bytes][Body: 100 bytes][Footer: 5 bytes]
try (FileChannel channel = FileChannel.open(
Paths.get("data.bin"), StandardOpenOption.READ)) {
// Prepare 3 buffers for header, body, footer
ByteBuffer headerBuffer = ByteBuffer.allocate(10);
ByteBuffer bodyBuffer = ByteBuffer.allocate(100);
ByteBuffer footerBuffer = ByteBuffer.allocate(5);
// Scatter read: read into multiple buffers in ONE operation!
ByteBuffer[] buffers = {headerBuffer, bodyBuffer, footerBuffer};
long bytesRead = channel.read(buffers);
System.out.println("Total bytes read: " + bytesRead);
// Process each buffer
headerBuffer.flip();
System.out.println("Header size: " + headerBuffer.limit());
bodyBuffer.flip();
System.out.println("Body size: " + bodyBuffer.limit());
footerBuffer.flip();
System.out.println("Footer size: " + footerBuffer.limit());
}
}
}
Gather Write - Ghi từ nhiều buffers
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
public class GatherWriteExample {
public static void main(String[] args) throws Exception {
try (FileChannel channel = FileChannel.open(
Paths.get("output.bin"),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
// Prepare multiple buffers
ByteBuffer header = ByteBuffer.wrap("HEADER".getBytes());
ByteBuffer body = ByteBuffer.wrap("This is the body content".getBytes());
ByteBuffer footer = ByteBuffer.wrap("END".getBytes());
// Gather write: write from multiple buffers in ONE operation!
ByteBuffer[] buffers = {header, body, footer};
long bytesWritten = channel.write(buffers);
System.out.println("Total bytes written: " + bytesWritten);
// Output: 33 (6 + 24 + 3)
}
}
}
Use case: HTTP Protocol Parsing
// HTTP Response format:
// [Status Line][Headers][Empty Line][Body]
ByteBuffer statusLine = ByteBuffer.allocate(100);
ByteBuffer headers = ByteBuffer.allocate(500);
ByteBuffer body = ByteBuffer.allocate(8192);
ByteBuffer[] buffers = {statusLine, headers, body};
socketChannel.read(buffers); // Scatter read!
// Automatically fills buffers in order
- Scatter = phân tán → Đọc 1 stream, phân tán vào nhiều buffers
- Gather = tập hợp → Tập hợp nhiều buffers, ghi thành 1 stream
Memory-mapped Files - MappedByteBuffer
Memory-mapped file map file trực tiếp vào memory → cực kỳ nhanh!
Cơ chế hoạt động
Traditional I/O:
File → Kernel buffer → User buffer → Application
Memory-mapped I/O:
File ←→ Memory (direct mapping) ←→ Application
(Không copy!)
Tạo Memory-mapped file
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class MemoryMappedFileExample {
public static void main(String[] args) throws Exception {
try (FileChannel channel = FileChannel.open(
Paths.get("large.dat"),
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE)) {
long fileSize = 10_000_000; // 10MB
// Map file vào memory
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE,
0, // position
fileSize // size
);
// Ghi data - tự động sync với file!
for (int i = 0; i < 1000; i++) {
buffer.putInt(i);
}
// Đọc data
buffer.position(0);
for (int i = 0; i < 10; i++) {
System.out.println("Value: " + buffer.getInt());
}
// force() - đẩy thay đổi ra disk ngay
buffer.force();
}
}
}
Use cases cho Memory-mapped files
✅ Đọc/ghi file CỰC LỚN (> 2GB) ✅ Random access (truy cập ngẫu nhiên nhiều lần) ✅ Shared memory giữa nhiều processes ✅ Database indexes
❌ Không nên dùng cho file nhỏ (overhead lớn)
Ví dụ: Đọc file cực lớn với memory-mapped
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class ReadHugeFile {
public static void main(String[] args) throws Exception {
String file = "huge.dat"; // File 5GB
try (FileChannel channel = FileChannel.open(
Paths.get(file), StandardOpenOption.READ)) {
long fileSize = channel.size();
System.out.println("File size: " + fileSize + " bytes");
// Map toàn bộ file
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY,
0,
fileSize
);
// Đọc random access - CỰC NHANH!
long sum = 0;
for (long i = 0; i < fileSize; i += 1024) {
buffer.position((int) i);
sum += buffer.get();
}
System.out.println("Checksum: " + sum);
}
}
}
Performance Comparison: I/O vs NIO
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class IOvsNIOPerformance {
public static void main(String[] args) throws Exception {
String file = "test.dat";
int fileSize = 50_000_000; // 50MB
createTestFile(file, fileSize);
// Test 1: FileInputStream (I/O)
long time1 = testFileInputStream(file);
// Test 2: BufferedInputStream (I/O)
long time2 = testBufferedInputStream(file);
// Test 3: FileChannel (NIO)
long time3 = testFileChannel(file);
// Test 4: Memory-mapped (NIO)
long time4 = testMemoryMapped(file);
System.out.println("\nResults:");
System.out.printf("FileInputStream: %5dms%n", time1);
System.out.printf("BufferedInputStream: %5dms%n", time2);
System.out.printf("FileChannel: %5dms%n", time3);
System.out.printf("Memory-mapped: %5dms%n", time4);
}
static long testFileInputStream(String file) throws IOException {
long start = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(file)) {
int b;
while ((b = fis.read()) != -1) {
// Process byte
}
}
return System.currentTimeMillis() - start;
}
static long testBufferedInputStream(String file) throws IOException {
long start = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(file))) {
int b;
while ((b = bis.read()) != -1) {
// Process byte
}
}
return System.currentTimeMillis() - start;
}
static long testFileChannel(String file) throws IOException {
long start = System.currentTimeMillis();
try (FileChannel channel = FileChannel.open(
Paths.get(file), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(8192);
while (channel.read(buffer) > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
buffer.get();
}
buffer.clear();
}
}
return System.currentTimeMillis() - start;
}
static long testMemoryMapped(String file) throws IOException {
long start = System.currentTimeMillis();
try (FileChannel channel = FileChannel.open(
Paths.get(file), StandardOpenOption.READ)) {
long size = channel.size();
var buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
while (buffer.hasRemaining()) {
buffer.get();
}
}
return System.currentTimeMillis() - start;
}
static void createTestFile(String file, int size) throws IOException {
try (FileOutputStream fos = new FileOutputStream(file)) {
byte[] data = new byte[8192];
for (int i = 0; i < size / data.length; i++) {
fos.write(data);
}
}
}
}
Kết quả thực tế (50MB file):
FileInputStream: 42350ms (42 giây - CỰC CHẬM!)
BufferedInputStream: 145ms
FileChannel: 132ms
Memory-mapped: 78ms (NHANH NHẤT!)
- KHÔNG BAO GIỜ dùng FileInputStream.read() không buffer!
- BufferedInputStream đủ tốt cho hầu hết use cases
- FileChannel tốt hơn một chút, code phức tạp hơn
- Memory-mapped nhanh nhất cho large files và random access
FileChannel Operations: position, truncate, force
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
public class FileChannelOperations {
public static void main(String[] args) throws Exception {
Path path = Paths.get("data.bin");
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.READ,
StandardOpenOption.WRITE)) {
// ===== position() - Get/set file pointer =====
System.out.println("Initial position: " + channel.position());
// Output: 0
// Write some data
ByteBuffer buffer = ByteBuffer.wrap("Hello World".getBytes());
channel.write(buffer);
System.out.println("After write: " + channel.position());
// Output: 11
// Set position manually (random access!)
channel.position(0); // Go back to start
System.out.println("After reposition: " + channel.position());
// Output: 0
// ===== size() - Get file size =====
System.out.println("File size: " + channel.size());
// Output: 11
// ===== truncate() - Cut file =====
channel.truncate(5); // Keep only first 5 bytes
System.out.println("After truncate: " + channel.size());
// Output: 5
// ===== force() - Flush to disk =====
buffer.clear();
buffer.put("New data".getBytes());
buffer.flip();
channel.write(buffer);
channel.force(true); // Force write to disk NOW!
// Parameter: true = also flush metadata (modified time, etc.)
System.out.println("Data flushed to disk");
}
}
}
Key operations:
position()/position(long)→ Get/set file pointer (random access!)size()→ Get file size in bytestruncate(long)→ Cut file to specified sizeforce(boolean)→ Flush buffer to disk (likeflush()for streams)
ByteBuffer State Transitions - Mermaid Diagram
Tổng kết
Channels
- FileChannel: Đọc/ghi files (read, write, map, transferTo/From)
- Two-way (có thể read + write)
- Kết hợp với Buffers để I/O
Buffers
- ByteBuffer: Buffer quan trọng nhất
- Properties: capacity, position, limit, mark
- Operations: put, get, flip, clear, compact, rewind
- flip() trước khi đọc! clear() trước khi ghi lại!
Memory-mapped Files
- Map file trực tiếp vào memory
- Cực kỳ nhanh cho large files (> 100MB)
- Tốt cho random access
Performance
- Luôn dùng buffering (BufferedInputStream hoặc ByteBuffer)
- transferTo/transferFrom cho file copy (zero-copy)
- Memory-mapped cho file cực lớn
Bài tập thực hành:
-
Implement file copy utility với 3 cách:
- BufferedInputStream/OutputStream
- FileChannel với ByteBuffer
- FileChannel.transferTo()
- So sánh performance
-
Viết chương trình đọc file nhị phân lớn (binary data):
- Parse các int, long, double từ file
- Dùng ByteBuffer.getInt(), getLong(), getDouble()
- Tính sum, average, min, max
-
Implement simple database index với Memory-mapped file:
- Lưu key-value pairs
- Support insert, search, delete
- Benchmark với HashMap in-memory