This is an automated email from the ASF dual-hosted git repository.

pkarwasz pushed a commit to branch feature/2.x/enable-thread-local
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit a86abbd6c499e595a938a3b5a4a56555d9bba57e
Author: Piotr P. Karwasz <piotr.git...@karwasz.org>
AuthorDate: Mon Apr 29 11:52:14 2024 +0200

    Enforce `log4j2.enableThreadlocals` system property
    
    If the `log4j2.enableThreadlocals` property is set to `false` Log4j
    should clear all `ThreadLocal`s from the calling thread before execution
    finishes.
    
    A similar rule is also applied to `ThreadContext`: assuming the user takes
    care of leaving an empty map and stack at the end of the execution, all
    the `ThreadLocal`s should be cleared.
---
 log4j-api-test/pom.xml                             | 37 +++++++++
 .../apache/logging/log4j/test/ThreadLocalUtil.java | 57 ++++++++++++++
 .../apache/logging/log4j/test/package-info.java    |  2 +-
 .../map/StringArrayThreadContextMapTest.java       | 29 ++++++-
 .../log4j/spi/DefaultThreadContextStackTest.java   | 27 +++++++
 .../logging/log4j/spi/ThreadContextMapTest.java    | 36 +++++++++
 .../internal/map/StringArrayThreadContextMap.java  | 11 ++-
 .../apache/logging/log4j/spi/AbstractLogger.java   | 29 ++++---
 .../CopyOnWriteSortedArrayThreadContextMap.java    | 16 +++-
 .../logging/log4j/spi/DefaultThreadContextMap.java | 12 ++-
 .../log4j/spi/DefaultThreadContextStack.java       | 26 +++++--
 .../org/apache/logging/log4j/util/Strings.java     |  8 +-
 .../java/org/apache/logging/log4j/util/Timer.java  | 19 ++---
 .../log4j/core/async/AsyncLoggerConfig.java        | 36 ++++-----
 .../logging/log4j/core/config/AppenderControl.java |  2 +-
 .../log4j/core/filter/StructuredDataFilter.java    | 20 +++--
 .../org/apache/logging/log4j/jul/LogManager.java   |  5 ++
 log4j-slf4j2-impl/pom.xml                          | 37 +++++++++
 .../org/apache/logging/slf4j/Log4jMDCAdapter.java  | 90 +++++++++++++++++-----
 .../apache/logging/slf4j/Log4jMDCAdapterTest.java  | 20 ++++-
 20 files changed, 425 insertions(+), 94 deletions(-)

diff --git a/log4j-api-test/pom.xml b/log4j-api-test/pom.xml
index 40d4dc2525..1673c66928 100644
--- a/log4j-api-test/pom.xml
+++ b/log4j-api-test/pom.xml
@@ -175,4 +175,41 @@
 
     </plugins>
   </build>
+
+  <profiles>
+
+    <!-- Fixes incompatible with Java 8 -->
+    <profile>
+
+      <id>java8-incompat-fixes</id>
+
+      <!-- CI uses Java 8 for running tests.
+           Hence, we assume CI=Java8 and apply our changes elsewhere.
+
+           One might think why not activate using `<jdk>[16,)` instead?
+           This doesn't work, since the match is not against "the JDK running 
tests", but "the JDK running Maven".
+           These two JDKs can differ due to Maven Toolchains.
+           See `java8-tests` profile in `/pom.xml` for details. -->
+      <activation>
+        <property>
+          <name>!env.CI</name>
+        </property>
+      </activation>
+
+      <!-- Illegal access is disabled by default in Java 16 due to JEP-396.
+           We are relaxing it for tests. -->
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <configuration>
+              <argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+
+    </profile>
+  </profiles>
 </project>
diff --git 
a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadLocalUtil.java
 
