Skip to content

Commit ea7ccc5

Browse files
author
Gary Gregory
committed
[IO-718] FileUtils.checksumCRC32 and FileUtils.checksum are not thread
safe.
1 parent 65868e5 commit ea7ccc5

3 files changed

Lines changed: 69 additions & 35 deletions

File tree

src/changes/changes.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ The <action> type attribute can be add,update,fix,remove.
117117
<action dev="ggregory" type="fix" due-to="Felix Rilling">
118118
Fix Typos in JavaDoc, Comments and Tests #201.
119119
</action>
120+
<action issue="IO-718" dev="ggregory" type="fix" due-to="Robert Cooper, Gary Gregory">
121+
FileUtils.checksumCRC32 and FileUtils.checksum are not thread safe.
122+
</action>
120123
<!-- ADD -->
121124
<action dev="ggregory" type="add" due-to="Gary Gregory">
122125
Add FileSystemProviders class.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public static int copy(
192192
final Reader input,
193193
final Writer output)
194194
throws IOException {
195-
final char[] buffer = new char[IOUtils.DEFAULT_BUFFER_SIZE];
195+
final char[] buffer = IOUtils.getCharArray();
196196
int count = 0;
197197
int n = 0;
198198
while (EOF != (n = input.read(buffer))) {

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

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -176,23 +176,15 @@ public class IOUtils {
176176
public static final String LINE_SEPARATOR_WINDOWS = StandardLineSeparator.CRLF.getString();
177177

178178
/**
179-
* The default buffer to use for the skip() methods.
179+
* Internal byte array buffer.
180180
*/
181-
private static final byte[] SKIP_BYTE_BUFFER = byteArray();
182-
183-
// Allocated in the relevant skip method if necessary.
184-
/*
185-
* These buffers are static and are shared between threads.
186-
* This is possible because the buffers are write-only - the contents are never read.
187-
*
188-
* N.B. there is no need to synchronize when creating these because:
189-
* - we don't care if the buffer is created multiple times (the data is ignored)
190-
* - we always use the same size buffer, so if it it is recreated it will still be OK
191-
* (if the buffer size were variable, we would need to synch. to ensure some other thread
192-
* did not create a smaller one)
181+
private static final ThreadLocal<byte[]> SKIP_BYTE_BUFFER = ThreadLocal.withInitial(() -> byteArray());
182+
183+
/**
184+
* Internal byte array buffer.
193185
*/
194-
private static char[] SKIP_CHAR_BUFFER;
195-
186+
private static final ThreadLocal<char[]> SKIP_CHAR_BUFFER = ThreadLocal.withInitial(() -> charArray());
187+
196188
/**
197189
* Returns the given InputStream if it is already a {@link BufferedInputStream}, otherwise creates a
198190
* BufferedInputStream from the given InputStream.
@@ -344,6 +336,29 @@ public static byte[] byteArray(final int size) {
344336
return new byte[size];
345337
}
346338

339+
/**
340+
* Returns a new char array of size {@link #DEFAULT_BUFFER_SIZE}.
341+
*
342+
* @return a new char array of size {@link #DEFAULT_BUFFER_SIZE}.
343+
* @since 2.9.0
344+
*/
345+
private static char[] charArray() {
346+
return charArray(DEFAULT_BUFFER_SIZE);
347+
}
348+
349+
/**
350+
* Returns a new char array of the given size.
351+
*
352+
* TODO Consider guarding or warning against large allocations...
353+
*
354+
* @param size array size.
355+
* @return a new char array of the given size.
356+
* @since 2.9.0
357+
*/
358+
private static char[] charArray(final int size) {
359+
return new char[size];
360+
}
361+
347362
/**
348363
* Closes the given {@link Closeable} as a null-safe operation.
349364
*
@@ -755,7 +770,7 @@ public static void closeQuietly(final Writer writer) {
755770
*/
756771
public static long consume(final InputStream input)
757772
throws IOException {
758-
return copyLarge(input, NullOutputStream.NULL_OUTPUT_STREAM, SKIP_BYTE_BUFFER);
773+
return copyLarge(input, NullOutputStream.NULL_OUTPUT_STREAM, getByteArray());
759774
}
760775

761776
/**
@@ -783,7 +798,9 @@ public static boolean contentEquals(final InputStream input1, final InputStream
783798
return false;
784799
}
785800

786-
final byte[] array1 = byteArray();
801+
// reuse one
802+
final byte[] array1 = getByteArray();
803+
// allocate another
787804
final byte[] array2 = byteArray();
788805
int pos1;
789806
int pos2;
@@ -839,8 +856,10 @@ public static boolean contentEquals(final Reader input1, final Reader input2) th
839856
return false;
840857
}
841858

842-
final char[] array1 = new char[DEFAULT_BUFFER_SIZE];
843-
final char[] array2 = new char[DEFAULT_BUFFER_SIZE];
859+
// reuse one
860+
final char[] array1 = getCharArray();
861+
// but allocate another
862+
final char[] array2 = charArray();
844863
int pos1;
845864
int pos2;
846865
int count1;
@@ -1318,7 +1337,7 @@ public static long copyLarge(final InputStream inputStream, final OutputStream o
13181337
*/
13191338
public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset,
13201339
final long length) throws IOException {
1321-
return copyLarge(input, output, inputOffset, length, byteArray());
1340+
return copyLarge(input, output, inputOffset, length, getByteArray());
13221341
}
13231342

13241343
/**
@@ -1387,7 +1406,7 @@ public static long copyLarge(final InputStream input, final OutputStream output,
13871406
* @since 1.3
13881407
*/
13891408
public static long copyLarge(final Reader reader, final Writer writer) throws IOException {
1390-
return copyLarge(reader, writer, new char[DEFAULT_BUFFER_SIZE]);
1409+
return copyLarge(reader, writer, getCharArray());
13911410
}
13921411

13931412
/**
@@ -1436,7 +1455,7 @@ public static long copyLarge(final Reader reader, final Writer writer, final cha
14361455
*/
14371456
public static long copyLarge(final Reader reader, final Writer writer, final long inputOffset, final long length)
14381457
throws IOException {
1439-
return copyLarge(reader, writer, inputOffset, length, new char[DEFAULT_BUFFER_SIZE]);
1458+
return copyLarge(reader, writer, inputOffset, length, getCharArray());
14401459
}
14411460

14421461
/**
@@ -1484,6 +1503,24 @@ public static long copyLarge(final Reader reader, final Writer writer, final lon
14841503
return totalRead;
14851504
}
14861505

1506+
/**
1507+
* Gets the thread local byte array.
1508+
*
1509+
* @return the thread local byte array.
1510+
*/
1511+
static byte[] getByteArray() {
1512+
return SKIP_BYTE_BUFFER.get();
1513+
}
1514+
1515+
/**
1516+
* Gets the thread local char array.
1517+
*
1518+
* @return the thread local char array.
1519+
*/
1520+
static char[] getCharArray() {
1521+
return SKIP_CHAR_BUFFER.get();
1522+
}
1523+
14871524
/**
14881525
* Returns the length of the given array in a null-safe manner.
14891526
*
@@ -2113,7 +2150,8 @@ public static long skip(final InputStream input, final long toSkip) throws IOExc
21132150
long remain = toSkip;
21142151
while (remain > 0) {
21152152
// See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
2116-
final long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length));
2153+
final byte[] byteArray = getByteArray();
2154+
final long n = input.read(byteArray, 0, (int) Math.min(remain, byteArray.length));
21172155
if (n < 0) { // EOF
21182156
break;
21192157
}
@@ -2138,11 +2176,11 @@ public static long skip(final ReadableByteChannel input, final long toSkip) thro
21382176
if (toSkip < 0) {
21392177
throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
21402178
}
2141-
final ByteBuffer skipByteBuffer = ByteBuffer.allocate((int) Math.min(toSkip, SKIP_BYTE_BUFFER.length));
2179+
final ByteBuffer skipByteBuffer = ByteBuffer.allocate((int) Math.min(toSkip, DEFAULT_BUFFER_SIZE));
21422180
long remain = toSkip;
21432181
while (remain > 0) {
21442182
skipByteBuffer.position(0);
2145-
skipByteBuffer.limit((int) Math.min(remain, SKIP_BYTE_BUFFER.length));
2183+
skipByteBuffer.limit((int) Math.min(remain, DEFAULT_BUFFER_SIZE));
21462184
final int n = input.read(skipByteBuffer);
21472185
if (n == EOF) {
21482186
break;
@@ -2177,18 +2215,11 @@ public static long skip(final Reader reader, final long toSkip) throws IOExcepti
21772215
if (toSkip < 0) {
21782216
throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip);
21792217
}
2180-
/*
2181-
* N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data
2182-
* is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer
2183-
* size were variable, we would need to synch. to ensure some other thread did not create a smaller one)
2184-
*/
2185-
if (SKIP_CHAR_BUFFER == null) {
2186-
SKIP_CHAR_BUFFER = new char[SKIP_BYTE_BUFFER.length];
2187-
}
21882218
long remain = toSkip;
21892219
while (remain > 0) {
21902220
// See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()
2191-
final long n = reader.read(SKIP_CHAR_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length));
2221+
final char[] charArray = getCharArray();
2222+
final long n = reader.read(charArray, 0, (int) Math.min(remain, charArray.length));
21922223
if (n < 0) { // EOF
21932224
break;
21942225
}

0 commit comments

Comments
 (0)