1. Introduction to asynchronous IO
Let’s clarify terms right away. In classic (synchronous) IO, when you call a read or write method, your execution thread (for example, the program’s main thread) stops and waits until the operation completes. It’s like calling a friend and, until they pick up, you just stand there staring at the phone.
Asynchronous IO (AIO) is when you delegate the read/write operation to the system and keep working. When the operation finishes, you’ll get a call back (for example, your callback method is invoked or a result is returned via Future).
Where is this useful?
- Server applications: to avoid wasting threads while the disk is “thinking”.
- Bulk processing of large files: to avoid blocking the main thread.
- UI applications: so the interface doesn’t “freeze” during reading/writing.
Imagine you ordered a pizza. In a synchronous world you’d stand by the door waiting for the delivery. In an asynchronous one you go about your business, and when the pizza arrives, they call to say: “The pizza is here!”
2. Overview of AsynchronousFileChannel
In Java, asynchronous IO is implemented in the java.nio.channels package since Java 7. The main character is the class AsynchronousFileChannel.
What can it do?
- Read and write file data asynchronously.
- Work with buffers (ByteBuffer).
- Support different ways to obtain results: via Future or via CompletionHandler.
- Let you explicitly specify a thread pool (ExecutorService) to handle events.
Core methods
- read(ByteBuffer dst, long position): returns Future<Integer>.
- read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
- write(ByteBuffer src, long position): returns Future<Integer>.
- write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
- static open(Path file, Set<OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) — opens the channel.
Usage options:
- Via Future: you start the operation and can wait for it to complete later.
- Via CompletionHandler: you pass a handler that will be invoked when the operation completes (or fails with an error).
Example: opening a file for asynchronous read/write
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"),
EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)
);
You can also explicitly specify a thread pool to handle events:
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
ExecutorService executor = Executors.newFixedThreadPool(4);
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"),
EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE),
executor
);
Interesting fact:
If you don’t specify an ExecutorService, Java will create its own internal thread pool to service IO events. For simple tasks that’s enough, but for server applications it’s better to manage the pool yourself.
3. Executor threads (ExecutorService) and their role
When you work with an asynchronous channel, behind the scenes Java must run your callbacks or complete the Future. It does this not by magic but using special worker threads—an executor service.
If you don’t provide your own thread pool, Java simply creates an internal one—typically one thread per processor. Convenient, but not always safe. When you want to control how many threads run, which tasks are more important, and how load is distributed, it’s better to create your own ExecutorService and pass it to open.
This is especially important in server applications. Without your own thread pool, you can easily get unexpected load spikes—and instead of smooth operation the server starts to choke.
Example:
ExecutorService pool = Executors.newFixedThreadPool(8);
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("huge.log"),
EnumSet.of(StandardOpenOption.READ),
pool
);
Impact of pool choice:
- Many threads—more parallelism but higher system load.
- Few threads—fewer concurrent operations but less overhead.
- If you launch thousands of asynchronous operations, think about the balance!
4. Practice: asynchronous file reading
Synchronous reading (for comparison)
import java.nio.file.Files;
import java.nio.file.Path;
byte[] data = Files.readAllBytes(Path.of("input.txt"));
System.out.println("Bytes read: " + data.length);
The problem here is that the thread simply waits until the entire file is read. If the file is large or the disk is slow, the program slows down as well—everything else pauses during that time.
Asynchronous reading with AsynchronousFileChannel and Future
import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
public class AsyncReadExample {
public static void main(String[] args) throws Exception {
Path path = Path.of("input.txt");
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024); // read 1 KB at a time
Future<Integer> result = channel.read(buffer, 0);
// You can do something else in parallel!
System.out.println("Reading started...");
// ... and then wait for the result
int bytesRead = result.get(); // blocks the thread until the operation completes
System.out.println("Bytes read: " + bytesRead);
buffer.flip();
// Convert bytes to a string (if it's text)
byte[] data = new byte[bytesRead];
buffer.get(data, 0, bytesRead);
String text = new String(data);
System.out.println("Content: " + text);
}
}
}
- channel.read(buffer, 0) — starts an asynchronous read from position 0.
- Returns a Future<Integer> that you can use to wait for the result.
- While the operation is not finished, you can perform other actions.
- result.get() blocks the thread, but only if the result is not ready yet.
Asynchronous reading with CompletionHandler
(We’ll cover this in more detail in the next lecture, but as a teaser...)
import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.channels.CompletionHandler;
public class AsyncReadWithHandler {
public static void main(String[] args) throws Exception {
Path path = Path.of("input.txt");
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer buf) {
buf.flip();
byte[] data = new byte[bytesRead];
buf.get(data, 0, bytesRead);
String text = new String(data);
System.out.println("Asynchronously read: " + text);
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
System.err.println("Read error: " + exc.getMessage());
}
});
// Don't let the program exit immediately (otherwise the callback won't have time)
Thread.sleep(100); // In real applications—prefer synchronization via a latch, future, etc.
}
}
}
5. Useful details
Comparison: asynchronous vs synchronous reading
| Characteristic | Synchronous IO ( Files.readAllBytes ) | Asynchronous IO ( AsynchronousFileChannel ) |
|---|---|---|
| Blocks the thread | Yes | No (if you don’t call get()) |
| Scalability | Low | High |
| Suitable for UI/servers | No | Yes |
| Code complexity | Simple | Slightly more complex |
| Resource management | Simple | Important: don’t forget to close the channel! |
Asynchronous IO workflow
sequenceDiagram
participant Main as Your thread
participant OS as Operating system
participant Disk as Disk
Main->>OS: Starts asynchronous read (read)
OS->>Disk: Reads data
Main->>Main: Performs other tasks
OS-->>Main: Reports completion (Future/CompletionHandler)
Main->>Main: Processes the result
6. Common mistakes when working with AsynchronousFileChannel
Error #1: forgot to close the channel.
AsynchronousFileChannel is a resource that must be closed. If you forget to close the channel (channel.close() or try-with-resources), you can get file descriptor leaks and issues accessing files. Use try-with-resources whenever possible.
Error #2: blocking get() on the main thread.
If you use a Future and call get() on the main thread (for example, in a UI application), you defeat the purpose of asynchronous IO—the thread will still wait. Use a CompletionHandler or a separate thread to wait for the result.
Error #3: incorrect ByteBuffer usage.
After writing to the buffer, don’t forget to call flip() to prepare it for reading. After reading—clear() or compact() if you will reuse it.
Error #4: forgot to handle errors.
Asynchronous operations can fail (for example, file not found, no access). If you don’t handle exceptions in a CompletionHandler or don’t check the Future for errors, the program will “silently” not perform the operation.
Error #5: unaccounted-for concurrency.
If you start several operations on the same channel at the same time, make sure your code is thread-safe and there are no races for buffers or file positions.
GO TO FULL VERSION