b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadLocalUtil.java
new file mode 100644
index 0000000000..a316ed379f
--- /dev/null
+++ 
b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadLocalUtil.java
@@ -0,0 +1,57 @@
+/*
+ * 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.logging.log4j.test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * Utility class to test {@link ThreadLocal} fields.
+ */
+public final class ThreadLocalUtil {
+
+    private ThreadLocalUtil() {}
+
+    /**
+     * Returns the number of {@link ThreadLocal} objects that have a value for 
the current thread.
+     * <p>
+     *     <strong>WARNING:</strong> The {@link ThreadLocal#get()} method is 
<strong>not</strong> side-effect free.
+     *     If the thread local is not initialized, it initializes it with a 
value of {@code null}.
+     * </p>
+     * @return The number of {@code ThreadLocal} objects with a value in the 
current thread.
+     */
+    public static int getThreadLocalCount() {
+        return assertDoesNotThrow(() -> {
+            final Field threadLocalsField = 
Thread.class.getDeclaredField("threadLocals");
+            threadLocalsField.setAccessible(true);
+            final Object threadLocalMap = 
threadLocalsField.get(Thread.currentThread());
+            final Method expungeStaleEntries = 
threadLocalMap.getClass().getDeclaredMethod("expungeStaleEntries");
+            expungeStaleEntries.setAccessible(true);
+            expungeStaleEntries.invoke(threadLocalMap);
+            final Field sizeField = 
threadLocalMap.getClass().getDeclaredField("size");
+            sizeField.setAccessible(true);
+            return (int) sizeField.get(threadLocalMap);
+        });
+    }
+
+    public static void assertThreadLocalCount(final int expexted) {
+        assertThat(getThreadLocalCount()).as("Count ThreadLocals with 
value").isEqualTo(expexted);
+    }
+}
diff --git 
a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/package-info.java 
b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/package-info.java
index 5a0ebc4510..9f17256730 100644
--- 
a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/package-info.java
+++ 
b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/package-info.java
@@ -15,7 +15,7 @@
  * limitations under the license.
  */
 @Export
-@Version("2.21.1")
+@Version("2.24.0")
 package org.apache.logging.log4j.test;
 
 import org.osgi.annotation.bundle.Export;
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java
index 2f845d8bf7..3e6a94aa5b 100644
--- 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java
@@ -16,16 +16,19 @@
  */
 package org.apache.logging.log4j.internal.map;
 
+import static 
org.apache.logging.log4j.test.ThreadLocalUtil.assertThreadLocalCount;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import org.apache.logging.log4j.test.ThreadLocalUtil;
 import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
 import org.apache.logging.log4j.util.TriConsumer;
 import org.junit.jupiter.api.Test;
@@ -34,7 +37,9 @@ import org.junit.jupiter.api.Test;
  * Tests the {@code StringArrayThreadContextMap} class.
  */
 @UsingThreadContextMap
-public class StringArrayThreadContextMapTest {
+class StringArrayThreadContextMapTest {
+
+    private static final String KEY = "StringArrayThreadContextMapTest";
 
     @Test
     public void testEqualsVsSameKind() {
@@ -284,4 +289,26 @@ public class StringArrayThreadContextMapTest {
         map.forEach(triConsumer, iterationResultMap);
         assertTrue(iterationResultMap.isEmpty());
     }
+
+    @Test
+    void threadLocalsRemovedWhenMapEmpty() {
+        final StringArrayThreadContextMap contextMap = new 
StringArrayThreadContextMap();
+        final int threadLocalCount = ThreadLocalUtil.getThreadLocalCount();
+
+        contextMap.put(KEY, "threadLocalsRemovedWhenMapEmpty");
+        assertThreadLocalCount(threadLocalCount + 1);
+        contextMap.remove(KEY);
+        assertThreadLocalCount(threadLocalCount);
+
+        contextMap.put("key1", "value1");
+        contextMap.put("key2", "value2");
+        assertThreadLocalCount(threadLocalCount + 1);
+        contextMap.removeAll(Arrays.asList("key1", "key2"));
+        assertThreadLocalCount(threadLocalCount);
+
+        contextMap.put(KEY, "threadLocalsRemovedWhenMapEmpty");
+        assertThreadLocalCount(threadLocalCount + 1);
+        contextMap.clear();
+        assertThreadLocalCount(threadLocalCount);
+    }
 }
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextStackTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextStackTest.java
index 4778ec63c1..e94ed9914e 100644
--- 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextStackTest.java
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextStackTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.logging.log4j.spi;
 
+import static 
org.apache.logging.log4j.test.ThreadLocalUtil.assertThreadLocalCount;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -28,7 +29,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.List;
 import org.apache.logging.log4j.ThreadContext.ContextStack;
+import org.apache.logging.log4j.test.ThreadLocalUtil;
 import org.apache.logging.log4j.test.junit.UsingAnyThreadContext;
 import org.junit.jupiter.api.Test;
 
@@ -339,4 +342,28 @@ public class DefaultThreadContextStackTest {
         stack.retainAll(Arrays.asList("msg1", "msg3"));
         assertEquals("[msg1, msg3]", stack.toString());
     }
+
+    @Test
+    void threadLocalUnsetWhenStackEmpty() throws Exception {
+        final DefaultThreadContextStack stack = new 
DefaultThreadContextStack(true);
+        final int threadLocalCount = ThreadLocalUtil.getThreadLocalCount();
+        // push and pop
+        stack.push("push and pop");
+        assertThreadLocalCount(threadLocalCount + 1);
+        stack.pop();
+        assertThreadLocalCount(threadLocalCount);
+
+        // push and remove
+        stack.push("push and remove");
+        assertThreadLocalCount(threadLocalCount + 1);
+        stack.remove("push and remove");
+        assertThreadLocalCount(threadLocalCount);
+
+        // addAll and removeAll
+        final List<String> data = Arrays.asList("addAll", "removeAll");
+        stack.addAll(data);
+        assertThreadLocalCount(threadLocalCount + 1);
+        stack.removeAll(data);
+        assertThreadLocalCount(threadLocalCount);
+    }
 }
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
index f394e71bea..97c3cdf03b 100644
--- 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
@@ -16,13 +16,16 @@
  */
 package org.apache.logging.log4j.spi;
 
+import static 
org.apache.logging.log4j.test.ThreadLocalUtil.assertThreadLocalCount;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.time.Duration;
+import java.util.Arrays;
 import java.util.Properties;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.stream.Stream;
+import org.apache.logging.log4j.test.ThreadLocalUtil;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -73,4 +76,37 @@ class ThreadContextMapTest {
             executorService.shutdown();
         }
     }
+
+    static Stream<ThreadContextMap> threadLocalsRemovedWhenMapEmpty() {
+        return Stream.of(new DefaultThreadContextMap(), new 
CopyOnWriteSortedArrayThreadContextMap());
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void threadLocalsRemovedWhenMapEmpty(final ThreadContextMap contextMap) {
+        // JUnit calls contextMap#toString() and sets the `ThreadLocal`.
+        contextMap.clear();
+        final int threadLocalCount = ThreadLocalUtil.getThreadLocalCount();
+
+        contextMap.put(KEY, "threadLocalsRemovedWhenMapEmpty");
+        assertThreadLocalCount(threadLocalCount + 1);
+        contextMap.remove(KEY);
+        assertThreadLocalCount(threadLocalCount);
+
+        contextMap.put("key1", "value1");
+        contextMap.put("key2", "value2");
+        assertThreadLocalCount(threadLocalCount + 1);
+        if (contextMap instanceof DefaultThreadContextMap) {
+            ((DefaultThreadContextMap) 
contextMap).removeAll(Arrays.asList("key1", "key2"));
+        }
+        if (contextMap instanceof CleanableThreadContextMap) {
+            ((CleanableThreadContextMap) 
contextMap).removeAll(Arrays.asList("key1", "key2"));
+        }
+        assertThreadLocalCount(threadLocalCount);
+
+        contextMap.put(KEY, "threadLocalsRemovedWhenMapEmpty");
+        assertThreadLocalCount(threadLocalCount + 1);
+        contextMap.clear();
+        assertThreadLocalCount(threadLocalCount);
+    }
 }
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
index 608ca77ffb..de837a9353 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
@@ -79,7 +79,11 @@ public class StringArrayThreadContextMap implements 
ThreadContextMap, ReadOnlySt
         if (state != null) {
             final UnmodifiableArrayBackedMap modifiedMap =
                     
UnmodifiableArrayBackedMap.getInstance(state).copyAndRemove(key);
-            threadLocalMapState.set(modifiedMap.getBackingArray());
+            if (modifiedMap.isEmpty()) {
+                threadLocalMapState.remove();
+            } else {
+                threadLocalMapState.set(modifiedMap.getBackingArray());
+            }
         }
     }
 
