Skip to content

Commit 0bf22e0

Browse files
committed
Adds a reentrancy guard to the thread-local pool
1 parent a556dfc commit 0bf22e0

File tree

3 files changed

+205
-103
lines changed

3 files changed

+205
-103
lines changed

src/main/java/org/apache/commons/io/CopyUtils.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,14 +278,18 @@ public static int copy(
278278
final Reader input,
279279
final Writer output)
280280
throws IOException {
281-
final char[] buffer = IOUtils.getScratchCharArray();
282-
int count = 0;
283-
int n;
284-
while (EOF != (n = input.read(buffer))) {
285-
output.write(buffer, 0, n);
286-
count += n;
281+
final char[] buffer = IOUtils.ScratchBufferHolder.getScratchCharArray();
282+
try {
283+
int count = 0;
284+
int n;
285+
while (EOF != (n = input.read(buffer))) {
286+
output.write(buffer, 0, n);
287+
count += n;
288+
}
289+
return count;
290+
} finally {
291+
IOUtils.ScratchBufferHolder.releaseScratchCharArray(buffer);
287292
}
288-
return count;
289293
}
290294

291295
/**

src/main/java/org/apache/commons/io/IOUtils.java

Lines changed: 153 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,94 @@ public class IOUtils {
132132
// Writer. Each method should take at least one of these as a parameter,
133133
// or return one of them.
134134

135+
/**
136+
* Holder for per-thread internal scratch buffers.
137+
*
138+
* <p>Buffers are created lazily and reused within the same thread to reduce allocation overhead. In the rare case of reentrant access, a temporary buffer
139+
* is allocated to avoid data corruption.</p>
140+
*
141+
* <p>Typical usage:</p>
142+
*
143+
* <pre>{@code
144+
* final byte[] buffer = ScratchBufferHolder.getScratchByteArray();
145+
* try {
146+
* // use the buffer
147+
* } finally {
148+
* ScratchBufferHolder.releaseScratchByteArray(buffer);
149+
* }
150+
* }</pre>
151+
*/
152+
static final class ScratchBufferHolder {
153+
154+
/**
155+
* Holder for internal byte array buffer.
156+
*/
157+
private static final ThreadLocal<Object[]> SCRATCH_BYTE_BUFFER_HOLDER = ThreadLocal.withInitial(() -> new Object[] { false, byteArray() });
158+
159+
/**
160+
* Holder for internal char array buffer.
161+
*/
162+
private static final ThreadLocal<Object[]> SCRATCH_CHAR_BUFFER_HOLDER = ThreadLocal.withInitial(() -> new Object[] { false, charArray() });
163+
164+
165+
/**
166+
* Gets the internal byte array buffer.
167+
*
168+
* @return the internal byte array buffer.
169+
*/
170+
static byte[] getScratchByteArray() {
171+
final Object[] holder = SCRATCH_BYTE_BUFFER_HOLDER.get();
172+
// If already in use, return a new array
173+
if ((boolean) holder[0]) {
174+
return byteArray();
175+
}
176+
holder[0] = true;
177+
return (byte[]) holder[1];
178+
}
179+
180+
/**
181+
* Gets the char byte array buffer.
182+
*
183+
* @return the char byte array buffer.
184+
*/
185+
static char[] getScratchCharArray() {
186+
final Object[] holder = SCRATCH_CHAR_BUFFER_HOLDER.get();
187+
// If already in use, return a new array
188+
if ((boolean) holder[0]) {
189+
return charArray();
190+
}
191+
holder[0] = true;
192+
return (char[]) holder[1];
193+
}
194+
195+
196+
/**
197+
* If the argument is the internal byte array, release it for reuse.
198+
*
199+
* @param array the byte array to release.
200+
*/
201+
static void releaseScratchByteArray(byte[] array) {
202+
final Object[] holder = SCRATCH_BYTE_BUFFER_HOLDER.get();
203+
if (array == holder[1]) {
204+
Arrays.fill(array, (byte) 0);
205+
holder[0] = false;
206+
}
207+
}
208+
209+
/**
210+
* If the argument is the internal char array, release it for reuse.
211+
*
212+
* @param array the byte array to release.
213+
*/
214+
static void releaseScratchCharArray(char[] array) {
215+
final Object[] holder = SCRATCH_CHAR_BUFFER_HOLDER.get();
216+
if (array == holder[1]) {
217+
Arrays.fill(array, (char) 0);
218+
holder[0] = false;
219+
}
220+
}
221+
}
222+
135223
/**
136224
* CR char '{@value}'.
137225
*
@@ -201,16 +289,6 @@ public class IOUtils {
201289
*/
202290
public static final String LINE_SEPARATOR_WINDOWS = StandardLineSeparator.CRLF.getString();
203291

204-
/**
205-
* Internal byte array buffer, intended for both reading and writing.
206-
*/
207-
private static final ThreadLocal<byte[]> SCRATCH_BYTE_BUFFER = ThreadLocal.withInitial(IOUtils::byteArray);
208-
209-
/**
210-
* Internal char array buffer, intended for both reading and writing.
211-
*/
212-
private static final ThreadLocal<char[]> SCRATCH_CHAR_BUFFER = ThreadLocal.withInitial(IOUtils::charArray);
213-
214292
/**
215293
* The maximum size of an array in many Java VMs.
216294
* <p>
@@ -582,8 +660,8 @@ static void checkFromToIndex(final int fromIndex, final int toIndex, final int l
582660
* @see IO#clear()
583661
*/
584662
static void clear() {
585-
SCRATCH_BYTE_BUFFER.remove();
586-
SCRATCH_CHAR_BUFFER.remove();
663+
ScratchBufferHolder.SCRATCH_BYTE_BUFFER_HOLDER.remove();
664+
ScratchBufferHolder.SCRATCH_CHAR_BUFFER_HOLDER.remove();
587665
}
588666

589667
/**
@@ -1127,39 +1205,43 @@ public static boolean contentEquals(final Reader input1, final Reader input2) th
11271205
}
11281206

11291207
// reuse one
1130-
final char[] array1 = getScratchCharArray();
1208+
final char[] array1 = ScratchBufferHolder.getScratchCharArray();
11311209
// but allocate another
11321210
final char[] array2 = charArray();
11331211
int pos1;
11341212
int pos2;
11351213
int count1;
11361214
int count2;
1137-
while (true) {
1138-
pos1 = 0;
1139-
pos2 = 0;
1140-
for (int index = 0; index < DEFAULT_BUFFER_SIZE; index++) {
1141-
if (pos1 == index) {
1142-
do {
1143-
count1 = input1.read(array1, pos1, DEFAULT_BUFFER_SIZE - pos1);
1144-
} while (count1 == 0);
1145-
if (count1 == EOF) {
1146-
return pos2 == index && input2.read() == EOF;
1215+
try {
1216+
while (true) {
1217+
pos1 = 0;
1218+
pos2 = 0;
1219+
for (int index = 0; index < DEFAULT_BUFFER_SIZE; index++) {
1220+
if (pos1 == index) {
1221+
do {
1222+
count1 = input1.read(array1, pos1, DEFAULT_BUFFER_SIZE - pos1);
1223+
} while (count1 == 0);
1224+
if (count1 == EOF) {
1225+
return pos2 == index && input2.read() == EOF;
1226+
}
1227+
pos1 += count1;
11471228
}
1148-
pos1 += count1;
1149-
}
1150-
if (pos2 == index) {
1151-
do {
1152-
count2 = input2.read(array2, pos2, DEFAULT_BUFFER_SIZE - pos2);
1153-
} while (count2 == 0);
1154-
if (count2 == EOF) {
1155-
return pos1 == index && input1.read() == EOF;
1229+
if (pos2 == index) {
1230+
do {
1231+
count2 = input2.read(array2, pos2, DEFAULT_BUFFER_SIZE - pos2);
1232+
} while (count2 == 0);
1233+
if (count2 == EOF) {
1234+
return pos1 == index && input1.read() == EOF;
1235+
}
1236+
pos2 += count2;
1237+
}
1238+
if (array1[index] != array2[index]) {
1239+
return false;
11561240
}
1157-
pos2 += count2;
1158-
}
1159-
if (array1[index] != array2[index]) {
1160-
return false;
11611241
}
11621242
}
1243+
} finally {
1244+
ScratchBufferHolder.releaseScratchCharArray(array1);
11631245
}
11641246
}
11651247

@@ -1639,9 +1721,13 @@ public static long copyLarge(final InputStream inputStream, final OutputStream o
16391721
* @throws IOException if an I/O error occurs.
16401722
* @since 2.2
16411723
*/
1642-
public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset,
1643-
final long length) throws IOException {
1644-
return copyLarge(input, output, inputOffset, length, getScratchByteArray());
1724+
public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, final long length) throws IOException {
1725+
final byte[] buffer = ScratchBufferHolder.getScratchByteArray();
1726+
try {
1727+
return copyLarge(input, output, inputOffset, length, buffer);
1728+
} finally {
1729+
ScratchBufferHolder.releaseScratchByteArray(buffer);
1730+
}
16451731
}
16461732

