Heart containing Coding Chica Java 101

Building As We Go! Java’s StringBuffer for Multi-Threaded Use Cases

TIP: References Quick List

Table of Contents

  1. Table of Contents
  2. Introduction
  3. Unit Tests
  4. Method Signature / Synchronization
    1. Maven Build Failure
  5. Adding the Default Behavior
    1. Maven Build Failure
  6. Appending Names
    1. Maven Build Failure
    2. Moving the Exclamation Point
    3. Maven Build Success
  7. 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 StringBuffer is designed to be safe to use concurrently from multiple threads, if the constructor or the append or insert operation 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.

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/StringBuffer.html

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.

Building As We Go! Java’s StringBuffer for Multi-Threaded Use Cases

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.