In this article you will learn: same/asynchronous + blocking/non-blocking performance differences; BIO, NIO, AIO; Understand and implement multiplexing when NIO operates Socket; At the same time, master the core operation skills of IO.

What is the difference between BIO, NIO and AIO?

What is the difference between same/asynchronous, blocked/non-blocking?

What is the most elegant way to implement file reading and writing?

How does NIO implement multiplexing?

With these questions, let’s enter the world of IO.

Before we begin, let’s consider a question: What is the full name of what we often call “IO”?

Many people may see this question and I have the same face meng, the full name of IO is actually: Input/Output abbreviation.

I. IO introduction

As opposed to NIO, BIO stands for BlockingIO, the block I/O module that Java started out with.

1.1 Differences between BIO, NIO and AIO

  1. BIO is the traditional java.io package, which is implemented based on the flow model and interacts in a synchronous, blocking manner, which means that threads will block while reading input or output streams until the read or write action is complete, and the calls between them are in a reliable linear order. It has the advantage that the code is relatively simple and intuitive; The disadvantage is that THE EFFICIENCY and scalability of I/O are low, which can easily lead to application performance bottlenecks.
  2. NIO is the java.nio package introduced in Java 1.4. It provides new abstractions such as channels, selectors, and buffers to build multiplexed, synchronous, non-blocking I/O programs while providing a way to manipulate data closer to the underlying performance of the operating system.
  3. Asynchronous I/O (AIO) is an Asynchronous and non-blocking operation based on events and callbacks. The Asynchronous I/O is an Asynchronous operation based on events and callbacks. It doesn’t clog up there, and when background processing is complete, the operating system notifies the appropriate thread for subsequent operations.

1.2 Understanding IO

Traditional IO can be roughly divided into four types:

  • InputStream, OutputStream Byte operation IO
  • Writer, Reader Character-based I/O operations
  • File I/O for disk-based operations
  • Socket Indicates the IO of network-based operations

The java.net Scoket is often referred to as synchronous blocking IO because network communication is also IO behavior.

There are many classes and interfaces under java. IO, but they are generally subsets of InputStream, OutputStream, Writer, and Reader. Mastering the use of these four classes and files is the key to use IO well.

1.3 the IO using

Next look at the InputStream, OutputStream, Writer, Reader inheritance diagram and usage examples.

1.3.1 InputStream using

Inheritance diagram and class methods, as shown below:

InputStream Example of using this command:

InputStream inputStream = new FileInputStream("D:\\log.txt");
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
String str = new String(bytes, "utf-8");
System.out.println(str);
inputStream.close();
Copy the code

1.3.2 OutputStream use

Inheritance diagram and class methods, as shown below:

OutputStream Usage examples:

OutputStream outputStream = new FileOutputStream("D:\\log.txt".true); // Whether to append. True = append
outputStream.write("Hello, Lao Wang.".getBytes("utf-8"));
outputStream.close();
Copy the code

1.3.3 Writer use

Writer inherits the diagram and class methods as shown below:

Writer Example:

Writer writer = new FileWriter("D:\\log.txt".true); // Whether to append files, true= append
writer.append("Hello, Lao Wang.");
writer.close();
Copy the code

1.3.4 Reader using

The Reader inheritance diagram and class methods look like this:

Reader examples:

Reader reader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(reader);
StringBuffer bf = new StringBuffer();
String str;
while((str = bufferedReader.readLine()) ! =null) {
    bf.append(str + "\n");
}
bufferedReader.close();
reader.close();
System.out.println(bf.toString());
Copy the code

Two, synchronous, asynchronous, blocking, non-blocking

We’ve talked a lot about synchronous, asynchronous, blocking, and non-blocking, but let’s talk more about what they mean and what they mean when combined.

2.1 Synchronous and Asynchronous

Synchronization is a reliable task sequence in which the completion of one task depends on another task. The dependent task can only be completed after the dependent task is completed. Either success succeeds, failure fails, and the states of the two tasks remain the same. Asynchronism, however, does not need to wait for the dependent task to complete, but only notifies the dependent task of what work to complete, and the dependent task is executed immediately, as long as it completes the whole task. As to whether the dependent task is actually completed in the end, the dependent task cannot be determined, so it is an unreliable task sequence. We can use phone calls and text messages as a good metaphor for synchronous and asynchronous operations.