@@ -89,6 +93,11 @@ public class StringArrayThreadContextMap implements 
ThreadContextMap, ReadOnlySt
             final UnmodifiableArrayBackedMap modifiedMap =
                     
UnmodifiableArrayBackedMap.getInstance(state).copyAndRemoveAll(keys);
             threadLocalMapState.set(modifiedMap.getBackingArray());
+            if (modifiedMap.isEmpty()) {
+                threadLocalMapState.remove();
+            } else {
+                threadLocalMapState.set(modifiedMap.getBackingArray());
+            }
         }
     }
 
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
index ecc499ac58..058ce0bc47 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
@@ -102,7 +102,12 @@ public abstract class AbstractLogger implements 
ExtendedLogger, LocationAwareLog
     protected final String name;
     private final MessageFactory2 messageFactory;
     private final FlowMessageFactory flowMessageFactory;
-    private static final ThreadLocal<int[]> recursionDepthHolder = new 
ThreadLocal<>(); // LOG4J2-1518, LOG4J2-2031
+    /**
+     * See <a 
href="https://issues.apache.org/jira/browse/LOG4J2-1518";>LOG4J2-1518</a>,
+     * <a 
href="https://issues.apache.org/jira/browse/LOG4J2-2031";>LOG4J2-2031</a>.
+     */
+    private static final ThreadLocal<Integer> recursionDepthHolder = new 
ThreadLocal<>();
+
     private static final ThreadLocal<DefaultLogBuilder> logBuilder = 
