This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-io.git
The following commit(s) were added to refs/heads/master by this push: new c12eaff7 Don't use thread local buffers for write only operations c12eaff7 is described below commit c12eaff7f747353a7a9a97df735fd3301f42e313 Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Fri May 12 10:37:49 2023 -0400 Don't use thread local buffers for write only operations - Use a shared static byte array for write only throw away data - Keep using thread locals for read-write buffers - Allow tests to clear thread locals to ensure proper re-initialization --- src/main/java/org/apache/commons/io/IO.java | 31 +++++++++ src/main/java/org/apache/commons/io/IOUtils.java | 80 +++++++++++++++++----- .../java/org/apache/commons/io/IOUtilsTest.java | 9 +++ 3 files changed, 101 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/apache/commons/io/IO.java b/src/main/java/org/apache/commons/io/IO.java new file mode 100644 index 00000000..d9db4246 --- /dev/null +++ b/src/main/java/org/apache/commons/io/IO.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +/** + * Component-wide operations on Apache Commons IO. + */ +class IO { + + /** + * Clears any state, throughout Apache Commons IO. Handy for tests. + */ + static void clear() { + IOUtils.clear(); + } + +} diff --git a/src/main/java/org/apache/commons/io/IOUtils.java b/src/main/java/org/apache/commons/io/IOUtils.java index 58fb5521..cca9a798 100644 --- a/src/main/java/org/apache/commons/io/IOUtils.java +++ b/src/main/java/org/apache/commons/io/IOUtils.java @@ -193,14 +193,24 @@ public class IOUtils { public static final String LINE_SEPARATOR_WINDOWS = StandardLineSeparator.CRLF.getString(); /** - * Internal byte array buffer. + * Internal byte array buffer, intended for both reading and writing. */ - private static final ThreadLocal<byte[]> SCRATCH_BYTE_BUFFER = ThreadLocal.withInitial(IOUtils::byteArray); + private static final ThreadLocal<byte[]> SCRATCH_BYTE_BUFFER_RW = ThreadLocal.withInitial(IOUtils::byteArray); /** - * Internal byte array buffer. + * Internal byte array buffer, intended for write only operations. */ - private static final ThreadLocal<char[]> SCRATCH_CHAR_BUFFER = ThreadLocal.withInitial(IOUtils::charArray); + private static final byte[] SCRATCH_BYTE_BUFFER_WO = byteArray(); + + /** + * Internal char array buffer, intended for both reading and writing. + */ + private static final ThreadLocal<char[]> SCRATCH_CHAR_BUFFER_RW = ThreadLocal.withInitial(IOUtils::charArray); + + /** + * Internal char array buffer, intended for write only operations. + */ + private static final char[] SCRATCH_CHAR_BUFFER_WO = charArray(); /** * Returns the given InputStream if it is already a {@link BufferedInputStream}, otherwise creates a @@ -377,6 +387,21 @@ public class IOUtils { return new char[size]; } + /** + * Clears any state. + * <ul> + * <li>Removes the current thread's value for thread-local variables.</li> + * <li>Sets static scratch arrays to 0s.</li> + * </ul> + * @see IO#clear() + */ + static void clear() { + SCRATCH_BYTE_BUFFER_RW.remove(); + SCRATCH_CHAR_BUFFER_RW.remove(); + Arrays.fill(SCRATCH_BYTE_BUFFER_WO, (byte) 0); + Arrays.fill(SCRATCH_CHAR_BUFFER_WO, (char) 0); + } + /** * Closes the given {@link Closeable} as a null-safe operation. * @@ -1660,21 +1685,39 @@ public class IOUtils { } /** - * Gets the thread local byte array. + * Gets the internal byte array buffer, intended for both reading and writing. + * + * @return the internal byte array buffer, intended for both reading and writing. + */ + private static byte[] getScratchByteArray() { + return SCRATCH_BYTE_BUFFER_RW.get(); + } + + /** + * Gets the internal byte array intended for write only operations. * - * @return the thread local byte array. + * @return the internal byte array intended for write only operations. */ - static byte[] getScratchByteArray() { - return SCRATCH_BYTE_BUFFER.get(); + private static byte[] getScratchByteArrayWriteOnly() { + return SCRATCH_BYTE_BUFFER_WO; } /** - * Gets the thread local char array. + * Gets the char byte array buffer, intended for both reading and writing. * - * @return the thread local char array. + * @return the char byte array buffer, intended for both reading and writing. */ static char[] getScratchCharArray() { - return SCRATCH_CHAR_BUFFER.get(); + return SCRATCH_CHAR_BUFFER_RW.get(); + } + + /** + * Gets the internal char array intended for write only operations. + * + * @return the internal char array intended for write only operations. + */ + private static char[] getScratchCharArrayWriteOnly() { + return SCRATCH_CHAR_BUFFER_WO; } /** @@ -2291,16 +2334,15 @@ public class IOUtils { if (toSkip < 0) { throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); } - /* - * N.B. no need to synchronize access to SKIP_BYTE_BUFFER: - we don't care if the buffer is created multiple - * times (the data is ignored) - we always use the same size buffer, so if it is recreated it will still be - * OK (if the buffer size were variable, we would need to synch. to ensure some other thread did not create a - * smaller one) - */ + // + // No need to synchronize access to SCRATCH_BYTE_BUFFER_WO: We don't care if the buffer is written multiple + // times or in parallel since the data is ignored. We reuse the same buffer, if the buffer size were variable or read-write, + // we would need to synch or use a thread local to ensure some other thread safety. + // long remain = toSkip; while (remain > 0) { // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() - final byte[] byteArray = getScratchByteArray(); + final byte[] byteArray = getScratchByteArrayWriteOnly(); final long n = input.read(byteArray, 0, (int) Math.min(remain, byteArray.length)); if (n < 0) { // EOF break; @@ -2368,7 +2410,7 @@ public class IOUtils { long remain = toSkip; while (remain > 0) { // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() - final char[] charArray = getScratchCharArray(); + final char[] charArray = getScratchCharArrayWriteOnly(); final long n = reader.read(charArray, 0, (int) Math.min(remain, charArray.length)); if (n < 0) { // EOF break; diff --git a/src/test/java/org/apache/commons/io/IOUtilsTest.java b/src/test/java/org/apache/commons/io/IOUtilsTest.java index dd988b2e..395b8e8f 100644 --- a/src/test/java/org/apache/commons/io/IOUtilsTest.java +++ b/src/test/java/org/apache/commons/io/IOUtilsTest.java @@ -77,6 +77,8 @@ import org.apache.commons.io.output.StringBuilderWriter; import org.apache.commons.io.test.TestUtils; import org.apache.commons.io.test.ThrowOnCloseReader; import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -127,6 +129,13 @@ public class IOUtilsTest { assertArrayEquals(b0, b1, "Content not equal according to java.util.Arrays#equals()"); } + @BeforeAll + @AfterAll + public static void beforeAll() { + // Not required, just to exercise the method and make sure there are no adverse side-effect when recycling thread locals. + IO.clear(); + } + @BeforeEach public void setUp() { try {