Before we start to introduce the IO in Java, we need to understand IO in operating system, which will make us easier to understand the mechanism under the hood and to opt the program.
Basic of IO
User Space & Kernel Space
IO operation can only be done by kernel thread/process, so whenever a user process request an IO operation, it will give the task to kernel to do it. Depends on how user process react when submitting task and waiting for response, there exists many ways to classify:
- Whether user process is blocked: Block vs Non-block
- Whether user process get data by polling in loop or by notification: Synchronous vs Asynchronous
We will cover more details and examples about this in this serial of blogs.
Disk Speed
The following is some speed statistic taken from this gist
Latency Comparison Numbers
--------------------------
L1 cache reference 0.5 ns
Branch mispredict 5 ns
L2 cache reference 7 ns 14x L1 cache
Mutex lock/unlock 25 ns
Main memory reference 100 ns 20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy 3,000 ns 3 us
Send 1K bytes over 1 Gbps network 10,000 ns 10 us
Read 4K randomly from SSD* 150,000 ns 150 us ~1GB/sec SSD
From which, we can easily see how cache of disk driver and buffer can speed up performance, and how should we opt our IO performance.
Devices
Operating systems divide IO devices into two categories: character device and block device. Character device is often used to read/write char one by one, while block device is often operating in blocks and buffer. The example of block device is like hard disk, while char device is like serial port.
EOF
What does EOF on a connection mean? The idea of EOF is often confusing to newbie, especially in the context of Internet connections. First, we need to understand that there is no such thing as an EOF character. Rather, EOF is a condition that is detected by the kernel. An application finds out about the EOF condition when it receives a zero return code from the read function. For disk files, EOF occurs when the current file position exceeds the file length. For Internet connections, EOF occurs when a process closes its end of the connection. The process at the other end of the connection detects the EOF when it attempts to read past the last byte in the stream.
IO in Java
Back to the world when Java just came out, it has a strong and complex IO designs. It has two sub types: character stream and byte stream. The IO model make the abstraction that every IO operation is done with a stream of data, and, depending on how to interpret the data, separated into Reader&Writer
– character stream and InputStream&OutputStream
– byte stream. The full picture of IO army’s hierarchy is like following:
As we can see, each stream has many sub stream types, and thanks to the Decorator
design patterns, they can combine with each other to produce much more different streams. Some people criticize the complexity of this design: even a simple write to file can be like following:
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("file")));
writer.write(123);
Because the functionality (interface
) of Reader/Writer
is much human friendly, which will convert data in stream into Java data types, people much prefer to use Reader/Writer
. So Java IO army provides a bridge to convert the InputStream&OutputStream
to Reader&Writer
– InputStreamReader&OutputStreamWriter
.
Limitations of IO
Aside from this inconvenience, Java IO is too important to leave out, so it is still heavily used. But, with the prevalence of Java IO, more drawbacks of this IO model is exposed.
Buffered
The default behavior of this IO model will not use buffer to assist read/write performance, unless user directly add a Buffered
decoration deliberately like above usage. The performance distinction can be very large between buffering and no buffering.
When running the following code snippet with a 1MB
file, the run time is 1530.x ms
, 19.xxx ms
, 5.xxx ms
on my computer.
FileInputStream fis = new FileInputStream(file);
int cnt = 0;
int b;
while ((b = fis.read()) != -1) {
if (b == '\n') {
cnt++;
}
}
FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis);
int cnt = 0;
int b;
while ((b = bis.read()) != -1) {
if (b == '\n') {
cnt++;
}
}
FileInputStream fis = new FileInputStream(file);
byte buf[] = new byte[2048];
int cnt = 0;
int n;
while ((n = fis.read(buf)) != -1) {
for (int i = 0; i < n; i++) {
if (buf[i] == '\n') {
cnt++;
}
}
}
And if we need to output to a terminal window, we will met output buffering – line buffering. It is important for a terminal to show printed result when a newline character is encountered. That means, System.out
will flush a line if there is a newline. So if we disable line buffering
, we can output a larger block of character which will be much faster.
Rules to Opt
From the above examples and trials, we can have following principles when do IO optimizations:
- Avoid accessing the disk & underlying operating system.
- Avoid method calls.
- Avoid processing bytes and characters one by one.
Blocking
Besides the buffering problem, there is another problems: blocking. In old IO model, all IO operation is blocking operation, i.e. program sends the read/write request and waiting until the operation is done. If we need to do multiple IO operation at the same time, we need multiple threads. This works if simultaneous IO operation is not so much, but what if there exists 150000 short IO request at the same time? You may argue that there is no such situation, but there is. Considering a web server which provides request for user, it can be popular and so many user access it at the same time. In this case, a blocking IO model is not suitable, for the additional cost:
- Too many thread to allocate;
- Memory consumption of thread (64k or even more), waste of resource;
- The performance penalty of context switch for different threads;
So, later Java
introduced more advanced IO model – Non-blocking IO – to solve this kind of problems and we will cover in next post.
Ref
Written with StackEdit.
评论
发表评论