Skip to content

Commit 4c695fa

Browse files
author
Roger Riggs
committed
8351000: StringBuilder getChar and putChar robustness
Reviewed-by: liach, lancea, rgiulietti, pminborg
1 parent 482538b commit 4c695fa

File tree

2 files changed

+312
-5
lines changed

2 files changed

+312
-5
lines changed

src/java.base/share/classes/java/lang/AbstractStringBuilder.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -29,7 +29,6 @@
2929
import jdk.internal.math.FloatToDecimal;
3030
import jdk.internal.util.DecimalDigits;
3131

32-
import java.io.IOException;
3332
import java.nio.CharBuffer;
3433
import java.util.Arrays;
3534
import java.util.Spliterator;
@@ -360,8 +359,12 @@ public void setLength(int newLength) {
360359
*/
361360
@Override
362361
public char charAt(int index) {
362+
byte coder = this.coder;
363+
byte[] value = this.value;
364+
// Ensure count is less than or equal to capacity (racy reads and writes can produce inconsistent values)
365+
int count = Math.min(this.count, value.length >> coder);
363366
checkIndex(index, count);
364-
if (isLatin1()) {
367+
if (coder == LATIN1) {
365368
return (char)(value[index] & 0xff);
366369
}
367370
return StringUTF16.getChar(value, index);
@@ -420,6 +423,7 @@ public int codePointAt(int index) {
420423
* of this sequence.
421424
*/
422425
public int codePointBefore(int index) {
426+
byte[] value = this.value;
423427
int i = index - 1;
424428
checkIndex(i, count);
425429
if (isLatin1()) {
@@ -1730,7 +1734,7 @@ private final void putCharsAt(int index, CharSequence s, int off, int end) {
17301734
} else {
17311735
inflate();
17321736
// store c to make sure it has a UTF16 char
1733-
StringUTF16.putChar(this.value, j++, c);
1737+
StringUTF16.putCharSB(this.value, j++, c);
17341738
i++;
17351739
StringUTF16.putCharsSB(this.value, j, s, i, end);
17361740
return;
@@ -1825,7 +1829,7 @@ private final void appendChars(CharSequence s, int off, int end) {
18251829
count = j;
18261830
inflate();
18271831
// Store c to make sure sb has a UTF16 char
1828-
StringUTF16.putChar(this.value, j++, c);
1832+
StringUTF16.putCharSB(this.value, j++, c);
18291833
count = j;
18301834
i++;
18311835
StringUTF16.putCharsSB(this.value, j, s, i, end);
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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

Comments
 (0)