ThreadLocal.withInitial(DefaultLogBuilder::new);
 
     /**
@@ -2860,23 +2865,16 @@ public abstract class AbstractLogger implements 
ExtendedLogger, LocationAwareLog
         }
     }
 
-    private static int[] getRecursionDepthHolder() {
-        int[] result = recursionDepthHolder.get();
-        if (result == null) {
-            result = new int[1];
-            recursionDepthHolder.set(result);
-        }
-        return result;
-    }
-
     private static void incrementRecursionDepth() {
-        getRecursionDepthHolder()[0]++;
+        recursionDepthHolder.set(getRecursionDepth() + 1);
     }
 
     private static void decrementRecursionDepth() {
-        final int newDepth = --getRecursionDepthHolder()[0];
-        if (newDepth < 0) {
-            throw new IllegalStateException("Recursion depth became negative: 
" + newDepth);
+        final int newDepth = getRecursionDepth() - 1;
+        if (newDepth == 0) {
+            recursionDepthHolder.remove();
+        } else {
+            recursionDepthHolder.set(newDepth);
         }
     }
 
@@ -2887,7 +2885,8 @@ public abstract class AbstractLogger implements 
ExtendedLogger, LocationAwareLog
      * @return the depth of the nested logging calls in the current Thread
      */
     public static int getRecursionDepth() {
-        return getRecursionDepthHolder()[0];
+        final Integer recursionDepth = recursionDepthHolder.get();
+        return recursionDepth != null ? recursionDepth : 0;
     }
 
     @PerformanceSensitive
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
index ca9f501c21..9377d5e16f 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
@@ -165,8 +165,12 @@ class CopyOnWriteSortedArrayThreadContextMap implements 
ReadOnlyThreadContextMap
         if (map != null) {
             final StringMap copy = createStringMap(map);
             copy.remove(key);
-            copy.freeze();
-            localMap.set(copy);
+            if (copy.isEmpty()) {
+                localMap.remove();
+            } else {
+                copy.freeze();
+                localMap.set(copy);
+            }
         }
     }
 
@@ -178,8 +182,12 @@ class CopyOnWriteSortedArrayThreadContextMap implements 
ReadOnlyThreadContextMap
             for (final String key : keys) {
                 copy.remove(key);
             }
-            copy.freeze();
-            localMap.set(copy);
+            if (copy.isEmpty()) {
+                localMap.remove();
+            } else {
+                copy.freeze();
+                localMap.set(copy);
+            }
         }
     }
 
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
index a07992d371..e5b658976a 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
@@ -104,7 +104,11 @@ public class DefaultThreadContextMap implements 
ThreadContextMap, ReadOnlyString
         if (map != null) {
             final Map<String, String> copy = new HashMap<>(map);
             copy.remove(key);
-            localMap.set(Collections.unmodifiableMap(copy));
+            if (copy.isEmpty()) {
+                localMap.remove();
+            } else {
+                localMap.set(Collections.unmodifiableMap(copy));
+            }
         }
     }
 
@@ -115,7 +119,11 @@ public class DefaultThreadContextMap implements 
ThreadContextMap, ReadOnlyString
             for (final String key : keys) {
                 copy.remove(key);
             }
-            localMap.set(Collections.unmodifiableMap(copy));
+            if (copy.isEmpty()) {
+                localMap.remove();
+            } else {
+                localMap.set(Collections.unmodifiableMap(copy));
+            }
         }
     }
 
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextStack.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextStack.java
index 5ed3fb41ea..7846f67f1a 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextStack.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextStack.java
@@ -35,7 +35,7 @@ public class DefaultThreadContextStack implements 
ThreadContextStack, StringBuil
 
     private static final long serialVersionUID = 5050501L;
 
-    private static final ThreadLocal<MutableThreadContextStack> STACK = new 
ThreadLocal<>();
+    private final ThreadLocal<MutableThreadContextStack> STACK = new 
ThreadLocal<>();
 
     private final boolean useStack;
 
@@ -197,8 +197,12 @@ public class DefaultThreadContextStack implements 
ThreadContextStack, StringBuil
         }
         final MutableThreadContextStack copy = (MutableThreadContextStack) 
values.copy();
         final String result = copy.pop();
-        copy.freeze();
-        STACK.set(copy);
+        if (copy.isEmpty()) {
+            STACK.remove();
+        } else {
+            copy.freeze();
+            STACK.set(copy);
+        }
         return result;
     }
 
@@ -221,8 +225,12 @@ public class DefaultThreadContextStack implements 
ThreadContextStack, StringBuil
         }
         final MutableThreadContextStack copy = (MutableThreadContextStack) 
values.copy();
         final boolean result = copy.remove(o);
-        copy.freeze();
-        STACK.set(copy);
+        if (copy.isEmpty()) {
+            STACK.remove();
+        } else {
+            copy.freeze();
+            STACK.set(copy);
+        }
         return result;
     }
 
@@ -237,8 +245,12 @@ public class DefaultThreadContextStack implements 
ThreadContextStack, StringBuil
         }
         final MutableThreadContextStack copy = (MutableThreadContextStack) 
