|
| 1 | +/* |
| 2 | + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. |
| 3 | + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| 4 | + * |
| 5 | + * This code is free software; you can redistribute it and/or modify it |
| 6 | + * under the terms of the GNU General Public License version 2 only, as |
| 7 | + * published by the Free Software Foundation. |
| 8 | + * |
| 9 | + * This code is distributed in the hope that it will be useful, but WITHOUT |
| 10 | + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| 11 | + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| 12 | + * version 2 for more details (a copy is included in the LICENSE file that |
| 13 | + * accompanied this code). |
| 14 | + * |
| 15 | + * You should have received a copy of the GNU General Public License version |
| 16 | + * 2 along with this work; if not, write to the Free Software Foundation, |
| 17 | + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| 18 | + * |
| 19 | + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| 20 | + * or visit www.oracle.com if you need additional information or have any |
| 21 | + * questions. |
| 22 | + */ |
| 23 | + |
| 24 | +/* |
| 25 | + * @test |
| 26 | + * @summary Test multi-threaded use of StringBuilder |
| 27 | + * @compile --release 8 RacingSBThreads.java |
| 28 | + * @run main/othervm -esa RacingSBThreads read |
| 29 | + * @run main/othervm -esa RacingSBThreads insert |
| 30 | + * @run main/othervm -esa RacingSBThreads append |
| 31 | + * @run main/othervm -Xcomp RacingSBThreads |
| 32 | + */ |
| 33 | + |
| 34 | +import java.nio.CharBuffer; |
| 35 | +import java.time.Duration; |
| 36 | +import java.time.Instant; |
| 37 | +import java.util.Arrays; |
| 38 | +import java.util.Locale; |
| 39 | +import java.util.concurrent.atomic.AtomicInteger; |
| 40 | +import java.util.function.BiConsumer; |
| 41 | + |
| 42 | +/** |
| 43 | + * Test racing accesses in StringBuilder. |
| 44 | + * Test source code should be compatible with JDK 8 to allow testing on older versions. |
| 45 | + */ |
| 46 | +public class RacingSBThreads { |
| 47 | + |
| 48 | + private static final int TIMEOUT_SEC = 1; // Duration to run each test case |
| 49 | + private static final int N = 10_000_000; // static number of iterations for writes and modifies |
| 50 | + private static final int LEN = 100_000; // Length of initial SB |
| 51 | + |
| 52 | + // Strings available to be used as the initial contents of a StringBuilder |
| 53 | + private static final String UTF16_CHARS = initString('\u1000', LEN); |
| 54 | + private static final String LATIN1_CHARS = initString('a', LEN); |
| 55 | + |
| 56 | + // Cache jtreg timeout factor to allow test to be run as a standalone main() |
| 57 | + private static final double TIMEOUT_FACTOR = Double.parseDouble(System.getProperty("test.timeout.factor", "1.0")); |
| 58 | + |
| 59 | + // Constant arguments available to be passed to StringBuilder operations |
| 60 | + private static final StringBuilder otherSB = new StringBuilder("ab\uFF21\uFF22"); |
| 61 | + private static final StringBuilder otherLongerSB = new StringBuilder("abcde\uFF21\uFF22\uFF23\uFF24\uFF25"); |
| 62 | + |
| 63 | + // Create a String with a repeated character |
| 64 | + private static String initString(char c, int len) { |
| 65 | + char[] chars = new char[len]; |
| 66 | + Arrays.fill(chars, c); |
| 67 | + return new String(chars); |
| 68 | + } |
| 69 | + |
| 70 | + // Plain unsynchronized reference to a StringBuilder |
| 71 | + // Updated by the writer thread |
| 72 | + // Read by the reader thread |
| 73 | + private StringBuilder buf; |
| 74 | + |
| 75 | + // The current stress test case |
| 76 | + private final StressKind stressKind; |
| 77 | + |
| 78 | + // Count of faults, zero if no faults found |
| 79 | + private final AtomicInteger faultCount = new AtomicInteger(0); |
| 80 | + |
| 81 | + /** |
| 82 | + * Run the stress cases indicated by command line arguments or run all cases. |
| 83 | + * Running each for TIMEOUT_SEC seconds or until a failure. |
| 84 | + * The timeout/test duration can be scaled by setting System property |
| 85 | + * `test.timeout.factor` to a double value, for example, `-Dtest.timeout.factor=2.0` |
| 86 | + * @param args command line arguments |
| 87 | + */ |
| 88 | + public static void main(String[] args) { |
| 89 | + Duration duration = Duration.ofSeconds((long)(TIMEOUT_SEC * TIMEOUT_FACTOR)); |
| 90 | + |
| 91 | + StressKind[] kinds = StressKind.values(); |
| 92 | + if (args.length > 0) { |
| 93 | + // Parse explicitly supplied StressKind arguments |
| 94 | + try { |
| 95 | + kinds = Arrays.stream(args) |
| 96 | + .map((s) -> StressKind.valueOf(s.toUpperCase(Locale.ROOT))) |
| 97 | + .toArray(StressKind[]::new); |
| 98 | + } catch (Exception ex) { |
| 99 | + System.out.println("Invalid StressKind arguments: " + Arrays.toString(args)); |
| 100 | + return; |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + // Run each kind for the duration |
| 105 | + int totalFaults = 0; |
| 106 | + for (StressKind sk : kinds) { |
| 107 | + Instant end = Instant.now().plus(duration); // note clock time, not runtime |
| 108 | + while (Instant.now().isBefore(end)) { |
| 109 | + int faultCount = new RacingSBThreads(sk).stress(); |
| 110 | + if (faultCount > 0) { |
| 111 | + System.out.printf("ERROR: Test case %s, %d faults%n", sk, faultCount); |
| 112 | + } |
| 113 | + totalFaults += faultCount; |
| 114 | + } |
| 115 | + } |
| 116 | + if (totalFaults > 0) { |
| 117 | + throw new AssertionError("Total faults: " + totalFaults); |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + // Enum of the various test cases with a lambda to invoke for each |
| 122 | + enum StressKind { |
| 123 | + /** |
| 124 | + * Reading characters should always be one of the known values being written to the destination |
| 125 | + */ |
| 126 | + READ(LATIN1_CHARS, (sb, chr) -> { |
| 127 | + char ch = sb.charAt(LEN * 4 / 5); |
| 128 | + if (ch != chr & ch != (chr & 0xff) & ch != chr >> 8) { |
| 129 | + throw new AssertionError("Unexpected characters in buffer: 0x" + Integer.toHexString(ch)); |
| 130 | + } |
| 131 | + }), |
| 132 | + /** |
| 133 | + * Insert another StringBuilder; in the face of racy changes to the destination |
| 134 | + */ |
| 135 | + INSERT(LATIN1_CHARS, (sb, C) -> { |
| 136 | + sb.insert(sb.length() - 1, otherLongerSB, 0, otherLongerSB.length()); |
| 137 | + }), |
| 138 | + /** |
| 139 | + * Appending a StringBuilder in the face of racy changes to the destination |
| 140 | + */ |
| 141 | + APPEND(LATIN1_CHARS, (sb, C) -> { |
| 142 | + sb.append(otherSB, 0, otherSB.length()); |
| 143 | + }), |
| 144 | + ; |
| 145 | + |
| 146 | + private final BiConsumer<StringBuilder,Character> func; |
| 147 | + private final String sbInitString; |
| 148 | + |
| 149 | + /** |
| 150 | + * Defines a test case. |
| 151 | + * @param sbInitString the initial contents of the StringBuilder; chooses the coder |
| 152 | + * @param func the test BiConsumer to apply to the StringBuilder |
| 153 | + */ |
| 154 | + private StressKind(String sbInitString, BiConsumer<StringBuilder,Character> func) { |
| 155 | + this.func = func; |
| 156 | + this.sbInitString = sbInitString; |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + public RacingSBThreads(StressKind stressKind) { |
| 161 | + this.stressKind = stressKind; |
| 162 | + } |
| 163 | + |
| 164 | + /** |
| 165 | + * Run the stress case. |
| 166 | + * One thread continuously creates a StringBuilder and fills it before trimming it to zero. |
| 167 | + * The other thread performs the test case on the same StringBuilder (without any synchronization) |
| 168 | + * @return the count of faults |
| 169 | + */ |
| 170 | + private int stress() { |
| 171 | + PokeBuilder r = new PokeBuilder(this, N); |
| 172 | + Writer w = new Writer(this, N); |
| 173 | + |
| 174 | + Thread writer = new Thread(w::createShrink); |
| 175 | + Thread reader = new Thread(r::readModify); |
| 176 | + writer.start(); |
| 177 | + reader.start(); |
| 178 | + join(reader); |
| 179 | + System.out.println(r); |
| 180 | + writer.interrupt(); |
| 181 | + join(writer); |
| 182 | + System.out.println(w); |
| 183 | + return r.racing.faultCount.get(); |
| 184 | + } |
| 185 | + |
| 186 | + /** |
| 187 | + * Wait for a thread to terminate. |
| 188 | + * @param thread a thread to wait for |
| 189 | + */ |
| 190 | + private void join(Thread thread) { |
| 191 | + do { |
| 192 | + try { |
| 193 | + thread.join(); |
| 194 | + break; |
| 195 | + } catch (InterruptedException ie) { |
| 196 | + // ignore and retry |
| 197 | + } |
| 198 | + } while (true); |
| 199 | + } |
| 200 | + |
| 201 | + /** |
| 202 | + * Run a StressKind case in a loop keeping track of exceptions. |
| 203 | + * The StringBuilder under test is shared with the writer task without benefit of synchronization. |
| 204 | + */ |
| 205 | + private static class PokeBuilder { |
| 206 | + private final RacingSBThreads racing; |
| 207 | + private final int iterations; |
| 208 | + private int nulls; |
| 209 | + private int bounds; |
| 210 | + private int pokeCycles; |
| 211 | + private int bufChanges; |
| 212 | + |
| 213 | + public PokeBuilder(RacingSBThreads racing, int iterations) { |
| 214 | + this.racing = racing; |
| 215 | + this.iterations = iterations; |
| 216 | + nulls = 0; |
| 217 | + bounds = 0; |
| 218 | + pokeCycles = 0; |
| 219 | + bufChanges = 0; |
| 220 | + } |
| 221 | + |
| 222 | + // Repeatedly change the racy StringBuilder, ignoring and counting exceptions |
| 223 | + private void readModify() { |
| 224 | + System.out.println("Starting " + racing.stressKind); |
| 225 | + sleep(100); |
| 226 | + for (int i = 0; i < iterations; ++i) { |
| 227 | + pokeCycles++; |
| 228 | + StringBuilder sb = racing.buf; // read once |
| 229 | + try { |
| 230 | + if (sb.length() > Integer.MAX_VALUE / 4) { |
| 231 | + sb.setLength(Integer.MAX_VALUE / 4); |
| 232 | + } |
| 233 | + // Invoke the test case |
| 234 | + racing.stressKind.func.accept(sb, racing.stressKind.sbInitString.charAt(0)); |
| 235 | + if (sb != racing.buf) { |
| 236 | + bufChanges++; |
| 237 | + } |
| 238 | + } catch (NullPointerException e) { |
| 239 | + ++nulls; |
| 240 | + } catch (IndexOutOfBoundsException e) { |
| 241 | + ++bounds; |
| 242 | + } catch (AssertionError ae) { |
| 243 | + racing.faultCount.incrementAndGet(); |
| 244 | + throw ae; |
| 245 | + } |
| 246 | + } |
| 247 | + } |
| 248 | + |
| 249 | + private static void sleep(int i) { |
| 250 | + try { |
| 251 | + Thread.sleep(i); |
| 252 | + } catch (InterruptedException ignored) { |
| 253 | + } |
| 254 | + } |
| 255 | + |
| 256 | + public String toString() { |
| 257 | + return String.format("pokeCycles:%d, bounds:%d, bufChanges:%d, nulls=%d", |
| 258 | + pokeCycles, bounds, bufChanges, nulls); |
| 259 | + } |
| 260 | + } |
| 261 | + |
| 262 | + /** |
| 263 | + * Repeatedly create and append strings to a StringBuilder shared through fields of RacingSBThreads. |
| 264 | + * The StringBuilder is created new on each iteration and truncated at the end of each iteration. |
| 265 | + * Exceptions are counted and reported. |
| 266 | + */ |
| 267 | + private static class Writer { |
| 268 | + private final RacingSBThreads racing; |
| 269 | + private final int iterations; |
| 270 | + private int sumWriter; |
| 271 | + private int writeCycles; |
| 272 | + private int putBounds; |
| 273 | + |
| 274 | + public Writer(RacingSBThreads racing, int iterations) { |
| 275 | + this.racing = racing; |
| 276 | + this.iterations = iterations; |
| 277 | + } |
| 278 | + |
| 279 | + private void createShrink() { |
| 280 | + for (int i = 0; i < iterations; ++i) { |
| 281 | + if (i % 100_000 == 0) { |
| 282 | + if (Thread.interrupted()) { |
| 283 | + break; |
| 284 | + } |
| 285 | + } |
| 286 | + try { |
| 287 | + ++writeCycles; |
| 288 | + racing.buf = new StringBuilder(racing.stressKind.sbInitString); |
| 289 | + racing.buf.append(UTF16_CHARS); |
| 290 | + sumWriter += racing.buf.length(); |
| 291 | + racing.buf.setLength(0); |
| 292 | + racing.buf.trimToSize(); |
| 293 | + } catch (Exception ex) { |
| 294 | + ++putBounds; |
| 295 | + } |
| 296 | + } |
| 297 | + } |
| 298 | + |
| 299 | + public String toString() { |
| 300 | + return String.format("writeCycles:%d, bounds:%d, sumWriter=%d", writeCycles, putBounds, sumWriter); |
| 301 | + } |
| 302 | + } |
| 303 | +} |
0 commit comments