Article From:https://www.cnblogs.com/lfs2640666960/p/9970353.html

Synchronization, Asynchronization, Blocking, Non-Blocking

First of all, these concepts are very easy to confuse, but they are also involved in NIO, so summarize [1].

  • Synchronization: When the API call returns, the caller knows what the result of the operation is (how many bytes are actually read / written).
  • Asynchronism: When an API call returns, the caller does not know the result of the operation, and then calls back the notification result.
  • Blocking: When no data is readable or cannot be written to all data, suspend the current thread waiting.
  • Non-blocking: Read as much data as you can read and then return. Write as much data as you can write and then return.

For I/O operations, according to the documents of Oracle’s official website, the criterion of synchronous and asynchronous partition is “whether the caller needs to wait for I/O operations to complete”. The meaning of “waiting for I/O operations to complete” is not to read or write all data, but to really perform I/O operations.Whether the caller has to wait, for example, during the time when data is transmitted between the TCP/IP stack buffer and the JVM buffer.

Therefore, our commonly used read () and write () methods are synchronous I/O, synchronous I/O is divided into two modes: blocking and non-blocking. If the non-blocking mode detects that no data is readable, it returns directly and does not really perform I/O operations.

In conclusion, there are only three mechanisms in Java: synchronous blocking I/O, synchronous non-blocking I/O and asynchronous I/O. The first two are mentioned below. JDK 1.7 began to introduce asynchronous I/O, which is called NIO.2.

Traditional IO

We know that the emergence of a new technology is always accompanied by improvements and improvements, as is the emergence of Java NIO.

Traditional I/O is blocking I/O, the main problem is the waste of system resources. For example, in order to read data from a TCP connection, we call the read () method of InputStream, which causes the current thread to be suspended until data arrives.In order to read the data of other connections, we have to start another thread. This may not be a problem when the number of concurrent connections is small, but when the number of concurrent connections is smallWhen the amount reaches a certain scale, the memory resources will be exhausted by a large number of threads. On the other hand, thread switching needs to change the state of the processor, such as the value of program counters and registers, so it is also a waste of resources to switch between a large number of threads very frequently.

With the development of technology, modern operating system provides a new I/O mechanism, which can avoid the waste of resources. Based on this, Java NIO was born. The representative feature of NIO is non-blocking I/O. Then we found that simple use of non-blocking I/O did not solve the problem, becauseIn non-blocking mode, the read () method returns immediately when the data is not read. We don’t know when the data arrives. We can only call the read () method to retry continuously. This is obviously a waste of CPU resources. As you can see from the following, the Selector component is just to solve this problem.This problem arises.

Java NIO Core components

1.Channel

concept

Java NIOAll I/O operations are based on Channel objects, just like stream operations are based on Stream objects, so it is necessary to know what Channel is first. The following is excerpted from the JDK 1.8 document

A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.

From the above, a Channel represents a connection with an entity, which can be a file, a network socket, etc. That is to say, the channel is a bridge provided by Java NIO to interact with the underlying I/O services of our program and operating system.

Channel is a very basic and abstract description. It interacts with different I/O services, performs different I/O operations and achieves different implementations. So there are File Channel, Socket Channel and so on.

Channels are similar to Stream in that they can read data into a Buffer or write data from a Buffer to a channel.

 
1.jpg

Of course, there are also differences, mainly reflected in the following two points:

  • A channel can read and write, while a Stream is one-way (so there are InputStream and OutputStream)
  • Channel has non-blocking I/O mode

Realization

Java NIOThe most commonly used channel implementations are as follows. It can be seen that they correspond to the traditional I/O operation classes one by one.

  • FileChannel:Read and write files
  • DatagramChannel: UDPProtocol Network Communication
  • SocketChannel:TCPProtocol Network Communication
  • ServerSocketChannel:Listening for TCP Connections

2.Buffer

NIOThe buffer used in this program is not a simple byte array, but a packaged Buffer class. Through the API it provides, we can manipulate data flexibly. Here are some details.

Corresponding to Java basic types, NIO provides a variety of Buffer types, such as ByteBuffer, CharBuffer, IntBuffer, etc. The difference is that the unit length of the buffer is different (read and write in units of variables of the corresponding type).)

