Why Buffer

First of all, why do we need to talk about a small Buffer separately? In other words, where exactly are buffers used?

For example, if we read a file from disk, instead of loading it directly from disk into memory, we first copy the data from disk into the kernel buffer, and then from the kernel buffer into the user buffer, which looks something like this:

For example, when we write files to disk, we don’t write data directly to disk. Instead, data is written from the user buffer to the kernel buffer, which is then flushed to disk by the operating system.

For example, when the server receives data from the client, it does not go directly to the user Buffer. Instead, the nic is first copied to the kernel-mode Buffer and then copied from the kernel-mode Buffer to the user-mode Buffer.

So why bother? Copy and copy and copy. First of all we’re going to exclude by elimination and we’re going to do it for fun.

Buffers exist to reduce the frequency of interactions with devices such as disks, and as mentioned in the previous blog, “reading and writing to disks is an expensive operation.” So what’s expensive about it? Simply put, interactions with devices (such as IO to disks) are designed to interrupt the operating system. Interrupt needs to save the running context of the previous process, and need to restore the context after the interrupt, and also involves the switch between kernel mode and user mode, which is a time-consuming operation in general.

If you’re not familiar with an operating system, you might be a little confused. Such as:

  • What is user mode
  • What is kernel state

You can check out my article on the differences between user-mode and kernel mode.

The use of the Buffer

We’ll show you how Buffer is implemented in the NIO package in Java. There are seven implementations of Buffer that contain all the data types implemented in Java.

In this article, we use ByteBuffer, which has the following common methods:

  • put
  • get
  • flip
  • rewind
  • mark
  • reset
  • clear

Let’s take a look at these methods with a practical example.

put

To put is to write data to a ByteBuffer, and there are many implementations of overloading:

public ByteBuffer put(ByteBuffer src) {... }public ByteBuffer put(byte[] src, int offset, int length) {... }public final ByteBuffer put(byte[] src) {... }Copy the code

We can pass ByteBuffer objects directly, we can pass native byte arrays directly, and we can specify offset, length, and so on. Here’s a concrete example:

public static void main(String[] args) {
    ByteBuffer buffer = ByteBuffer.allocate(16);
    buffer.put(new byte[] {'s'.'h'});
}
Copy the code

In order to give you a more intuitive view of the internal situation of ByteBuffer, I sorted it into the form of a graph. When the above code runs, the buffer will look like this:

When you try to print the variable buffer using system.out.println (buffer), you get something like this:

java.nio.HeapByteBuffer[pos=2 lim=16 cap= 16]Copy the code

Capacity is the size of the ByteBuffer that we created, 16.

For the other two variables, you can see from the diagram that the position variable refers to the subscript to be written next time. In the above code, we only wrote 2 bytes, so position refers to 2, and the limit is interesting. This will be used in conjunction with examples later on.

get

Get gets data from ByteBuffer.

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'s'.'h'});
  System.out.println(buffer.get());
}
Copy the code

If you run through the code above you’ll see that it prints out 0, not 115 ASCII for S as we’d expect.

I’m just going to tell you that this is what you expect, and you shouldn’t be able to get a value at this point. Let’s look at the source code of get:

public byte get(a) { return hb[ix(nextGetIndex())]; }

protected int ix(int i) { return i + offset; }

final int nextGetIndex(a) {                          
  int p = position;
  if (p >= limit)
    throw new BufferUnderflowException();
  // Position will be moved back one bit
  position = p + 1;
  return p;
}
Copy the code

Position is 2 and limit is 16, so the final nextGetIndex is the value of p at 2, and ix is 2 + 0 = 2, where offset defaults to 0.

So in a nutshell, you end up with something with index 2, which looks like this.

So of course we don’t have data. The important thing to note here is that calling the GET method does not get any data, but causes the position pointer to move backwards. In other words, it takes up a place. If you call the GET method several times in a row and then call the PUT method to write data, you will leave several places unassigned. For example, suppose we run the following code:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'s'.'h'});

  buffer.get();
  buffer.get();
  buffer.get();
  buffer.get();

  buffer.put(new byte[] {'e'});
}
Copy the code

The data will look like the following, and position will move backwards

So you might ask, what if I really need to get the data? In this case, you can get it like this:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'s'});
  System.out.println(buffer.get(0)); / / 115
}
Copy the code

If we pass in the index we want to retrieve, we get it without moving position back.

Get () = get() = get() I have to give you an index. Which brings us to another method, flip.

flip

Without further ado, let’s take an example:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'s'.'h'}); // java.nio.HeapByteBuffer[pos=2 lim=16 cap=16]
  buffer.flip();
  System.out.println(buffer); // java.nio.HeapByteBuffer[pos=0 lim=2 cap=16]
}
Copy the code