values.copy();
         final boolean result = copy.removeAll(objects);
-        copy.freeze();
-        STACK.set(copy);
+        if (copy.isEmpty()) {
+            STACK.remove();
+        } else {
+            copy.freeze();
+            STACK.set(copy);
+        }
         return result;
     }
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
index 91057b53d6..09f596a578 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
@@ -311,6 +311,10 @@ public final class Strings {
         return str.toUpperCase(Locale.ROOT);
     }
 
+    private static StringBuilder getStringBuilder() {
+        return Constants.ENABLE_THREADLOCALS ? FORMAT_BUFFER_REF.get() : new 
StringBuilder();
+    }
+
     /**
      * Concatenates 2 Strings without allocation.
      * @param str1 the first string.
@@ -323,7 +327,7 @@ public final class Strings {
         } else if (isEmpty(str2)) {
             return str1;
         }
-        final StringBuilder sb = FORMAT_BUFFER_REF.get();
+        final StringBuilder sb = getStringBuilder();
         try {
             return sb.append(str1).append(str2).toString();
         } finally {
@@ -344,7 +348,7 @@ public final class Strings {
         if (count < 0) {
             throw new IllegalArgumentException("count");
         }
-        final StringBuilder sb = FORMAT_BUFFER_REF.get();
+        final StringBuilder sb = getStringBuilder();
         try {
             for (int index = 0; index < count; index++) {
                 sb.append(str);
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Timer.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/util/Timer.java
index f85fb1a1c0..cbb2841a7d 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Timer.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Timer.java
@@ -38,15 +38,10 @@ public class Timer implements Serializable, 
StringBuilderFormattable {
     private Status status; // The timer's status
     private long elapsedTime; // The elapsed time
     private final int iterations;
-    private static long NANO_PER_SECOND = 1000000000L;
-    private static long NANO_PER_MINUTE = NANO_PER_SECOND * 60;
-    private static long NANO_PER_HOUR = NANO_PER_MINUTE * 60;
-    private ThreadLocal<Long> startTime = new ThreadLocal<Long>() {
-        @Override
-        protected Long initialValue() {
-            return 0L;
-        }
-    };
+    private static final long NANO_PER_SECOND = 1000000000L;
+    private static final long NANO_PER_MINUTE = NANO_PER_SECOND * 60;
+    private static final long NANO_PER_HOUR = NANO_PER_MINUTE * 60;
+    private final ThreadLocal<Long> startTime = ThreadLocal.withInitial(() -> 
0L);
 
     /**
      * Constructor.
@@ -64,7 +59,7 @@ public class Timer implements Serializable, 
StringBuilderFormattable {
     public Timer(final String name, final int iterations) {
         this.name = name;
         status = Status.Stopped;
-        this.iterations = (iterations > 0) ? iterations : 0;
+        this.iterations = Math.max(iterations, 0);
     }
 
     /**
@@ -89,7 +84,7 @@ public class Timer implements Serializable, 
StringBuilderFormattable {
      */
     public synchronized String stop() {
         elapsedTime += System.nanoTime() - startTime.get();
-        startTime.set(0L);
+        startTime.remove();
         status = Status.Stopped;
         return toString();
     }
@@ -99,7 +94,7 @@ public class Timer implements Serializable, 
StringBuilderFormattable {
      */
     public synchronized void pause() {
         elapsedTime += System.nanoTime() - startTime.get();
-        startTime.set(0L);
+        startTime.remove();
         status = Status.Paused;
     }
 
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfig.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfig.java
index b3c11bb5d6..d181000800 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfig.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfig.java
@@ -94,12 +94,7 @@ public class AsyncLoggerConfig extends LoggerConfig {
         }
     }
 
-    private static final ThreadLocal<Boolean> ASYNC_LOGGER_ENTERED = new 
ThreadLocal<Boolean>() {
-        @Override
-        protected Boolean initialValue() {
-            return Boolean.FALSE;
-        }
-    };
+    private static final ThreadLocal<Boolean> ASYNC_LOGGER_ENTERED = new 
ThreadLocal<>();
 
     private final AsyncLoggerConfigDelegate delegate;
 