BufferThere are three important variables, which are the key to understanding Buffer’s working mechanism.

  • capacity (Total capacity)
  • position (Pointer current position)
  • limit (Read/Write Boundary Location

BufferThe way we work is very similar to the character array in C. By analogy, capacity is the total length of the array. Position is the subscript variable for our reading/writing characters, and limit is the position of the terminator. The three variables at Buffer’s inception are shown below.

 
2.jpg

In the process of reading/writing Buffer, position moves backwards, and limit is the boundary of position movement. It’s not hard to imagine that limit should be set to capacity when writing to BufferSize, and when reading Buffer, limit should be set to the actual end of the data. (Note: Buffer dataWrite in The channel is Buffer.read Operation, from channelread Data to Buffer is BufferWrite in Operation)

Before performing read/write operations on Buffer, we can call some assistant methods provided by Buffer class to set position and limit correctly, mainly as follows

  • flip(): Set limit to position, and then position to 0. Call before reading Buffer.
  • rewind(): Just set position to zero. It is usually invoked before re-reading Buffer data, such as when reading the same Buffer data to write to multiple channels.
  • clear(): Back to the initial state, where limit equals capacity, position is set to zero. Call Buffer before rewriting.
  • compact(): Move the unread data (between position and limit) to the beginning of the buffer and set position to the next position at the end of the data. In fact, it is equivalent to rewriting such a piece of data to the buffer.

Then, look at an example of using FileChannel to read and write text files, which verifies the channel readability and writability characteristics and the basic use of Buffer (note that FileChannel cannot be set to non-blocking mode).

FileChannel channel = new RandomAccessFile("test.txt", "rw").getChannel();
    channel.position(channel.size());  // Move the file pointer to the end (add write)ByteBuffer by teBuffer = ByteBuffer. allocate (20);

    // Data Writing BufferByteBuffer. put ("Hello, World! \ n ". getBytes (Standard Charsets. UTF_8);// Buffer -> Channel
    byteBuffer.flip();
    while (byteBuffer.hasRemaining()) {
        channel.write(byteBuffer);
    }

    channel.position(0); // Move the file pointer to the beginning (read from scratch)CharBuffer charBuffer = CharBuffer. allocate (10);
    CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();

    // Read out all the dataByteBuffer. clear ();while (channel.read(byteBuffer) != -1 || byteBuffer.position() > 0) {
        byteBuffer.flip();

        // Decoding with UTF-8 DecoderCharBuffer. clear ();Decoder. decode (byteBuffer, charBuffer,false);
        System.out.print(charBuffer.flip().toString());

        byteBuffer.compact(); // Data may be surplus}Channel. close (); copy code

In this example, two Buffers are used, in which byteBuffer serves as a data buffer for channel reads and writes, and charBuffer stores decoded characters. The use of clear () and flip () is, as mentioned above, the most important thing to note.In the latter compact () method, even if the size of charBuffer is sufficient to accommodate byteBuffer decoded data, the compact () is also necessary, because UTF-8 encoding of commonly used Chinese characters takes up three words.Section, so there is a very high probability of truncation in the middle of the situation, see the following figure:

 
3.jpg

When Decoder reads 0xe4 at the end of the buffer, it cannot be mapped to a Unicode. The third parameter of the decode () method, false, serves to make Decoder think of the bytes that cannot be mapped and the data that follows them.Additional data, so the decode () method stops here, and position falls back to 0xe4. In this way, the first byte encoded by the word “in” is left in the buffer and must be compact to the front to be correct.Stitching with sequential data.

BTW,CharsetDecoder in the example is also a new feature of Java NIO, so you should find a point that NIO operates in a buffer-oriented manner (traditional I/O is stream-oriented).

So far, we have learned the basic usage of Channel and Buffer. Next, let’s say that one thread manages important components of multiple Channels.

3.Selector

Selector What is it?

Selector(A selector is a special component that collects the state (or event) of each channel. We first register the channel to the selector and set up the events we care about, then we can wait for the events to happen quietly by calling the select () method.

There are four events in the channel that we can monitor:

  • Accept:Acceptable connections
  • Connect:Successful connection
  • Read:Data Readable
  • Write:It’s ready to write data.

Why use Selector

As mentioned earlier, if I/O is blocked, it needs multithreading (wasting memory), and if I/O is non-blocked, it needs to be retried (consuming CPU). The emergence of Selector solves this awkward problem. In non-blocking mode, through Selector, our threads are only ready.Channel works without blindly retrying. For example, when no data arrives in all channels, no Read event occurs, and our threads are suspended at the select () method, thus freeing up CPU resources.

Usage method

As shown below, create a Selector and register a Channel.

Note: To register Channel to Selector, you first need to set Channel to non-blocking mode, otherwise an exception will be thrown.

Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);Copy code

register()The second parameter of the method is called “interest set”, which is the set of events you care about. If you care about multiple events, separate them with a bitwise or operator, such as

SelectionKey.OP_READ | SelectionKey.OP_WRITECopy code

This writing is not unfamiliar at all. It is so popular in programming languages that support bit operations. It can identify many states with an integer variable. How can it do this? It’s very simple. For example, first, we predefine some constants whose values (binary) are as follows.

 
4.jpg

It can be found that the bits whose values are 1 are staggered, so there is no ambiguity in the values obtained by bitwise OR operation on them, and it can be deduced from which variables are calculated. How to judge, yes, is the “bitwise and” operation. For example, there is now a state set variable with a value of 0011.We only need to determine whether the value of “0011 & amp; OP_READ” is 1 or 0 to determine whether the collection contains the OP_READ state.

Then, notice that the register () method returns an object of SelectionKey, which contains the registration information, and we can also modify the registration information through it. As you can see from the complete example below, after select (), we also knowGet the channels that are ready by getting a collection of SelectionKey.

A complete example

Now that the concepts and theories have been elaborated (actually, I find it embarrassing that I haven’t written much here), let’s take a look at a complete example.

This example uses Java NIO to implement a single-threaded server. Its function is simple. It listens to the client connection. When the connection is established, it reads the client’s message and responds to a message to the client.

Note that I use the character’\ 0′(a byte with a value of 0) to identify the end of the message.

Single Thread Server

public class NioServer {

    public static void main(String[] args) throws IOException {
        // Create a selectorSelector selector = Selector. open ();// Initialize TCP Connection Monitoring ChannelServerSocketChannel listenChannel = ServerSocketChannel. open ();ListenCHannel.bind (new InetSocketAddress(9999));
        listenChannel.configureBlocking(false);
        // Register to selector (listening for its ACCEPT events)Listen Channel.register(selector, SelectionKey.OP_ACCEPT);

        // Create a bufferByteBuffer buffer = ByteBuffer. allocate (100);

        while (true) {
            selector.select(); //Blocking until there is a listening eventIterator & lt; SelectionKey & gt; keyIter = selector. selectedKeys (). iterator ();

            // Access Channel events from select by iterator in turnwhile (keyIter.hasNext()) {
                SelectionKey key = keyIter.next();

                if (key.isAcceptable()) { // Connected is acceptableSocket Channel channel = ((Server Socket Channel) key. channel (). accept ();Channel. configureBlockingfalse);
                    channel.register(selector, SelectionKey.OP_READ);

                    System.out.println("And ["+channel.getRemoteAddress ()+"】Connection established! "";}else if (key.isReadable()) { // There is data to readBuffer. clear ();// Reading to the end of the stream indicates that the TCP connection has been disconnected.// So you need to close the channel or cancel listening for READ events// Otherwise, it will cycle indefinitely.if (((SocketChannel) key.channel()).read(buffer) == -1) {
                        key.channel().close();
                        continue;
                    } 

                    // Traversing data byte by byteBuffer. flip ();while (buffer.hasRemaining()) {
                        byte b = buffer.get();

                        if (b == 0) { // \0 at the end of the client messageSystem. out. println ();// Response ClientBuffer. clear ();Buffer.put ("Hello, Client!\0".getBytes());
                            buffer.flip();
                            while (buffer.hasRemaining()) {
                                ((SocketChannel) key.channel()).write(buffer);
                            }
                        } else {
                            System.out.print((char) b);
                        }
                    }
                }

                // Handled events must be removed manuallyKeyIter. remove ();}}}} Copy code

Client

This client is purely for testing. In order to look less laborious, it is written in the traditional way. The code is very short.

To test rigorously, a large number of Clients should be run concurrently to count the response time of the server, and do not send data immediately after the connection is established, so as to give full play to the advantages of non-blocking I/O of the server.

public class Client {

    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("localhost", 9999);
        InputStream is = socket.getInputStream();
        OutputStream os = socket.getOutputStream();

        // Send data to server firstOs.write ("Hello, Server!\0".getBytes());

        // Read the data from the serverint b;
        while ((b = is.read()) != 0) {
            System.out.print((char) b);
        }
        System.out.println();

        socket.close();
    }
}Copy code

NIO vs IO

After learning NIO, we all have the question: when should we use NIO and when should we use traditional I/O?

In fact, after understanding their characteristics, the answer is quite clear. NIO is good at managing multiple connections in one thread and saving system resources, but if the amount of data transmitted by each connection is large, because it is synchronous I/O, the overall response speed will be very slow; while traditional I/O creates for each connection.A thread can make full use of the parallel processing ability of the processor, but if the number of connections is too large, the memory resources will be very tight.

The conclusion is that the number of connections is more data than NIO, and the number of connections is less I/O (which is easy to write).

Next

Through the learning of NIO core components, we understand the basic method of non-blocking server implementation. However, careful you must also find that the complete example above actually hides a lot of problems. For example, in the example, the output of each byte read is simple, and it must be read in the real environment.Because of the non-blocking nature of NIO, it may only read a part of the message at a time, which is very bad. If the same connection will send more than one message in a row, it not only needs to splice the message, but also needs to be cut. Similarly, in the example, the client responds.When using a while () loop to ensure that all the data are written before doing other work, in practical application, for performance, it will certainly not be written like this. In addition, in order to make full use of the multi-core parallel processing capabilities of modern processors, a thread group should be used to manage the events of these connections.

To solve these problems, we need a rigorous and tedious design, but fortunately, we have an open source framework available, that is, elegant and powerful Netty, Netty based on Java NIO, provides an asynchronous invocation interface, a good choice for developing high-performance servers.I have used it in my eyes, but I haven’t studied it in depth. I plan to learn it well next step, and then write a note.

Java NIOThe goal of the design is to provide APIs for programmers to enjoy the latest I/O mechanism of modern operating systems, so the coverage is wide. In addition to the components and features involved in this article, there are many other things, such as Pipe (pipeline), Path (path), Files (file), etc. Some of them are used.Some of the new components to improve I/O performance are tools to simplify I/O operations. See the links in the final References for specific usage.

Leave a Reply

Your email address will not be published. Required fields are marked *