File Dialogs  «Prev  Next»


Lesson 9

Modern Java I/O: Non-Blocking Channels and Serialization Security

In earlier versions of Java (1.1 through Java 7), file input and output operations were typically performed using the java.io package. This model was synchronous and blocking — once a thread requested data from a file, socket, or stream, it had to wait until the operation completed before continuing. Modern Java uses NIO (New Input/Output) — a set of APIs in the java.nio package that support non-blocking and asynchronous I/O. NIO allows threads to request reads or writes, then continue performing other work while the operation completes in the background.

From Blocking I/O to Non-Blocking NIO

Traditional I/O relies on streams such as FileInputStream or BufferedReader, which block execution until data becomes available. NIO introduces channels and buffers to improve scalability and performance, especially in concurrent or high-throughput environments. A channel represents a connection to an I/O source (such as a file or socket), and buffers act as containers for the data being read or written.

The key feature of NIO is that channels can be placed into non-blocking mode. When a channel is non-blocking, read or write operations return immediately, even if no data is currently available.

Example: Non-Blocking Read with FileChannel and Selector

The example below demonstrates how a worker thread can perform non-blocking file reading using FileChannel and Selector. The thread submits the read request and continues performing other work without waiting for I/O completion.




import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Iterator;

public class NonBlockingFileReader implements Runnable {
    private final Path filePath;

    public NonBlockingFileReader(Path filePath) {
        this.filePath = filePath;
    }

    @Override
    public void run() {
        try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ)) {
            channel.configureBlocking(false);
            Selector selector = Selector.open();
            channel.register(selector, SelectionKey.OP_READ);

            ByteBuffer buffer = ByteBuffer.allocate(1024);

            while (true) {
                int readyChannels = selector.select(1000); // timeout 1 second
                if (readyChannels == 0) {
                    System.out.println(Thread.currentThread().getName() + " doing other work...");
                    continue; // no channels ready yet
                }

                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();

                    if (key.isReadable()) {
                        FileChannel ch = (FileChannel) key.channel();
                        int bytesRead = ch.read(buffer);
                        if (bytesRead == -1) {
                            System.out.println("End of file reached.");
                            return;
                        }
                        buffer.flip();
                        System.out.println("Read: " + new String(buffer.array(), 0, bytesRead));
                        buffer.clear();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Thread readerThread = new Thread(new NonBlockingFileReader(Path.of("example.txt")));
        readerThread.start();
        System.out.println("Main thread continues doing other work...");
    }
}

In this example:

Serialization Security Considerations

Serialization allows Java objects to be converted into a byte stream and later reconstructed. However, this process must be handled carefully to avoid security issues. In modern Java, serialization vulnerabilities are mitigated by validation hooks, ObjectInputFilter, and the transition toward record-based data transfer or JSON serialization (e.g., with Jackson or Gson).

A major concern is that malicious serialized data can expose private fields or create unauthorized object graphs. To mitigate this, use ObjectInputFilter to restrict deserialization:


import java.io.*;
import java.util.*;

public class SecureDeserialization {
    public static void main(String[] args) throws Exception {
        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("com.javadeploy.model.*;!*");
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"))) {
            ois.setObjectInputFilter(filter);
            Object obj = ois.readObject();
            System.out.println("Deserialized object: " + obj);
        }
    }
}

This code restricts deserialization to safe packages and prevents arbitrary class instantiation from untrusted sources.

Modern Alternatives to Java Serialization

Conclusion

The evolution from blocking I/O to java.nio reflects Java’s broader shift toward scalability and efficiency. Modern applications should use non-blocking I/O where concurrency or large data throughput is required, and adopt safe serialization techniques to prevent security vulnerabilities. Understanding these technologies is fundamental to writing performant, secure, and maintainable enterprise applications.


SEMrush Software