2.2 Blocking and non-blocking

Blocking and non-blocking are defined primarily in terms of CPU consumption. Blocking is when the CPU stops and waits for a slow operation to complete before the CPU can do something else. Non-blocking means that the CPU does something else while the slow operation is executed, and then the CPU completes the next operation when the slow operation is complete. While the non-blocking approach apparently improves CPU utilization, it also has the added consequence of increased thread switching on the system. Whether the increased CPU usage time will compensate for the switching cost of the system needs to be evaluated.

2.3 Same/different, blocking/non-blocking combination

There are four types of same/different, blocking/non-blocking combinations, as shown in the table below:

combination Performance analysis
A synchronized block The most common usage is the simplest to use, but I/O performance is generally poor and the CPU is mostly idle.
Synchronous nonblocking A common way to improve I/O performance is to change the BLOCKING mode of I/O to a non-blocking mode. This is particularly effective when network I/O connections are long and data transmission is not large. This approach generally improves I/O performance, but increases CPU consumption. Consider whether the increased I/O performance compensates for CPU consumption, that is, whether the system bottleneck is in I/O or CPU.
Asynchronous blocking This method is often used in distributed databases. For example, when a record is written in a distributed database, there will usually be one synchronous blocking record, and two or three backup records will be written to other machines. These backup records usually use asynchronous blocking to write I/O. Asynchronous blocking can improve network I/O efficiency, especially when multiple copies of the same data are written simultaneously, as described above.
Asynchronous nonblocking This combination is complex and can only be used in very complex distributed situations, such as message synchronization between clusters. For example, Cassandra’s Gossip communication mechanism is asynchronous and non-blocking. It is suitable for transferring multiple copies of the same data to different machines in the cluster at the same time, and the data transfer volume is small but very frequent. The network I/O performance is maximized in this manner.

Before Java 7, files read like this:

// Add a file
FileWriter fileWriter = new FileWriter(filePath, true);
fileWriter.write(Content);
fileWriter.close();

// Read the file
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
StringBuffer bf = new StringBuffer();
String str;
while((str = bufferedReader.readLine()) ! =null) {
    bf.append(str + "\n");
}
bufferedReader.close();
fileReader.close();
System.out.println(bf.toString());
Copy the code

Java 7 introduced Files (java.nio package), which greatly simplifies file reading and writing, as follows:

/ / write file (additional ways: StandardOpenOption. APPEND)
Files.write(Paths.get(filePath), Content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);

// Read the file
byte[] data = Files.readAllBytes(Paths.get(filePath));
System.out.println(new String(data, StandardCharsets.UTF_8));
Copy the code

It’s one line of code to read and write files, and yes that’s the most elegant file manipulation.

There are also many useful methods under Files, such as creating a multi-layer folder, which is also easy to write:

// Create multiple (single) directories (if not created, no error will be reported)
new File("D://a//b").mkdirs();
Copy the code

Fourth, Socket and NIO multiplexing

This section takes you through the implementation of NIO multiplexing as well as AIO Socket implementation.

4.1 Traditional Socket implementation

Next we will implement a simple Socket, the server only sends information to the client, and then the client prints out the example, the code is as follows:

