Chuyển tới nội dung chính

NIO: Channels và Buffers

Mục tiêu bài học

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:

  1. Channels - Kênh I/O hai chiều (read + write)
  2. Buffers - Container cho dữ liệu trong memory
  3. 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.iojava.nio
Stream-oriented (dòng)Buffer-oriented (bộ đệm)
Blocking (đồng bộ)Non-blocking (bất đồng bộ)
Đọc tuần tự, 1 chiềuCó thể di chuyển qua lại trong buffer
Không có SelectorsCó Selectors (I/O multiplexing)
Khi nào dùng NIO?
  • 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 BufferDirect Buffer
Trong Java heapNgoài heap (native memory)
GC quản lýKhông do GC quản lý
Allocate/deallocate nhanhAllocate/deallocate chậm
I/O chậm hơnI/O nhanh hơn (zero-copy)
Dùng cho: short-lived buffersDùng cho: long-lived, frequent I/O
📖 Direct vs Heap ByteBuffer - Performance Trade-offs

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)
🔥 Bẫy OCP: Direct buffer memory leak!
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
🔥 Bẫy OCP: Forgetting flip() before reading!
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

OperationpositionlimitUse case
flip()→ 0→ positionChuyển ghi → đọc
clear()→ 0→ capacityChuẩn bị ghi lại từ đầu
compact()→ remaining→ capacityGiữ data chưa đọc, ghi thêm
rewind()→ 0không đổiĐọc lại từ đầu
mark()không đổikhông đổiĐánh dấu vị trí
reset()→ markkhông đổiQuay 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
💡 Cách nhớ Scatter/Gather
  • 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!)
Kết luận Performance
  1. KHÔNG BAO GIỜ dùng FileInputStream.read() không buffer!
  2. BufferedInputStream đủ tốt cho hầu hết use cases
  3. FileChannel tốt hơn một chút, code phức tạp hơn
  4. 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 bytes
  • truncate(long) → Cut file to specified size
  • force(boolean) → Flush buffer to disk (like flush() 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:

  1. Implement file copy utility với 3 cách:

    • BufferedInputStream/OutputStream
    • FileChannel với ByteBuffer
    • FileChannel.transferTo()
    • So sánh performance
  2. 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
  3. Implement simple database index với Memory-mapped file:

    • Lưu key-value pairs
    • Support insert, search, delete
    • Benchmark với HashMap in-memory

Đọc thêm