@@ -126,29 +121,30 @@ public class AsyncLoggerConfig extends LoggerConfig {
     protected void log(final LogEvent event, final LoggerConfigPredicate 
predicate) {
         // See LOG4J2-2301
         if (predicate == LoggerConfigPredicate.ALL
-                && ASYNC_LOGGER_ENTERED.get() == Boolean.FALSE
                 &&
                 // Optimization: AsyncLoggerConfig is identical to LoggerConfig
                 // when no appenders are present. Avoid splitting for 
synchronous
                 // and asynchronous execution paths until encountering an
                 // AsyncLoggerConfig with appenders.
                 hasAppenders()) {
-            // This is the first AsnycLoggerConfig encountered by this LogEvent
-            ASYNC_LOGGER_ENTERED.set(Boolean.TRUE);
             try {
-                if (!isFiltered(event)) {
-                    // Detect the first time we encounter an 
AsyncLoggerConfig. We must log
-                    // to all non-async loggers first.
-                    processLogEvent(event, 
LoggerConfigPredicate.SYNCHRONOUS_ONLY);
-                    // Then pass the event to the background thread where
-                    // all async logging is executed. It is important this
-                    // happens at most once and after all synchronous loggers
-                    // have been invoked, because we lose parameter references
-                    // from reusable messages.
-                    logToAsyncDelegate(event);
+                if (ASYNC_LOGGER_ENTERED.get() == null) {
+                    // This is the first AsyncLoggerConfig encountered by this 
LogEvent
+                    ASYNC_LOGGER_ENTERED.set(Boolean.TRUE);
+                    if (!isFiltered(event)) {
+                        // Detect the first time we encounter an 
AsyncLoggerConfig. We must log
+                        // to all non-async loggers first.
+                        processLogEvent(event, 
LoggerConfigPredicate.SYNCHRONOUS_ONLY);
+                        // Then pass the event to the background thread where
+                        // all async logging is executed. It is important this
+                        // happens at most once and after all synchronous 
loggers
+                        // have been invoked, because we lose parameter 
references
+                        // from reusable messages.
+                        logToAsyncDelegate(event);
+                    }
                 }
             } finally {
-                ASYNC_LOGGER_ENTERED.set(Boolean.FALSE);
+                ASYNC_LOGGER_ENTERED.remove();
             }
         } else {
             super.log(event, predicate);
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AppenderControl.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AppenderControl.java
index 2dfd13b2a8..660abac909 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AppenderControl.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AppenderControl.java
@@ -123,7 +123,7 @@ public class AppenderControl extends AbstractFilterable {
             recursive.set(this);
             callAppender0(event);
         } finally {
-            recursive.set(null);
+            recursive.remove();
         }
     }
 
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
index d9c991d6b6..6a105fff54 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
@@ -30,6 +30,7 @@ import org.apache.logging.log4j.core.config.plugins.Plugin;
 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
 import org.apache.logging.log4j.core.config.plugins.PluginElement;
 import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.apache.logging.log4j.core.util.Constants;
 import org.apache.logging.log4j.core.util.KeyValuePair;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.StructuredDataMessage;
@@ -45,7 +46,7 @@ import org.apache.logging.log4j.util.StringBuilders;
 public final class StructuredDataFilter extends MapFilter {
 
     private static final int MAX_BUFFER_SIZE = 2048;
-    private static ThreadLocal<StringBuilder> threadLocalStringBuilder = new 
ThreadLocal<>();
+    private static final ThreadLocal<StringBuilder> threadLocalStringBuilder = 
new ThreadLocal<>();
 
     private StructuredDataFilter(
             final Map<String, List<String>> map, final boolean oper, final 
Result onMatch, final Result onMismatch) {
@@ -105,14 +106,17 @@ public final class StructuredDataFilter extends MapFilter 
{
     }
 
     private StringBuilder getStringBuilder() {
-        StringBuilder result = threadLocalStringBuilder.get();
-        if (result == null) {
-            result = new StringBuilder();
-            threadLocalStringBuilder.set(result);
+        if (Constants.ENABLE_THREADLOCALS) {
+            StringBuilder result = threadLocalStringBuilder.get();
+            if (result == null) {
+                result = new StringBuilder();
+                threadLocalStringBuilder.set(result);
+            }
+            StringBuilders.trimToMaxSize(result, MAX_BUFFER_SIZE);
+            result.setLength(0);
+            return result;
         }
-        StringBuilders.trimToMaxSize(result, MAX_BUFFER_SIZE);
-        result.setLength(0);
-        return result;
+        return new StringBuilder();
     }
 
     private StringBuilder appendOrNull(final String value, final StringBuilder 
sb) {
diff --git 
a/log4j-jul/src/main/java/org/apache/logging/log4j/jul/LogManager.java 
b/log4j-jul/src/main/java/org/apache/logging/log4j/jul/LogManager.java
index bb8f6e737e..4f396d13b4 100644
--- a/log4j-jul/src/main/java/org/apache/logging/log4j/jul/LogManager.java
+++ b/log4j-jul/src/main/java/org/apache/logging/log4j/jul/LogManager.java
@@ -16,6 +16,8 @@
  */
 package org.apache.logging.log4j.jul;
 
+import static org.apache.logging.log4j.util.Constants.ENABLE_THREADLOCALS;
+
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashSet;
@@ -94,6 +96,9 @@ public class LogManager extends java.util.logging.LogManager {
                 return loggerAdapter.getLogger(name);
             } finally {
                 activeRequests.remove(name);
+                if (!ENABLE_THREADLOCALS && activeRequests.isEmpty()) {
+                    recursive.remove();
+                }
             }
         }
         LOGGER.warn("Recursive call to getLogger for {} ignored.", name);
diff --git a/log4j-slf4j2-impl/pom.xml b/log4j-slf4j2-impl/pom.xml
index f6011d3948..67f4c01bed 100644
--- a/log4j-slf4j2-impl/pom.xml
+++ b/log4j-slf4j2-impl/pom.xml
@@ -157,4 +157,41 @@
 
     </plugins>
   </build>
+
+  <profiles>
+
+    <!-- Fixes incompatible with Java 8 -->
+    <profile>
+
+      <id>java8-incompat-fixes</id>
+
+      <!-- CI uses Java 8 for running tests.
+           Hence, we assume CI=Java8 and apply our changes elsewhere.
+
+           One might think why not activate using `<jdk>[16,)` instead?
+           This doesn't work, since the match is not against "the JDK running 
tests", but "the JDK running Maven".
+           These two JDKs can differ due to Maven Toolchains.
+           See `java8-tests` profile in `/pom.xml` for details. -->
+      <activation>
+        <property>
+          <name>!env.CI</name>
+        </property>
+      </activation>
+
+      <!-- Illegal access is disabled by default in Java 16 due to JEP-396.
+           We are relaxing it for tests. -->
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <configuration>
+              <argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+
+    </profile>
+  </profiles>
 </project>
diff --git 
a/log4j-slf4j2-impl/src/main/java/org/apache/logging/slf4j/Log4jMDCAdapter.java 
b/log4j-slf4j2-impl/src/main/java/org/apache/logging/slf4j/Log4jMDCAdapter.java
index bae69e961d..ab1184e5bb 100644
--- 
a/log4j-slf4j2-impl/src/main/java/org/apache/logging/slf4j/Log4jMDCAdapter.java
+++ 
b/log4j-slf4j2-impl/src/main/java/org/apache/logging/slf4j/Log4jMDCAdapter.java
@@ -25,11 +25,9 @@ import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.ThreadContext.ContextStack;
 import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Constants;
 import org.slf4j.spi.MDCAdapter;
 
-/**
- *
- */
 public class Log4jMDCAdapter implements MDCAdapter {
 
     private static final Logger LOGGER = StatusLogger.getLogger();
@@ -90,7 +88,12 @@ public class Log4jMDCAdapter implements MDCAdapter {
         if (!Objects.equals(ThreadContext.get(key), value)) {
             LOGGER.warn("The key {} was used in both the string and 
stack-valued MDC.", key);
         }
-        ThreadContext.put(key, mapOfStacks.peekByKey(key));
+        final String oldValue = mapOfStacks.peekByKey(key);
+        if (oldValue != null) {
+            ThreadContext.put(key, oldValue);
+        } else {
+            ThreadContext.remove(key);
+        }
         return value;
     }
 
@@ -111,41 +114,92 @@ public class Log4jMDCAdapter implements MDCAdapter {
             ThreadContext.clearStack();
         } else {
             mapOfStacks.clearByKey(key);
-            ThreadContext.put(key, null);
+            ThreadContext.remove(key);
         }
     }
 
+    // Used by tests
+    void clearDeque() {
+        mapOfStacks.clear();
+    }
+
     private static class ThreadLocalMapOfStacks {
 
         private final ThreadLocal<Map<String, Deque<String>>> tlMapOfStacks = 
ThreadLocal.withInitial(HashMap::new);
 
+        private Map<String, Deque<String>> getMapOfStacks() {
+            Map<String, Deque<String>> localMap = tlMapOfStacks.get();
+            if (localMap == null) {
+                tlMapOfStacks.set(localMap = new HashMap<>());
+            }
+            return localMap;
+        }
+
+        private void removeIfEmpty(final Map<String, Deque<String>> 
mapOfStacks) {
+            if (!Constants.ENABLE_THREADLOCALS && mapOfStacks != null && 
mapOfStacks.isEmpty()) {
+                tlMapOfStacks.remove();
+            }
+        }
+
         public void pushByKey(final String key, final String value) {
-            tlMapOfStacks
-                    .get()
-                    .computeIfAbsent(key, ignored -> new ArrayDeque<>())
-                    .push(value);
+            getMapOfStacks().computeIfAbsent(key, ignored -> new 
ArrayDeque<>()).push(value);
         }
 
         public String popByKey(final String key) {
-            final Deque<String> deque = tlMapOfStacks.get().get(key);
-            return deque != null ? deque.poll() : null;
+            final Map<String, Deque<String>> mapOfStacks = tlMapOfStacks.get();
+            if (mapOfStacks != null) {
+                final Deque<String> deque = mapOfStacks.get(key);
+                if (deque != null) {
+                    final String result = deque.poll();
+                    if (!Constants.ENABLE_THREADLOCALS && deque.isEmpty()) {
+                        mapOfStacks.remove(key);
+                        removeIfEmpty(mapOfStacks);
+                    }
+                    return result;
+                }
+            }
+            return null;
         }
 
         public Deque<String> getCopyOfDequeByKey(final String key) {
-            final Deque<String> deque = tlMapOfStacks.get().get(key);
-            return deque != null ? new ArrayDeque<>(deque) : null;
+            final Map<String, Deque<String>> mapOfStacks = tlMapOfStacks.get();
+            if (mapOfStacks != null) {
+                final Deque<String> deque = mapOfStacks.get(key);
+                return deque != null ? new ArrayDeque<>(deque) : null;
+            }
+            return null;
+        }
+
+        public void clear() {
+            final Map<String, Deque<String>> mapOfStacks = tlMapOfStacks.get();
+            if (Constants.ENABLE_THREADLOCALS) {
+                mapOfStacks.clear();
+            } else {
+                tlMapOfStacks.remove();
+            }
         }
 
         public void clearByKey(final String key) {
-            final Deque<String> deque = tlMapOfStacks.get().get(key);
-            if (deque != null) {
-                deque.clear();
+            final Map<String, Deque<String>> mapOfStacks = tlMapOfStacks.get();
+            if (mapOfStacks != null) {
+                mapOfStacks.computeIfPresent(key, (ignored, deque) -> {
+                    if (Constants.ENABLE_THREADLOCALS) {
+                        deque.clear();
+                        return deque;
+                    }
+                    return null;
+                });
             }
+            removeIfEmpty(mapOfStacks);
         }
 
         public String peekByKey(final String key) {
-            final Deque<String> deque = tlMapOfStacks.get().get(key);
-            return deque != null ? deque.peek() : null;
+            final Map<String, Deque<String>> mapOfStacks = tlMapOfStacks.get();
+            if (mapOfStacks != null) {
+                final Deque<String> deque = mapOfStacks.get(key);
+                return deque != null ? deque.peek() : null;
+            }
+            return null;
         }
     }
 }
diff --git 
a/log4j-slf4j2-impl/src/test/java/org/apache/logging/slf4j/Log4jMDCAdapterTest.java
 
b/log4j-slf4j2-impl/src/test/java/org/apache/logging/slf4j/Log4jMDCAdapterTest.java
index 0559608f46..3d38f5ce9f 100644
--- 
a/log4j-slf4j2-impl/src/test/java/org/apache/logging/slf4j/Log4jMDCAdapterTest.java
+++ 
b/log4j-slf4j2-impl/src/test/java/org/apache/logging/slf4j/Log4jMDCAdapterTest.java
@@ -16,16 +16,21 @@
  */
 package org.apache.logging.slf4j;
 
+import static 
org.apache.logging.log4j.test.ThreadLocalUtil.assertThreadLocalCount;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
+import org.apache.logging.log4j.test.ThreadLocalUtil;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junitpioneer.jupiter.SetSystemProperty;
 
-public class Log4jMDCAdapterTest {
+@SetSystemProperty(key = "log4j2.enableThreadLocals", value = "false")
+class Log4jMDCAdapterTest {
 
     private static final Log4jMDCAdapter MDC_ADAPTER = new Log4jMDCAdapter();
     private static final String KEY = "Log4j2";
@@ -51,11 +56,22 @@ public class Log4jMDCAdapterTest {
 
     @ParameterizedTest
     @MethodSource("keys")
-    public void testPushPopByKey(final String key) {
+    void testPushPopByKey(final String key) {
         MDC_ADAPTER.clearDequeByKey(key);
         final Deque<String> expectedValues = createDeque(100);
         expectedValues.descendingIterator().forEachRemaining(v -> 
MDC_ADAPTER.pushByKey(key, v));
         
assertThat(MDC_ADAPTER.getCopyOfDequeByKey(key)).containsExactlyElementsOf(expectedValues);
         assertThat(popDeque(key)).containsExactlyElementsOf(expectedValues);
     }
+
+    @Test
+    void threadLocalClearedWhenMapOfStacksEmpty() {
+        MDC_ADAPTER.clear();
+        MDC_ADAPTER.clearDeque();
+        final int threadLocalCount = ThreadLocalUtil.getThreadLocalCount();
+        MDC_ADAPTER.pushByKey(KEY, "threadLocalClearedWhenMapOfStacksEmpty");
+        assertThreadLocalCount(threadLocalCount + 1);
+        
assertThat(MDC_ADAPTER.popByKey(KEY)).isEqualTo("threadLocalClearedWhenMapOfStacksEmpty");
+        assertThreadLocalCount(threadLocalCount);
+    }
 }


Reply via email to