16471733
/**
@@ -1711,7 +1797,12 @@ public static long copyLarge(final InputStream input, final OutputStream output,
17111797
* @since 1.3
17121798
*/
17131799
public static long copyLarge(final Reader reader, final Writer writer) throws IOException {
1714-
return copyLarge(reader, writer, getScratchCharArray());
1800+
final char[] buffer = ScratchBufferHolder.getScratchCharArray();
1801+
try {
1802+
return copyLarge(reader, writer, buffer);
1803+
} finally {
1804+
ScratchBufferHolder.releaseScratchCharArray(buffer);
1805+
}
17151806
}
17161807

17171808
/**
@@ -1758,7 +1849,12 @@ public static long copyLarge(final Reader reader, final Writer writer, final cha
17581849
* @since 2.2
17591850
*/
17601851
public static long copyLarge(final Reader reader, final Writer writer, final long inputOffset, final long length) throws IOException {
1761-
return copyLarge(reader, writer, inputOffset, length, getScratchCharArray());
1852+
final char[] buffer = ScratchBufferHolder.getScratchCharArray();
1853+
try {
1854+
return copyLarge(reader, writer, inputOffset, length, buffer);
1855+
} finally {
1856+
ScratchBufferHolder.releaseScratchCharArray(buffer);
1857+
}
17621858
}
17631859

