diff --git a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java index c2f90e22802..b8d209b8a6e 100644 --- a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java +++ b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 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 @@ -29,7 +29,6 @@ import jdk.internal.math.DoubleToDecimal; import jdk.internal.math.FloatToDecimal; import jdk.internal.util.DecimalDigits; -import java.io.IOException; import java.nio.CharBuffer; import java.util.Arrays; import java.util.Spliterator; @@ -360,8 +359,12 @@ abstract sealed class AbstractStringBuilder implements Appendable, CharSequence */ @Override public char charAt(int index) { + byte coder = this.coder; + byte[] value = this.value; + // Ensure count is less than or equal to capacity (racy reads and writes can produce inconsistent values) + int count = Math.min(this.count, value.length >> coder); checkIndex(index, count); - if (isLatin1()) { + if (coder == LATIN1) { return (char)(value[index] & 0xff); } return StringUTF16.getChar(value, index); @@ -420,6 +423,7 @@ abstract sealed class AbstractStringBuilder implements Appendable, CharSequence * of this sequence. */ public int codePointBefore(int index) { + byte[] value = this.value; int i = index - 1; checkIndex(i, count); if (isLatin1()) { @@ -1730,7 +1734,7 @@ abstract sealed class AbstractStringBuilder implements Appendable, CharSequence } else { inflate(); // store c to make sure it has a UTF16 char - StringUTF16.putChar(this.value, j++, c); + StringUTF16.putCharSB(this.value, j++, c); i++; StringUTF16.putCharsSB(this.value, j, s, i, end); return; @@ -1825,7 +1829,7 @@ abstract sealed class AbstractStringBuilder implements Appendable, CharSequence count = j; inflate(); // Store c to make sure sb has a UTF16 char - StringUTF16.putChar(this.value, j++, c); + StringUTF16.putCharSB(this.value, j++, c); count = j; i++; StringUTF16.putCharsSB(this.value, j, s, i, end); diff --git a/test/jdk/java/lang/StringBuilder/RacingSBThreads.java b/test/jdk/java/lang/StringBuilder/RacingSBThreads.java new file mode 100644 index 00000000000..9177f5de1aa --- /dev/null +++ b/test/jdk/java/lang/StringBuilder/RacingSBThreads.java @@ -0,0 +1,303 @@ +/* + * 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 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 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); + } + } +}