Table of Contents
- Table of Contents
- Introduction
- Unit Tests
- Method Signature / Synchronization
- Adding the Default Behavior
- Appending Names
- Summary
Introduction
When we need to construct a String piece-by-piece, but the construction might be done by multiple treads, then we can use Java’s thread-safe StringBuffer.
A thread-safe, mutable sequence of characters. A string buffer is like a
String, but can be modified. At any point in time it contains some particular sequence of characters, but the length and content of the sequence can be changed through certain method calls.String buffers are safe for use by multiple threads. The methods are synchronized where necessary so that all the operations on any particular instance behave as if they occur in some serial order that is consistent with the order of the method calls made by each of the individual threads involved.
…
Whenever an operation occurs involving a source sequence (such as appending or inserting from a source sequence), this class synchronizes only on the string buffer performing the operation, not on the source. Note that while
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/StringBuffer.htmlStringBufferis designed to be safe to use concurrently from multiple threads, if the constructor or theappendorinsertoperation is passed a source sequence that is shared across threads, the calling code must ensure that the operation has a consistent and unchanging view of the source sequence for the duration of the operation. This could be satisfied by the caller holding a lock during the operation’s call, by using an immutable source sequence, or by not sharing the source sequence across threads.
Unit Tests
Let’s add some unit tests to java101/src/test/java/codingchica/java101/AppTest.java to confirm when the functionality is correct:
@Nested
class AddNameToGreetingTest {
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", " "})
void addNameToGreeting_whenNamesNullOrBlank_thenReturnsExpectedValue(String name) {
// Setup
String expectedResult = "Hello!";
App app = new App("Sally");
// Execution
String actualGreeting = app.addNameToGreeting(name);
// Validation
assertEquals(expectedResult, actualGreeting, () -> String.format("Adding %s", name));
}
@ParameterizedTest
@CsvSource(
value = {
" |Mary Jane|Jim|Jane:Hello!|Hello Mary Jane!|Hello Mary Jane, Jim!|Hello Mary Jane, Jim!",
" :Hello!"
},
ignoreLeadingAndTrailingWhitespace = false,
delimiter = ':')
void addNameToGreeting_whenNamesPopulated_thenReturnsExpectedValue(
String pipeDelimitedNames, String pipeDelimitedExpectedResults) {
// Setup
String[] names = pipeDelimitedNames.split("\\|");
String[] expectedResults = pipeDelimitedExpectedResults.split("\\|");
assertEquals(names.length, expectedResults.length, "size");
App app = new App("Someone");
IntStream.range(0, names.length)
.forEachOrdered(
count -> {
// Execution
String actualGreeting = app.addNameToGreeting(names[count]);
// Validation
assertEquals(
expectedResults[count], actualGreeting, () -> String.format("Adding %s", names[count]));
});
}
}
Method Signature / Synchronization
Although individual method calls to the StringBuffer are synchronized, in this case, we still need to ensure that the combination of methods we plan to invoke are all completed as a single unit. In this example, we have one method that interacts with the StringBuffer and that method is annotated with the Lombok Synchronized annotation to ensure that it runs one instance at a time.
A similar need to synchronize would occur if we had a simple single-method insert, but used an Object or a StringBuffer as the source of the contents to append/insert.
Let’s add the method signature to java101/src/main/java/codingchica/java101/App.java:
/**
* Call with no name (null or empty name) to obtain the existing greeting.
* Call with a non-blank name to add the name to the existing greeting and return it.
*
* @param name The name to add to the greeting, if any.
* @return The current greeting.
*/
@Synchronized
public String addNameToGreeting(String name) {
return null;
}
Maven Build Failure
At this point, if we run the Maven build (mvn clean site install) we should see errors like:
[ERROR] Failures: [ERROR] AppTest$AddNameToGreetingTest.addNameToGreeting_whenNamesNullOrBlank_thenReturnsExpectedValue:349 Adding null ==> expected: <Hello!> but was: <null> [ERROR] AppTest$AddNameToGreetingTest.addNameToGreeting_whenNamesNullOrBlank_thenReturnsExpectedValue:349 Adding ==> expected: <Hello!> but was: <null> [ERROR] AppTest$AddNameToGreetingTest.addNameToGreeting_whenNamesNullOrBlank_thenReturnsExpectedValue:349 Adding ==> expected: <Hello!> but was: <null> [ERROR] AppTest$AddNameToGreetingTest.addNameToGreeting_whenNamesNullOrBlank_thenReturnsExpectedValue:349 Adding ==> expected: <Hello!> but was: <null> [ERROR] AppTest$AddNameToGreetingTest.addNameToGreeting_whenNamesPopulated_thenReturnsExpectedValue:370->lambda$addNameToGreeting_whenNamesPopulated_thenReturnsExpectedValue$2:376 Adding ==> expected: <Hello!> but was: <null> [ERROR] AppTest$AddNameToGreetingTest.addNameToGreeting_whenNamesPopulated_thenReturnsExpectedValue:370->lambda$addNameToGreeting_whenNamesPopulated_thenReturnsExpectedValue$2:376 Adding ==> expected: <Hello!> but was: <null>
Adding the Default Behavior
Let’s update the new addNameToGreeting method to include some basic logic to for the default behavior:
@Synchronized
public String addNameToGreeting(String name) {
String prefix = "Hello!";
if (greetingStringBuffer.length() == 0) {
greetingStringBuffer.append(prefix);
}
return greetingStringBuffer.toString();
}
Maven Build Failure
At this point, if we rerun the Maven build (mvn clean site install) we should expect fewer errors, like:
[ERROR] Failures: [ERROR] AppTest$AddNameToGreetingTest.addNameToGreeting_whenNamesPopulated_thenReturnsExpectedValue:370->lambda$addNameToGreeting_whenNamesPopulated_thenReturnsExpectedValue$2:376 Adding Mary Jane ==> expected: <Hello Mary Jane!> but was: <Hello!>
Appending Names
Let’s update the new addNameToGreeting method to include logic to append commas and new names to the end of the string:
@Synchronized
public String addNameToGreeting(String name) {
String prefix = "Hello!";
if (greetingStringBuffer.length() == 0) {
greetingStringBuffer.append(prefix);
}
if (name != null && !name.isBlank() && greetingStringBuffer.indexOf(name) < 0) {
if (greetingStringBuffer.length() > prefix.length()) {
greetingStringBuffer.append(',');
}
greetingStringBuffer.append(' ');
greetingStringBuffer.append(name);
}
return greetingStringBuffer.toString();
}
Maven Build Failure
Now, if we rerun the Maven build (mvn clean site install) we should expect a different error message, like:
[ERROR] Failures: [ERROR] AppTest$AddNameToGreetingTest.addNameToGreeting_whenNamesPopulated_thenReturnsExpectedValue:370->lambda$addNameToGreeting_whenNamesPopulated_thenReturnsExpectedValue$2:376 Adding Mary Jane ==> expected: <Hello Mary Jane!> but was: <Hello! Mary Jane>
Moving the Exclamation Point
Now, let’s add logic to the addNameToGreeting method to move the exclamation point (AKA bang) character to the end as we add new names:
@Synchronized
public String addNameToGreeting(String name) {
String prefix = "Hello!";
if (greetingStringBuffer.length() == 0) {
greetingStringBuffer.append(prefix);
}
if (name != null && !name.isBlank() && greetingStringBuffer.indexOf(name) < 0) {
int indexOfBang = greetingStringBuffer.indexOf("!");
if (indexOfBang >= 0) {
greetingStringBuffer.delete(indexOfBang, greetingStringBuffer.length());
}
if (greetingStringBuffer.length() > prefix.length()) {
greetingStringBuffer.append(',');
}
greetingStringBuffer.append(' ');
greetingStringBuffer.append(name);
greetingStringBuffer.append('!');
}
return greetingStringBuffer.toString();
}
Maven Build Success
At this point, we should now expect the build to succeed, as the functionality is now in place:
[INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 24.519 s
Summary
Using Java’s StringBuffer in multi-threaded use cases where we need to build or modify a String from multiple input sources can help ensure that updates occur as we expect, even if multiple threads try to make updates in parallel. However, if we need multiple appends/inserts/updates for a single update transaction, or if the source object that we’re passing into StringBuffer methods is subject to change while the StringBuffer method is being executed (such as an Object or another StringBuffer accessed by multiple threads), then we also need to do some synchronization in the calling code, as well.

Leave a comment