Something interesting happened. After the flip call, position changed from 2 to 0 and Limit from 16 to 2.

This word means “to turn over”, and my personal understanding of it is to turn over everything you’ve saved as if it were something

You’ll notice that position becomes 0 and limit becomes 2, which happens to be the range of values.

Now it gets more interesting:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'s'.'h'});
  buffer.flip();
  System.out.println((char)buffer.get()); // s
  System.out.println((char)buffer.get()); // h
}
Copy the code

After calling flip, the previously unusable get() actually works. It is not difficult to analyze the source code given in GET. Since position becomes 0, the final calculated result is 0, and the position is moved back one bit.

Here we are. You can understand that Buffer has two states, which are:

  • Read mode
  • Write mode

The newly created ByteBuffer is in write mode and can be switched to read mode by calling flip. Note, however, that the read/write mode is only a logical concept.

For example, when the flip call switches to so-called write mode, you can still call the PUT method to write data to The ByteBuffer.

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'s'.'h'});
  buffer.flip();
  buffer.put(new byte[] {'e'});
}
Copy the code

The put operation still succeeds, but you’ll notice that the last e overwrites the previous data, and now the value of ByteBuffer is eh instead of sh.

So you should be able to see by now that read mode and write mode means more:

  • Convenient mode for you to read
  • Convenient mode for you to write

By the way, a BufferUnderflowException will be thrown if a subsequent call to GET () causes position to be greater than or equal to limit after flip is called. This can also be seen from the previous get source code.

rewind

Rewind can also be used to run commands in read mode.

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'s'.'h'});
  buffer.flip();
  System.out.println((char)buffer.get()); // s
  System.out.println((char)buffer.get()); // h

  // Read from the beginning
  buffer.rewind();

  System.out.println((char)buffer.get()); // s
  System.out.println((char)buffer.get()); // h
}
Copy the code

Position = 0; position = 0;

public final Buffer rewind(a) {
  position = 0;
  mark = -1;
  return this;
}
Copy the code

Simply assign position to 0 and mark to -1. And who is mark? That’s the next method we’re going to talk about.

mark & reset

Mark is used to mark the position of the current postion. Reset is used to reset the position of the current postion.

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'a'.'b'.'c'.'d'});
  
  // Switch to read mode
  buffer.flip();
  System.out.println((char) buffer.get()); // a
  System.out.println((char) buffer.get()); // b

  // Remember current position
  buffer.mark();
  
  System.out.println((char) buffer.get()); // c
  System.out.println((char) buffer.get()); // d

  // Reset position to Mark's position
  buffer.reset();
  System.out.println((char) buffer.get()); // c
  System.out.println((char) buffer.get()); // d
}
Copy the code

So what you can see is that when position is equal to 2, we call mark and remember position. And then I went through all the data. We then call reset to return position to 2. We continue to call GET, and CD is ready to print again.

clear

Clear means to clear the buffer, but it’s not. Look at this:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'a'.'b'.'c'.'d'});
}
Copy the code

After putting, the buffer looks like this.

When we call clear, the buffer will look like this.

Clear: write (write, write); clear: write (write, write, write); clear: write (write, write, write);

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put(new byte[] {'a'.'b'.'c'.'d'});
  buffer.clear();
  buffer.put(new byte[] {'s'.'h'});
}
Copy the code

As you can see, the buffer becomes SHCD after the run, and the data written later overwrites the previous data.

In addition to clear switching to write mode, there is another way to switch, which is the last method in this article called Compact.

compact

Compact is used to move unread data to the head of Buffer and switch to write mode as follows:

public static void main(String[] args) {
  ByteBuffer buffer = ByteBuffer.allocate(16);
  buffer.put("abcd".getBytes(StandardCharsets.UTF_8));

  // Switch to read mode
  buffer.flip();
  System.out.println((char) buffer.get()); // a

  // Move unread data to the head of buffer
  buffer.compact(); // The buffer data becomes BCDD
}
Copy the code

After running flip, the buffer should be fine:

What happened after Compact? Two things in a nutshell:

  1. willpositionMove to the corresponding position
  2. Move unread data tobufferThe first

What is this correspondence? I’ll give you an example; For example, if the unread data is BCD, position is 3; If the unread data is CD, position is 2. So you see that the value of position is the length of the unread data.

From the internal implementation mechanism of buffer, all data within position-limit is considered as unread data

So, after running Compact, the buffer looks like this:

Limit is 16 because Compact puts buffer into what is called write mode.

EOF

There are some other methods that I will not list here, but you can play with them yourself if you are interested, there is no difficulty in understanding. And then we’ll probably write channels and selectors, because Java’s NIO, if you’re interested.

Welcome to wechat search and follow [SH full stack Notes]. If you think this article is helpful to you, please click a like, close a note, share a share and leave a message.