| /* | |
| * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* | |
| * @test | |
| * @summary Test multi-threaded use of StringBuilder | |
| * @compile --release 8 RacingSBThreads.java | |
| * @run main/othervm -esa RacingSBThreads read | |
| * @run main/othervm -esa RacingSBThreads insert | |
| * @run main/othervm -esa RacingSBThreads append | |
| * @run main/othervm -Xcomp RacingSBThreads | |
| */ | |
| import java.nio.CharBuffer; | |
| import java.time.Duration; | |
| import java.time.Instant; | |
| import java.util.Arrays; | |
| import java.util.Locale; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import java.util.function.BiConsumer; | |
| /** | |
| * Test racing accesses in StringBuilder. | |
| * Test source code should be compatible with JDK 8 to allow testing on older versions. | |
| */ | |
| public class RacingSBThreads { | |
| private static final int TIMEOUT_SEC = 1; // Duration to run each test case | |
| private static final int N = 10_000_000; // static number of iterations for writes and modifies | |
| private static final int LEN = 100_000; // Length of initial SB | |
| // Strings available to be used as the initial contents of a StringBuilder | |
| private static final String UTF16_CHARS = initString('\u1000', LEN); | |
| private static final String LATIN1_CHARS = initString('a', LEN); | |
| // Cache jtreg timeout factor to allow test to be run as a standalone main() | |
| private static final double TIMEOUT_FACTOR = Double.parseDouble(System.getProperty("test.timeout.factor", "1.0")); | |
| // Constant arguments available to be passed to StringBuilder operations | |
| private static final StringBuilder otherSB = new StringBuilder("ab\uFF21\uFF22"); | |
| private static final StringBuilder otherLongerSB = new StringBuilder("abcde\uFF21\uFF22\uFF23\uFF24\uFF25"); | |
| // Create a String with a repeated character | |
| private static String initString(char c, int len) { | |
| char[] chars = new char[len]; | |
| Arrays.fill(chars, c); | |
| return new String(chars); | |
| } | |
| // Plain unsynchronized reference to a StringBuilder | |
| // Updated by the writer thread | |
| // Read by the reader thread | |
| private StringBuilder buf; | |
| // The current stress test case | |
| private final StressKind stressKind; | |
| // Count of faults, zero if no faults found | |
| private final AtomicInteger faultCount = new AtomicInteger(0); | |
| /** | |
| * Run the stress cases indicated by command line arguments or run all cases. | |
| * Running each for TIMEOUT_SEC seconds or until a failure. | |
| * The timeout/test duration can be scaled by setting System property | |
| * `test.timeout.factor` to a double value, for example, `-Dtest.timeout.factor=2.0` | |
| * @param args command line arguments | |
| */ | |
| public static void main(String[] args) { | |
| Duration duration = Duration.ofSeconds((long)(TIMEOUT_SEC * TIMEOUT_FACTOR)); | |
| StressKind[] kinds = StressKind.values(); | |
| if (args.length > 0) { | |
| // Parse explicitly supplied StressKind arguments | |
| try { | |
| kinds = Arrays.stream(args) | |
| .map((s) -> StressKind.valueOf(s.toUpperCase(Locale.ROOT))) | |
| .toArray(StressKind[]::new); | |
| } catch (Exception ex) { | |
| System.out.println("Invalid StressKind arguments: " + Arrays.toString(args)); | |
| return; | |
| } | |
| } | |
| // Run each kind for the duration | |
| int totalFaults = 0; | |
| for (StressKind sk : kinds) { | |
| Instant end = Instant.now().plus(duration); // note clock time, not runtime | |
| while (Instant.now().isBefore(end)) { | |
| int faultCount = new RacingSBThreads(sk).stress(); | |
| if (faultCount > 0) { | |
| System.out.printf("ERROR: Test case %s, %d faults%n", sk, faultCount); | |
| } | |
| totalFaults += faultCount; | |
| } | |
| } | |
| if (totalFaults > 0) { | |
| throw new AssertionError("Total faults: " + totalFaults); | |
| } | |
| } | |
| // Enum of the various test cases with a lambda to invoke for each | |
| enum StressKind { | |
| /** | |
| * Reading characters should always be one of the known values being written to the destination | |
| */ | |
| READ(LATIN1_CHARS, (sb, chr) -> { | |
| char ch = sb.charAt(LEN * 4 / 5); | |
| if (ch != chr & ch != (chr & 0xff) & ch != chr >> 8) { | |
| throw new AssertionError("Unexpected characters in buffer: 0x" + Integer.toHexString(ch)); | |
| } | |
| }), | |
| /** | |
| * Insert another StringBuilder; in the face of racy changes to the destination | |
| */ | |
| INSERT(LATIN1_CHARS, (sb, C) -> { | |
| sb.insert(sb.length() - 1, otherLongerSB, 0, otherLongerSB.length()); | |
| }), | |
| /** | |
| * Appending a StringBuilder in the face of racy changes to the destination | |
| */ | |
| APPEND(LATIN1_CHARS, (sb, C) -> { | |
| sb.append(otherSB, 0, otherSB.length()); | |
| }), | |
| ; | |
| private final BiConsumer<StringBuilder,Character> func; | |
| private final String sbInitString; | |
| /** | |
| * Defines a test case. | |
| * @param sbInitString the initial contents of the StringBuilder; chooses the coder | |
| * @param func the test BiConsumer to apply to the StringBuilder | |
| */ | |
| private StressKind(String sbInitString, BiConsumer<StringBuilder,Character> func) { | |
| this.func = func; | |
| this.sbInitString = sbInitString; | |
| } | |
| } | |
| public RacingSBThreads(StressKind stressKind) { | |
| this.stressKind = stressKind; | |
| } | |
| /** | |
| * Run the stress case. | |
| * One thread continuously creates a StringBuilder and fills it before trimming it to zero. | |
| * The other thread performs the test case on the same StringBuilder (without any synchronization) | |
| * @return the count of faults | |
| */ | |
| private int stress() { | |
| PokeBuilder r = new PokeBuilder(this, N); | |
| Writer w = new Writer(this, N); | |
| Thread writer = new Thread(w::createShrink); | |
| Thread reader = new Thread(r::readModify); | |
| writer.start(); | |
| reader.start(); | |
| join(reader); | |
| System.out.println(r); | |
| writer.interrupt(); | |
| join(writer); | |
| System.out.println(w); | |
| return r.racing.faultCount.get(); | |
| } | |
| /** | |
| * Wait for a thread to terminate. | |
| * @param thread a thread to wait for | |
| */ | |
| private void join(Thread thread) { | |
| do { | |
| try { | |
| thread.join(); | |
| break; | |
| } catch (InterruptedException ie) { | |
| // ignore and retry | |
| } | |
| } while (true); | |
| } | |
| /** | |
| * Run a StressKind case in a loop keeping track of exceptions. | |
| * The StringBuilder under test is shared with the writer task without benefit of synchronization. | |
| */ | |
| private static class PokeBuilder { | |
| private final RacingSBThreads racing; | |
| private final int iterations; | |
| private int nulls; | |
| private int bounds; | |
| private int pokeCycles; | |
| private int bufChanges; | |
| public PokeBuilder(RacingSBThreads racing, int iterations) { | |
| this.racing = racing; | |
| this.iterations = iterations; | |
| nulls = 0; | |
| bounds = 0; | |
| pokeCycles = 0; | |
| bufChanges = 0; | |
| } | |
| // Repeatedly change the racy StringBuilder, ignoring and counting exceptions | |
| private void readModify() { | |
| System.out.println("Starting " + racing.stressKind); | |
| sleep(100); | |
| for (int i = 0; i < iterations; ++i) { | |
| pokeCycles++; | |
| StringBuilder sb = racing.buf; // read once | |
| try { | |
| if (sb.length() > Integer.MAX_VALUE / 4) { | |
| sb.setLength(Integer.MAX_VALUE / 4); | |
| } | |
| // Invoke the test case | |
| racing.stressKind.func.accept(sb, racing.stressKind.sbInitString.charAt(0)); | |
| if (sb != racing.buf) { | |
| bufChanges++; | |
| } | |
| } catch (NullPointerException e) { | |
| ++nulls; | |
| } catch (IndexOutOfBoundsException e) { | |
| ++bounds; | |
| } catch (AssertionError ae) { | |
| racing.faultCount.incrementAndGet(); | |
| throw ae; | |
| } | |
| } | |
| } | |
| private static void sleep(int i) { | |
| try { | |
| Thread.sleep(i); | |
| } catch (InterruptedException ignored) { | |
| } | |
| } | |
| public String toString() { | |
| return String.format("pokeCycles:%d, bounds:%d, bufChanges:%d, nulls=%d", | |
| pokeCycles, bounds, bufChanges, nulls); | |
| } | |
| } | |
| /** | |
| * Repeatedly create and append strings to a StringBuilder shared through fields of RacingSBThreads. | |
| * The StringBuilder is created new on each iteration and truncated at the end of each iteration. | |
| * Exceptions are counted and reported. | |
| */ | |
| private static class Writer { | |
| private final RacingSBThreads racing; | |
| private final int iterations; | |
| private int sumWriter; | |
| private int writeCycles; | |
| private int putBounds; | |
| public Writer(RacingSBThreads racing, int iterations) { | |
| this.racing = racing; | |
| this.iterations = iterations; | |
| } | |
| private void createShrink() { | |
| for (int i = 0; i < iterations; ++i) { | |
| if (i % 100_000 == 0) { | |
| if (Thread.interrupted()) { | |
| break; | |
| } | |
| } | |
| try { | |
| ++writeCycles; | |
| racing.buf = new StringBuilder(racing.stressKind.sbInitString); | |
| racing.buf.append(UTF16_CHARS); | |
| sumWriter += racing.buf.length(); | |
| racing.buf.setLength(0); | |
| racing.buf.trimToSize(); | |
| } catch (Exception ex) { | |
| ++putBounds; | |
| } | |
| } | |
| } | |
| public String toString() { | |
| return String.format("writeCycles:%d, bounds:%d, sumWriter=%d", writeCycles, putBounds, sumWriter); | |
| } | |
| } | |
| } |