Buffered I/O and Performance in Java
Every unbuffered read is a system call. Buffering turns thousands of them into a handful, and it is the single largest I/O win available.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Why buffering matters
An unbuffered stream asks the operating system for data on every call. A buffered one asks for a large block, keeps it in memory, and serves individual reads from there. The difference is not a small percentage; it is often an order of magnitude.
Unbuffered: read() -> system call -> 1 byte x 100000 times
Buffered: read() -> system call -> 8192 bytes x 13 times, rest from memoryThe difference in code
// Slow: one system call per byte
try (InputStream in = Files.newInputStream(path)) {
int b;
while ((b = in.read()) != -1) {
process(b);
}
}
// Fast: the same loop, one wrapper added
try (InputStream in = new BufferedInputStream(Files.newInputStream(path))) {
int b;
while ((b = in.read()) != -1) {
process(b);
}
}The buffered classes
| Class | Wraps | Adds |
|---|---|---|
BufferedInputStream | InputStream | A byte buffer |
BufferedOutputStream | OutputStream | A byte buffer |
BufferedReader | Reader | A char buffer and readLine |
BufferedWriter | Writer | A char buffer and newLine |
BufferedReader reader = new BufferedReader(new FileReader(file), 65536); // custom sizeThe default buffer is 8192 units. A larger buffer helps for very large sequential files, but the gain flattens quickly, so measure before tuning.
readLine comes from buffering
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
}A plain Reader has no readLine. Finding a line break requires looking ahead, which needs a buffer, so the method lives on BufferedReader.
Flushing
BufferedWriter writer = Files.newBufferedWriter(path);
writer.write("important");
// the file may still be empty here: the data is in the buffer
writer.flush(); // force it out
writer.close(); // flushes and then closes// try with resources closes, and therefore flushes, on every path
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writer.write("important");
}Data lost because a writer was never closed is one of the most common I/O bugs. try with resources removes it entirely, which is reason enough to use it every time.
Bulk transfer beats a loop
// Good
try (InputStream in = new BufferedInputStream(Files.newInputStream(source));
OutputStream out = new BufferedOutputStream(Files.newOutputStream(target))) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
// Better
try (InputStream in = Files.newInputStream(source);
OutputStream out = Files.newOutputStream(target)) {
in.transferTo(out);
}
// Best for a plain file copy
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);Note out.write(buffer, 0, read). Writing the whole array would append rubbish from the previous iteration on the final, partial read.
Which Files method already buffers
| Method | Buffered |
|---|---|
Files.newBufferedReader, newBufferedWriter | Yes |
Files.lines | Yes |
Files.readString, readAllLines, readAllBytes | Reads in bulk, so buffering is irrelevant |
Files.newInputStream, newOutputStream | No, wrap it yourself |
Files.copy | Yes, internally |
Memory against speed
// Fast but holds the whole file in memory
List<String> all = Files.readAllLines(path);
// Constant memory, suitable for any size
try (Stream<String> lines = Files.lines(path)) {
lines.filter(this::isRelevant).forEach(this::process);
}Choose by file size. Loading a configuration file entirely is sensible; loading a multi gigabyte log is not.
Random access
try (RandomAccessFile file = new RandomAccessFile("data.bin", "r")) {
file.seek(1024);
int value = file.readInt();
}
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 4096); // read at a position
}Streams are sequential. When arbitrary positions are needed, use RandomAccessFile or a FileChannel.
A practical benchmark shape
long start = System.nanoTime();
long lines = 0;
try (BufferedReader reader = Files.newBufferedReader(path)) {
while (reader.readLine() != null) {
lines++;
}
}
System.out.printf("%d lines in %d ms%n", lines, (System.nanoTime() - start) / 1_000_000);Run the same file through a buffered and an unbuffered reader and the difference speaks for itself. Remember that the operating system also caches, so a second run of the same file is always faster.
Common mistakes
- Reading or writing without buffering.
- Not closing a writer, and losing everything still in the buffer.
- Writing the whole array rather than
write(buffer, 0, read). - Wrapping a
BufferedReaderaround another one, which adds a layer for nothing. - Loading a huge file with
readAllLines. - Calling
flushafter every small write, which defeats the buffer.
Best practices
- Buffer every stream, or use a
Filesmethod that already does. - Always use try with resources.
- Prefer
transferToorFiles.copyto a hand written copy loop. - Stream large files instead of loading them.
- Leave the default buffer size unless measurement shows otherwise.
- Flush only when another process must see the data before the stream closes.
Practice
- Time a buffered and an unbuffered read of the same large file and report the ratio.
- Write to a file without closing the writer and explain what the file contains.
- Why does
readLineexist onBufferedReaderrather than onReader? - Explain the bug in
out.write(buffer)inside a copy loop. - Choose between
readAllLinesandFiles.linesfor a 4 GB log, and justify it.
Conclusion
Buffering converts a system call per byte into a system call per block, and it costs one wrapper. Buffer everything, close everything with try with resources, and stream rather than load when the file is large.