17641860
/**
@@ -1825,46 +1921,6 @@ static UnsynchronizedByteArrayOutputStream copyToOutputStream(
18251921
}
18261922
}
18271923

1828-
/**
1829-
* Fills the given array with 0s.
1830-
*
1831-
* @param arr The non-null array to fill.
1832-
* @return The given array.
1833-
*/
1834-
private static byte[] fill0(final byte[] arr) {
1835-
Arrays.fill(arr, (byte) 0);
1836-
return arr;
1837-
}
1838-
1839-
/**
1840-
* Fills the given array with 0s.
1841-
*
1842-
* @param arr The non-null array to fill.
1843-
* @return The given array.
1844-
*/
1845-
private static char[] fill0(final char[] arr) {
1846-
Arrays.fill(arr, (char) 0);
1847-
return arr;
1848-
}
1849-
1850-
/**
1851-
* Gets the internal byte array buffer, intended for both reading and writing.
1852-
*
1853-
* @return the internal byte array buffer, intended for both reading and writing.
1854-
*/
1855-
static byte[] getScratchByteArray() {
1856-
return fill0(SCRATCH_BYTE_BUFFER.get());
1857-
}
1858-
1859-
/**
1860-
* Gets the char byte array buffer, intended for both reading and writing.
1861-
*
1862-
* @return the char byte array buffer, intended for both reading and writing.
1863-
*/
1864-
static char[] getScratchCharArray() {
1865-
return fill0(SCRATCH_CHAR_BUFFER.get());
1866-
}
1867-
18681924
/**
18691925
* Returns the length of the given array in a null-safe manner.
18701926
*
@@ -2497,7 +2553,12 @@ public static URL resourceToURL(final String name, final ClassLoader classLoader
24972553
* @since 2.0
24982554
*/
24992555
public static long skip(final InputStream input, final long skip) throws IOException {
2500-
return skip(input, skip, IOUtils::getScratchByteArray);
2556+
final byte[] buffer = ScratchBufferHolder.getScratchByteArray();
2557+
try {
2558+
return skip(input, skip, () -> buffer);
2559+
} finally {
2560+
ScratchBufferHolder.releaseScratchByteArray(buffer);
2561+
}
25012562
}
25022563

25032564
/**
@@ -2604,14 +2665,18 @@ public static long skip(final Reader reader, final long toSkip) throws IOExcepti
26042665
throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
26052666
}
26062667
long remain = toSkip;
2607-
while (remain > 0) {
2608-
// See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
2609-
final char[] charArray = getScratchCharArray();
2610-
final long n = reader.read(charArray, 0, (int) Math.min(remain, charArray.length));
2611-
if (n < 0) { // EOF
2612-
break;
2668+
final char[] charArray = ScratchBufferHolder.getScratchCharArray();
2669+
try {
2670+
while (remain > 0) {
2671+
// See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
2672+
final long n = reader.read(charArray, 0, (int) Math.min(remain, charArray.length));
2673+
if (n < 0) { // EOF
2674+
break;
2675+
}
2676+
remain -= n;
26132677
}
2614-
remain -= n;
2678+
} finally {
2679+
ScratchBufferHolder.releaseScratchCharArray(charArray);
26152680
}
26162681
return toSkip - remain;
26172682
}

0 commit comments

Comments
 (0)