int port = 4343; / / the port number
// Socket server (simply sending messages)
Thread sThread = new Thread(new Runnable() {
    @Override
    public void run(a) {
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            while (true) {
                // Wait for connection
                Socket socket = serverSocket.accept();
                Thread sHandlerThread = new Thread(new Runnable() {
                    @Override
                    public void run(a) {
                        try (PrintWriter printWriter = new PrintWriter(socket.getOutputStream())) {
                            printWriter.println("hello world!");
                            printWriter.flush();
                        } catch(IOException e) { e.printStackTrace(); }}}); sHandlerThread.start(); }}catch(IOException e) { e.printStackTrace(); }}}); sThread.start();// Socket client (receiving messages and printing)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
    bufferedReader.lines().forEach(s -> System.out.println("Client:" + s));
} catch (UnknownHostException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
Copy the code
  • Call accept and block waiting for the client to connect.
  • Using Socket to simulate a simple client, only to connect, read and print;

In Java, the realization of the thread is a heavyweight, or destroy it is the beginning of the thread consumes server resources, even using the thread pool, use the traditional way of Socket, when the number of connections extremely rise may cause performance bottlenecks, the reason was that the thread online article switching costs will be reflected in the high concurrency clearly, And the above operation mode is synchronous blocking programming, performance problems will be particularly obvious when high concurrency.

The above process is as follows:

4.2 NIO multiplexing

NIO’s multiplexing capabilities make a lot of sense given these high concurrency problems.

NIO uses the single-thread polling event mechanism to efficiently locate ready channels to decide what to do. Only the SELECT phase is blocked, which can effectively avoid the problems caused by frequent thread switching when a large number of clients are connected, and the scalability of the application has been greatly improved.

// NIO multiplexing
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4.4.60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
threadPool.execute(new Runnable() {
    @Override
    public void run(a) {
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {
            serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select(); // block a Channel that is ready to wait
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    try (SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) {
                        channel.write(Charset.defaultCharset().encode("Hello world.")); } iterator.remove(); }}}catch(IOException e) { e.printStackTrace(); }}});// Socket client (receiving messages and printing)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
    bufferedReader.lines().forEach(s -> System.out.println("NIO client:" + s));
} catch (IOException e) {
    e.printStackTrace();
}
Copy the code
  • First, create a Selector with Selector.open() as a dispatcher-like role;
  • Then, a ServerSocketChannel is created and registered with the Selector, telling the dispatcher that it cares about new connection requests by specifying selectionkey. OP_ACCEPT.
  • Why do we explicitly configure non-blocking modes? This is because the blocking mode, register operation is not allowed, throws IllegalBlockingModeException exception;
  • Selector blocks in the select operation, which is woken up when an access request is made to a Channel;

The following diagram can effectively illustrate the NIO reuse process:

Thus, NIO’s multiplexing greatly improves the server-side’s ability to respond to high concurrency.

4.3 AIO version Socket implementation

Java 1.7 provides an AIO implementation of sockets like this:

// AIO thread reuse version
Thread sThread = new Thread(new Runnable() {
    @Override
    public void run(a) {
        AsynchronousChannelGroup group = null;
        try {
            group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4));
            AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
            server.accept(null.new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {
                @Override
                public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
                    server.accept(null.this); // Receive the next request
                    try {
                        Future<Integer> f = result.write(Charset.defaultCharset().encode("Hello world."));
                        f.get();
                        System.out.println("Server sending time:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                        result.close();
                    } catch(InterruptedException | ExecutionException | IOException e) { e.printStackTrace(); }}@Override
                public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {}}); group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); }catch(IOException | InterruptedException e) { e.printStackTrace(); }}}); sThread.start();// Socket client
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future<Void> future = client.connect(new InetSocketAddress(InetAddress.getLocalHost(), port));
future.get();
ByteBuffer buffer = ByteBuffer.allocate(100);
client.read(buffer, null.new CompletionHandler<Integer, Void>() {
    @Override
    public void completed(Integer result, Void attachment) {
        System.out.println("Client print:" + new String(buffer.array()));
    }

    @Override
    public void failed(Throwable exc, Void attachment) {
        exc.printStackTrace();
        try {
            client.close();
        } catch(IOException e) { e.printStackTrace(); }}}); Thread.sleep(10 * 1000);
Copy the code

Five, the summary

The above basic is IO from 1.0 to the current version (version of this article) JDK 8 core use operation, you can see that IO as a more common basic function, the development of the change is also very large, but also more and more simple to use, IO operation is relatively easy to understand, an input an output, A good command of input and output will also master IO, Socket as a network interaction integration function, obviously NIO multiplexing, to Socket brought more vitality and choice, users can choose the corresponding code strategy according to their own actual scenarios.

Of course, at the end of this article, I also gave you a sample code for this article: github.com/vipstone/ja…

Vi. Reference documents

t.cn/EwUJvWA

www.ibm.com/developerwo…


Follow the author’s wechat official account: