Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
273: The StringBuilder and StringBuffer Compared
I want to show you a piece of code that looks perfectly fine at first glance. Imagine you're building a system that collects logs from several different threads—maybe different parts of a web server handling requests—and you're appending them to a single shared buffer to write to a file later.
public class LogCollector {
private StringBuilder sharedLog = new StringBuilder();
public void addLog(String message) {
sharedLog.append(message).append("\n");
}
public String getFullLog() {
return sharedLog.toString();
}
}
// Usage in a multi-threaded environment
// Thread 1: collector.addLog("User A logged in");
// Thread 2: collector.addLog("User B clicked Home");
// Thread 3: collector.addLog("Database connection lost!");
The Mystery of the ArrayIndexOutOfBoundsException
If you run this in a single-threaded test, it works flawlessly. But once you deploy this to a production environment with actual concurrency, you're going to see something strange. Your logs will occasionally be missing characters, or worse, your application will crash with an ArrayIndexOutOfBoundsException coming from inside the StringBuilder.append() method.
Here is what's happening: StringBuilder is not thread-safe. It manages an internal character array. When that array fills up, StringBuilder allocates a larger array and copies the old data over. If two threads call append() at the exact same millisecond, they might both see that there is one slot left in the array. They both try to write to that same slot, or one tries to write while the other is in the middle of resizing the array. It's a classic race condition.
ReplacingStringBuilder with StringBuffer
In most of your daily work, StringBuilder is the right choice because it's faster. But in this specific case, we need synchronization. This is where StringBuffer comes in. To fix this, we simply swap the implementation:
public class LogCollector {
// Changed StringBuilder to StringBuffer
private StringBuffer sharedLog = new StringBuffer();
public void addLog(String message) {
sharedLog.append(message).append("\n");
}
public String getFullLog() {
return sharedLog.toString();
}
}
Why does this fix the crash? Because StringBuffer is essentially a "synchronized" version of StringBuilder. Almost every one of its public methods is marked with the synchronized keyword. This ensures that only one thread can modify the internal character array at a time. The race condition vanishes, and your logs remain intact.
Choosing Between Performance and Safety
You might be wondering, "If StringBuffer is safer, why would I ever use StringBuilder?" It comes down to overhead. Acquiring and releasing a lock (synchronization) takes a small amount of time. In a tight loop that runs a million times, that overhead adds up.
I follow a simple rule of thumb:
- Use
StringBuilderfor 95% of your tasks. If the variable is local to a method, it's physically impossible for other threads to access it, so synchronization is a waste of CPU cycles. - Use
StringBufferonly when you have a shared, mutable string buffer that is explicitly accessed by multiple threads simultaneously.
Keep in mind that in modern Java architecture, we usually avoid sharing mutable state like this anyway (preferring thread-safe queues or logging frameworks), but understanding the difference between these two classes is vital for reading legacy code and knowing how Java handles concurrency under the hood.
📋 Practical Task
Build a Concurrent Transaction Journal
You are tasked with creating a TransactionJournal class that allows multiple simulated banking threads to record transactions into a single shared string.
Requirements:
- Create a class
TransactionJournalwith a methodrecordTransaction(String txId, double amount). - The journal must use a thread-safe buffer to ensure that when multiple threads record transactions simultaneously, no data is lost and no exceptions are thrown.
- Implement a
mainmethod that spawns 10 different threads. Each thread should record 100 transactions (1,000 total) into the shared journal. - After all threads have finished (use
join()), print the total length of the final string to verify that all 1,000 entries were recorded successfully.
There are no comments for now.