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

pkarwasz pushed a commit to branch feature/move-thread-context
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit f61d9fdee5abc3f02b534c8f04f938e2fc465bcd
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Fri Jun 21 16:53:26 2024 +0200

    Make `StringArray` the default thread context map
---
 .../log4j/ThreadContextInheritanceTest.java        |  28 --
 .../map/StringArrayThreadContextMapTest.java       | 287 ---------------------
 .../map/UnmodifiableArrayBackedMapTest.java        |   6 +-
 .../log4j/spi/DefaultThreadContextMapTest.java     |  12 -
 .../logging/log4j/spi/ThreadContextMapTest.java    |   2 +-
 log4j-api/pom.xml                                  |  11 +-
 .../org/apache/logging/log4j/ThreadContext.java    |   5 -
 .../internal/map/StringArrayThreadContextMap.java  | 199 --------------
 .../internal/map/UnmodifiableArrayBackedMap.java   |  14 +-
 .../logging/log4j/spi/DefaultThreadContextMap.java | 150 +++++------
 .../logging/log4j/spi/NoOpThreadContextMap.java    |  16 +-
 .../org/apache/logging/log4j/spi/Provider.java     | 256 +++++-------------
 .../logging/log4j/spi/ThreadContextMapFactory.java |   4 +-
 .../async/AbstractAsyncThreadContextTestBase.java  |  69 ++---
 .../core/impl/ThreadContextDataInjectorTest.java   |  40 ++-
 .../{ => core/impl}/ThreadContextTestAccess.java   |   7 +-
 log4j-core/pom.xml                                 |   7 +
 .../logging/log4j/core/impl/Log4jProvider.java     | 167 ++++++++++++
 log4j-osgi-test/pom.xml                            |   2 +-
 .../jmh/ThreadContextVsScopedContextBenchmark.java |   3 -
 log4j-to-jul/pom.xml                               |  13 +
 .../apache/logging/log4j/tojul/JULProvider.java    |  10 +-
 log4j-to-slf4j/pom.xml                             |   7 +
 .../org/apache/logging/slf4j/SLF4JProvider.java    |   4 +-
 .../.2.x.x/2330_add_faster_web_app_context_map.xml |   2 +-
 25 files changed, 420 insertions(+), 901 deletions(-)

diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
index 96783ae600..c800b396a6 100644
--- 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
@@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.logging.log4j.spi.DefaultThreadContextMap;
 import org.apache.logging.log4j.test.ThreadContextUtilityClass;
-import org.apache.logging.log4j.test.junit.InitializesThreadContext;
 import org.apache.logging.log4j.test.junit.SetTestProperty;
 import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
 import org.apache.logging.log4j.test.junit.UsingThreadContextStack;
@@ -31,13 +30,11 @@ import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
-import org.junitpioneer.jupiter.SetSystemProperty;
 
 /**
  * Tests {@link ThreadContext}.
  */
 @SetTestProperty(key = DefaultThreadContextMap.INHERITABLE_MAP, value = "true")
-@InitializesThreadContext
 @UsingThreadContextMap
 @UsingThreadContextStack
 public class ThreadContextInheritanceTest {
@@ -63,31 +60,6 @@ public class ThreadContextInheritanceTest {
         assertEquals(ThreadContext.pop(), "Hello", "Incorrect simple stack 
value");
     }
 
-    @Test
-    @SetSystemProperty(key = DefaultThreadContextMap.INHERITABLE_MAP, value = 
"true")
-    @InitializesThreadContext
-    public void testInheritanceSwitchedOn() throws Exception {
-        System.setProperty(DefaultThreadContextMap.INHERITABLE_MAP, "true");
-        try {
-            ThreadContext.clearMap();
-            ThreadContext.put("Greeting", "Hello");
-            StringBuilder sb = new StringBuilder();
-            TestThread thread = new TestThread(sb);
-            thread.start();
-            thread.join();
-            String str = sb.toString();
-            assertEquals("Hello", str, "Unexpected ThreadContext value. 
Expected Hello. Actual " + str);
-            sb = new StringBuilder();
-            thread = new TestThread(sb);
-            thread.start();
-            thread.join();
-            str = sb.toString();
-            assertEquals("Hello", str, "Unexpected ThreadContext value. 
Expected Hello. Actual " + str);
-        } finally {
-            System.clearProperty(DefaultThreadContextMap.INHERITABLE_MAP);
-        }
-    }
-
     @Test
     @Tag("performance")
     public void perfTest() {
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
deleted file mode 100644
index 2f845d8bf7..0000000000
--- 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMapTest.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * 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.internal.map;
-
-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.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
-import org.apache.logging.log4j.util.TriConsumer;
-import org.junit.jupiter.api.Test;
-
-/**
- * Tests the {@code StringArrayThreadContextMap} class.
- */
-@UsingThreadContextMap
-public class StringArrayThreadContextMapTest {
-
-    @Test
-    public void testEqualsVsSameKind() {
-        final StringArrayThreadContextMap map1 = createMap();
-        final StringArrayThreadContextMap map2 = createMap();
-        assertEquals(map1, map1);
-        assertEquals(map2, map2);
-        assertEquals(map1, map2);
-        assertEquals(map2, map1);
-    }
-
-    @Test
-    public void testHashCodeVsSameKind() {
-        final StringArrayThreadContextMap map1 = createMap();
-        final StringArrayThreadContextMap map2 = createMap();
-        assertEquals(map1.hashCode(), map2.hashCode());
-    }
-
-    @Test
-    public void testGet() {
-        final StringArrayThreadContextMap map1 = createMap();
-        assertNull(map1.get("test"));
-        map1.put("test", "test");
-        assertEquals("test", map1.get("test"));
-        assertNull(map1.get("not_present"));
-        assertEquals("test", map1.getValue("test"));
-        assertNull(map1.getValue("not_present"));
-
-        map1.clear();
-        assertNull(map1.get("not_present"));
-    }
-
-    @Test
-    public void testPut() {
-        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap();
-        assertTrue(map.isEmpty());
-        assertFalse(map.containsKey("key"));
-        map.put("key", "value");
-
-        assertFalse(map.isEmpty());
-        assertTrue(map.containsKey("key"));
-        assertEquals("value", map.get("key"));
-    }
-
-    @Test
-    public void testPutAll() {
-        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap();
-        assertTrue(map.isEmpty());
-        assertFalse(map.containsKey("key"));
-        final int mapSize = 10;
-        final Map<String, String> newMap = new HashMap<>(mapSize);
-        for (int i = 1; i <= mapSize; i++) {
-            newMap.put("key" + i, "value" + i);
-        }
-        map.putAll(newMap);
-        assertFalse(map.isEmpty());
-        for (int i = 1; i <= mapSize; i++) {
-            assertTrue(map.containsKey("key" + i));
-            assertEquals("value" + i, map.get("key" + i));
-        }
-    }
-
-    /**
-     * Test method for
-     * {@link 
org.apache.logging.log4j.internal.map.StringArrayThreadContextMap#remove(java.lang.String)}
-     * .
-     */
-    @Test
-    public void testRemove() {
-        final StringArrayThreadContextMap map = createMap();
-        assertEquals("value", map.get("key"));
-        assertEquals("value2", map.get("key2"));
-
-        map.remove("key");
-        assertFalse(map.containsKey("key"));
-        assertEquals("value2", map.get("key2"));
-
-        map.clear();
-        map.remove("test");
-    }
-
-    @Test
-    public void testRemoveAll() {
-        final StringArrayThreadContextMap map = createMap();
-
-        Map<String, String> newValues = new HashMap<>();
-        newValues.put("1", "value1");
-        newValues.put("2", "value2");
-
-        map.putAll(newValues);
-        map.removeAll(newValues.keySet());
-
-        map.put("3", "value3");
-
-        map.clear();
-        map.removeAll(newValues.keySet());
-    }
-
-    @Test
-    public void testClear() {
-        final StringArrayThreadContextMap map = createMap();
-
-        map.clear();
-        assertTrue(map.isEmpty());
-        assertFalse(map.containsKey("key"));
-        assertFalse(map.containsKey("key2"));
-    }
-
-    /**
-     * @return
-     */
-    private StringArrayThreadContextMap createMap() {
-        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap();
-        assertTrue(map.isEmpty());
-        map.put("key", "value");
-        map.put("key2", "value2");
-        assertEquals("value", map.get("key"));
-        assertEquals("value2", map.get("key2"));
-        return map;
-    }
-
-    @Test
-    public void testGetCopyReturnsMutableMap() {
-        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap();
-        assertTrue(map.isEmpty());
-        final Map<String, String> copy = map.getCopy();
-        assertTrue(copy.isEmpty());
-
-        copy.put("key", "value"); // mutable
-        assertEquals("value", copy.get("key"));
-
-        // thread context map not affected
-        assertTrue(map.isEmpty());
-    }
-
-    @Test
-    public void testGetCopyReturnsMutableCopy() {
-        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap();
-        map.put("key1", "value1");
-        assertFalse(map.isEmpty());
-        final Map<String, String> copy = map.getCopy();
-        assertEquals("value1", copy.get("key1")); // copy has values too
-
-        copy.put("key", "value"); // copy is mutable
-        assertEquals("value", copy.get("key"));
-
-        // thread context map not affected
-        assertFalse(map.containsKey("key"));
-
-        // clearing context map does not affect copy
-        map.clear();
-        assertTrue(map.isEmpty());
-
-        assertFalse(copy.isEmpty());
-    }
-
-    @Test
-    public void testGetImmutableMapReturnsNullIfEmpty() {
-        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap();
-        assertTrue(map.isEmpty());
-        assertNull(map.getImmutableMapOrNull());
-    }
-
-    @Test
-    public void testGetImmutableMapReturnsImmutableMapIfNonEmpty() {
-        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap();
-        map.put("key1", "value1");
-        assertFalse(map.isEmpty());
-
-        final Map<String, String> immutable = map.getImmutableMapOrNull();
-        assertEquals("value1", immutable.get("key1")); // copy has values too
-
-        // immutable
-        assertThrows(UnsupportedOperationException.class, () -> 
immutable.put("key", "value"));
-    }
-
-    @Test
-    public void testGetImmutableMapCopyNotAffectdByContextMapChanges() {
-        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap();
-        map.put("key1", "value1");
-        assertFalse(map.isEmpty());
-
-        final Map<String, String> immutable = map.getImmutableMapOrNull();
-        assertEquals("value1", immutable.get("key1")); // copy has values too
-
-        // clearing context map does not affect copy
-        map.clear();
-        assertTrue(map.isEmpty());
-
-        assertFalse(immutable.isEmpty());
-    }
-
-    @Test
-    public void testToStringShowsMapContext() {
-        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap();
-        assertEquals("{}", map.toString());
-
-        map.put("key1", "value1");
-        assertEquals("{key1=value1}", map.toString());
-
-        map.remove("key1");
-        map.put("key2", "value2");
-        assertEquals("{key2=value2}", map.toString());
-    }
-
-    @Test
-    public void testEmptyMap() {
-        assertNull(UnmodifiableArrayBackedMap.EMPTY_MAP.get("test"));
-    }
-
-    @Test
-    public void testForEachBiConsumer_Log4jUtil() {
-        StringArrayThreadContextMap map = createMap();
-        Set<String> keys = new HashSet<>();
-        org.apache.logging.log4j.util.BiConsumer<String, String> 
log4j_util_action =
-                new org.apache.logging.log4j.util.BiConsumer<String, String>() 
{
-                    @Override
-                    public void accept(String key, String value) {
-                        keys.add(key);
-                    }
-                };
-        map.forEach(log4j_util_action);
-        assertEquals(map.toMap().keySet(), keys);
-
-        map.clear();
-        keys.clear();
-        map.forEach(log4j_util_action);
-        assertTrue(keys.isEmpty());
-    }
-
-    @Test
-    public void testForEachTriConsumer() {
-        StringArrayThreadContextMap map = createMap();
-        HashMap<String, String> iterationResultMap = new HashMap<>();
-        TriConsumer<String, String, Map<String, String>> triConsumer =
-                new TriConsumer<String, String, Map<String, String>>() {
-                    @Override
-                    public void accept(String k, String v, Map<String, String> 
s) {
-                        s.put(k, v);
-                    }
-                };
-        map.forEach(triConsumer, iterationResultMap);
-        assertEquals(map.toMap(), iterationResultMap);
-
-        map.clear();
-        iterationResultMap.clear();
-        map.forEach(triConsumer, iterationResultMap);
-        assertTrue(iterationResultMap.isEmpty());
-    }
-}
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java
index 9652034d70..5957a931bf 100644
--- 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMapTest.java
@@ -352,17 +352,17 @@ public class UnmodifiableArrayBackedMapTest {
         UnmodifiableArrayBackedMap newMap;
 
         originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
-        newMap = 
UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+        newMap = 
UnmodifiableArrayBackedMap.getMap(originalMap.getBackingArray());
         assertEquals(originalMap, newMap);
 
         originalMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
-        newMap = 
UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+        newMap = 
UnmodifiableArrayBackedMap.getMap(originalMap.getBackingArray());
         assertEquals(originalMap, newMap);
 
         originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP
                 .copyAndPutAll(getTestParameters())
                 .copyAndRemove("1");
-        newMap = 
UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+        newMap = 
UnmodifiableArrayBackedMap.getMap(originalMap.getBackingArray());
         assertEquals(originalMap, newMap);
     }
 
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java
index dedc5be86b..243ce0e9fc 100644
--- 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java
@@ -50,18 +50,6 @@ public class DefaultThreadContextMapTest {
         assertEquals(map1.hashCode(), map2.hashCode());
     }
 
-    @Test
-    public void testDoesNothingIfConstructedWithUseMapIsFalse() {
-        final DefaultThreadContextMap map = new DefaultThreadContextMap(false);
-        assertTrue(map.isEmpty());
-        assertFalse(map.containsKey("key"));
-        map.put("key", "value");
-
-        assertTrue(map.isEmpty());
-        assertFalse(map.containsKey("key"));
-        assertNull(map.get("key"));
-    }
-
     @Test
     public void testPut() {
         final DefaultThreadContextMap map = new DefaultThreadContextMap(true);
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..04392d7d7a 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
@@ -43,7 +43,7 @@ class ThreadContextMapTest {
         props.setProperty("log4j2.isThreadContextMapInheritable", "true");
         final PropertiesUtil util = new PropertiesUtil(props);
         return Stream.of(
-                new DefaultThreadContextMap(true, util),
+                new DefaultThreadContextMap(util),
                 new CopyOnWriteSortedArrayThreadContextMap(util),
                 new GarbageFreeSortedArrayThreadContextMap(util));
     }
diff --git a/log4j-api/pom.xml b/log4j-api/pom.xml
index af22fee165..5423c4f99f 100644
--- a/log4j-api/pom.xml
+++ b/log4j-api/pom.xml
@@ -46,7 +46,9 @@
     <bnd-module-name>org.apache.logging.log4j</bnd-module-name>
     <bnd-extra-package-options>
       <!-- Not exported by most OSGi system bundles, hence we use the system 
classloader to load `sun.reflect.Reflection` -->
-      !sun.reflect
+      !sun.reflect,
+      <!-- Annotations only -->
+      org.jspecify.*;resolution:=optional
     </bnd-extra-package-options>
     <bnd-extra-module-options>
       <!-- Used in StringBuilders through reflection -->
@@ -57,6 +59,13 @@
 
   </properties>
   <dependencies>
+
+    <dependency>
+      <groupId>org.jspecify</groupId>
+      <artifactId>jspecify</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
     <dependency>
       <groupId>org.osgi</groupId>
       <artifactId>org.osgi.core</artifactId>
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java
index ddc36de030..d919b3130a 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java
@@ -23,7 +23,6 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import org.apache.logging.log4j.internal.map.StringArrayThreadContextMap;
 import org.apache.logging.log4j.message.ParameterizedMessage;
 import org.apache.logging.log4j.spi.CleanableThreadContextMap;
 import org.apache.logging.log4j.spi.DefaultThreadContextMap;
@@ -276,8 +275,6 @@ public final class ThreadContext {
             ((ThreadContextMap2) contextMap).putAll(m);
         } else if (contextMap instanceof DefaultThreadContextMap) {
             ((DefaultThreadContextMap) contextMap).putAll(m);
-        } else if (contextMap instanceof StringArrayThreadContextMap) {
-            ((StringArrayThreadContextMap) contextMap).putAll(m);
         } else {
             for (final Map.Entry<String, String> entry : m.entrySet()) {
                 contextMap.put(entry.getKey(), entry.getValue());
@@ -320,8 +317,6 @@ public final class ThreadContext {
             ((CleanableThreadContextMap) contextMap).removeAll(keys);
         } else if (contextMap instanceof DefaultThreadContextMap) {
             ((DefaultThreadContextMap) contextMap).removeAll(keys);
-        } else if (contextMap instanceof StringArrayThreadContextMap) {
-            ((StringArrayThreadContextMap) contextMap).removeAll(keys);
         } else {
             for (final String key : keys) {
                 contextMap.remove(key);
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
deleted file mode 100644
index 608ca77ffb..0000000000
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * 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.internal.map;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import org.apache.logging.log4j.spi.ThreadContextMap;
-import org.apache.logging.log4j.util.BiConsumer;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
-import org.apache.logging.log4j.util.TriConsumer;
-
-/**
- * An equivalent for DefaultThreadContxtMap, except that it's backed by
- * UnmodifiableArrayBackedMap. An instance of UnmodifiableArrayBackedMap can be
- * represented as a single Object[], which can safely be stored on the
- * ThreadLocal with no fear of classloader-related memory leaks. Performance
- * of the underlying UnmodifiableArrayBackedMap exceeds HashMap in all
- * supported operations other than get(). Note that get() performance scales
- * linearly with the current map size, and callers are advised to minimize this
- * work.
- */
-public class StringArrayThreadContextMap implements ThreadContextMap, 
ReadOnlyStringMap {
-    private static final long serialVersionUID = -2635197170958057849L;
-
-    /**
-     * Property name ({@value} ) for selecting {@code InheritableThreadLocal} 
(value "true") or plain
-     * {@code ThreadLocal} (value is not "true") in the implementation.
-     */
-    public static final String INHERITABLE_MAP = 
"isThreadContextMapInheritable";
-
-    private ThreadLocal<Object[]> threadLocalMapState;
-
-    public StringArrayThreadContextMap() {
-        threadLocalMapState = new ThreadLocal<>();
-    }
-
-    @Override
-    public void put(final String key, final String value) {
-        final Object[] state = threadLocalMapState.get();
-        final UnmodifiableArrayBackedMap modifiedMap =
-                UnmodifiableArrayBackedMap.getInstance(state).copyAndPut(key, 
value);
-        threadLocalMapState.set(modifiedMap.getBackingArray());
-    }
-
-    public void putAll(final Map<String, String> m) {
-        final Object[] state = threadLocalMapState.get();
-        final UnmodifiableArrayBackedMap modifiedMap =
-                UnmodifiableArrayBackedMap.getInstance(state).copyAndPutAll(m);
-        threadLocalMapState.set(modifiedMap.getBackingArray());
-    }
-
-    @Override
-    public String get(final String key) {
-        final Object[] state = threadLocalMapState.get();
-        if (state == null) {
-            return null;
-        }
-        return UnmodifiableArrayBackedMap.getInstance(state).get(key);
-    }
-
-    @Override
-    public void remove(final String key) {
-        final Object[] state = threadLocalMapState.get();
-        if (state != null) {
-            final UnmodifiableArrayBackedMap modifiedMap =
-                    
UnmodifiableArrayBackedMap.getInstance(state).copyAndRemove(key);
-            threadLocalMapState.set(modifiedMap.getBackingArray());
-        }
-    }
-
-    public void removeAll(final Iterable<String> keys) {
-        final Object[] state = threadLocalMapState.get();
-        if (state != null) {
-            final UnmodifiableArrayBackedMap modifiedMap =
-                    
UnmodifiableArrayBackedMap.getInstance(state).copyAndRemoveAll(keys);
-            threadLocalMapState.set(modifiedMap.getBackingArray());
-        }
-    }
-
-    @Override
-    public void clear() {
-        threadLocalMapState.remove();
-    }
-
-    @Override
-    public Map<String, String> toMap() {
-        return getCopy();
-    }
-
-    @Override
-    public boolean containsKey(final String key) {
-        final Object[] state = threadLocalMapState.get();
-        return (state == null ? false : 
(UnmodifiableArrayBackedMap.getInstance(state)).containsKey(key));
-    }
-
-    @Override
-    public <V> void forEach(final BiConsumer<String, ? super V> action) {
-        final Object[] state = threadLocalMapState.get();
-        if (state == null) {
-            return;
-        }
-        final UnmodifiableArrayBackedMap map = 
UnmodifiableArrayBackedMap.getInstance(state);
-        map.forEach(action);
-    }
-
-    @Override
-    public <V, S> void forEach(final TriConsumer<String, ? super V, S> action, 
final S state) {
-        final Object[] localState = threadLocalMapState.get();
-        if (localState == null) {
-            return;
-        }
-        final UnmodifiableArrayBackedMap map = 
UnmodifiableArrayBackedMap.getInstance(localState);
-        map.forEach(action, state);
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    public <V> V getValue(final String key) {
-        return (V) get(key);
-    }
-
-    @Override
-    public Map<String, String> getCopy() {
-        final Object[] state = threadLocalMapState.get();
-        if (state == null) {
-            return new HashMap<>(0);
-        }
-        return new HashMap<>(UnmodifiableArrayBackedMap.getInstance(state));
-    }
-
-    @Override
-    public Map<String, String> getImmutableMapOrNull() {
-        final Object[] state = threadLocalMapState.get();
-        return (state == null ? null : 
UnmodifiableArrayBackedMap.getInstance(state));
-    }
-
-    @Override
-    public boolean isEmpty() {
-        return (size() == 0);
-    }
-
-    @Override
-    public int size() {
-        final Object[] state = threadLocalMapState.get();
-        return UnmodifiableArrayBackedMap.getInstance(state).size();
-    }
-
-    @Override
-    public String toString() {
-        final Object[] state = threadLocalMapState.get();
-        return state == null
-                ? "{}"
-                : UnmodifiableArrayBackedMap.getInstance(state).toString();
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        final Object[] state = threadLocalMapState.get();
-        result = prime * result
-                + ((state == null)
-                        ? 0
-                        : 
UnmodifiableArrayBackedMap.getInstance(state).hashCode());
-        return result;
-    }
-
-    @Override
-    public boolean equals(final Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (!(obj instanceof ThreadContextMap)) {
-            return false;
-        }
-        final ThreadContextMap other = (ThreadContextMap) obj;
-        final Map<String, String> map = 
UnmodifiableArrayBackedMap.getInstance(this.threadLocalMapState.get());
-        final Map<String, String> otherMap = other.getImmutableMapOrNull();
-        return Objects.equals(map, otherMap);
-    }
-}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java
index fb11083338..f5aa892729 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/UnmodifiableArrayBackedMap.java
@@ -57,7 +57,7 @@ import org.apache.logging.log4j.util.TriConsumer;
  * </ul>
  *
  */
-class UnmodifiableArrayBackedMap extends AbstractMap<String, String> 
implements Serializable, ReadOnlyStringMap {
+public class UnmodifiableArrayBackedMap extends AbstractMap<String, String> 
implements Serializable, ReadOnlyStringMap {
     /**
      * Implementation of Map.Entry. The implementation is simple since each 
instance
      * contains an index in the array, then getKey() and getValue() retrieve 
from
@@ -160,7 +160,7 @@ class UnmodifiableArrayBackedMap extends 
AbstractMap<String, String> implements
         return 2 * entryIndex + 1 + NUM_FIXED_ARRAY_ENTRIES;
     }
 
-    static UnmodifiableArrayBackedMap getInstance(Object[] backingArray) {
+    public static UnmodifiableArrayBackedMap getMap(Object[] backingArray) {
         if (backingArray == null || backingArray.length == 1) {
             return EMPTY_MAP;
         } else {
@@ -224,7 +224,7 @@ class UnmodifiableArrayBackedMap extends 
AbstractMap<String, String> implements
         return false;
     }
 
-    Object[] getBackingArray() {
+    public Object[] getBackingArray() {
         return backingArray;
     }
 
@@ -255,7 +255,7 @@ class UnmodifiableArrayBackedMap extends 
AbstractMap<String, String> implements
      * @param value
      * @return
      */
-    UnmodifiableArrayBackedMap copyAndPut(String key, String value) {
+    public UnmodifiableArrayBackedMap copyAndPut(String key, String value) {
         UnmodifiableArrayBackedMap newMap = new 
UnmodifiableArrayBackedMap(numEntries + 1);
         // include the numEntries value (array index 0)
         if (this.numEntries > 0) {
@@ -275,7 +275,7 @@ class UnmodifiableArrayBackedMap extends 
AbstractMap<String, String> implements
      * @param value
      * @return
      */
-    UnmodifiableArrayBackedMap copyAndPutAll(Map<String, String> entriesToAdd) 
{
+    public UnmodifiableArrayBackedMap copyAndPutAll(Map<String, String> 
entriesToAdd) {
         // create a new array that can hold the maximum output size
         UnmodifiableArrayBackedMap newMap = new 
UnmodifiableArrayBackedMap(numEntries + entriesToAdd.size());
 
@@ -309,7 +309,7 @@ class UnmodifiableArrayBackedMap extends 
AbstractMap<String, String> implements
      * @param value
      * @return
      */
-    UnmodifiableArrayBackedMap copyAndRemove(String key) {
+    public UnmodifiableArrayBackedMap copyAndRemove(String key) {
         int indexToRemove = -1;
         for (int oldIndex = 0; oldIndex < numEntries; oldIndex++) {
             if (backingArray[getArrayIndexForKey(oldIndex)].hashCode() == 
key.hashCode()
@@ -356,7 +356,7 @@ class UnmodifiableArrayBackedMap extends 
AbstractMap<String, String> implements
      * @param value
      * @return
      */
-    UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable<String> 
keysToRemoveIterable) {
+    public UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable<String> 
keysToRemoveIterable) {
         if (isEmpty()) {
             // shortcut: if this map is empty, the result will continue to be 
empty
             return EMPTY_MAP;
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..31e46c86d6 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
@@ -16,7 +16,8 @@
  */
 package org.apache.logging.log4j.spi;
 
-import java.util.Collections;
+import static 
org.apache.logging.log4j.internal.map.UnmodifiableArrayBackedMap.getMap;
+
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -26,13 +27,19 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.TriConsumer;
 
 /**
- * The actual ThreadContext Map. A new ThreadContext Map is created each time 
it is updated and the Map stored is always
- * immutable. This means the Map can be passed to other threads without 
concern that it will be updated. Since it is
- * expected that the Map will be passed to many more log events than the 
number of keys it contains the performance
- * should be much better than if the Map was copied for each event.
+ * The default implementation of {@link ThreadContextMap}
+ * <p>
+ *      An instance of UnmodifiableArrayBackedMap can be represented as a 
single {@code Object[]), which can safely
+ *      be stored on the {@code ThreadLocal) with no fear of 
classloader-related memory leaks.
+ *  </p>
+ *  <p>
+ *      Performance of the underlying {@link 
org.apache.logging.log4j.internal.map.UnmodifiableArrayBackedMap} exceeds
+ *      {@link HashMap} in all supported operations other than {@code get()}. 
Note that {@code get()} performance scales
+ *      linearly with the current map size, and callers are advised to 
minimize this work.
+ * </p>
  */
 public class DefaultThreadContextMap implements ThreadContextMap, 
ReadOnlyStringMap {
-    private static final long serialVersionUID = 8218007901108944053L;
+    private static final long serialVersionUID = -2635197170958057849L;
 
     /**
      * Property name ({@value} ) for selecting {@code InheritableThreadLocal} 
(value "true") or plain
@@ -40,88 +47,66 @@ public class DefaultThreadContextMap implements 
ThreadContextMap, ReadOnlyString
      */
     public static final String INHERITABLE_MAP = 
"isThreadContextMapInheritable";
 
-    private final boolean useMap;
-    private final ThreadLocal<Map<String, String>> localMap;
+    private ThreadLocal<Object[]> localState;
 
     public DefaultThreadContextMap() {
-        this(true);
+        this(PropertiesUtil.getProperties());
     }
 
     /**
      * @deprecated Since 2.24.0. See {@link Provider#getThreadContextMap()} on 
how to obtain a no-op map.
      */
     @Deprecated
-    public DefaultThreadContextMap(final boolean useMap) {
-        this(useMap, PropertiesUtil.getProperties());
+    public DefaultThreadContextMap(final boolean ignored) {
+        this(PropertiesUtil.getProperties());
     }
 
-    DefaultThreadContextMap(final boolean useMap, final PropertiesUtil 
properties) {
-        this.useMap = useMap;
-        localMap = properties.getBooleanProperty(INHERITABLE_MAP)
-                ? new InheritableThreadLocal<Map<String, String>>() {
+    DefaultThreadContextMap(final PropertiesUtil properties) {
+        localState = properties.getBooleanProperty(INHERITABLE_MAP)
+                ? new InheritableThreadLocal<Object[]>() {
                     @Override
-                    protected Map<String, String> childValue(final Map<String, 
String> parentValue) {
-                        return parentValue != null && useMap
-                                ? Collections.unmodifiableMap(new 
HashMap<>(parentValue))
-                                : null;
+                    protected Object[] childValue(final Object[] parentValue) {
+                        return parentValue;
                     }
                 }
-                : new ThreadLocal<Map<String, String>>();
+                : new ThreadLocal<>();
     }
 
     @Override
     public void put(final String key, final String value) {
-        if (!useMap) {
-            return;
-        }
-        Map<String, String> map = localMap.get();
-        map = map == null ? new HashMap<>(1) : new HashMap<>(map);
-        map.put(key, value);
-        localMap.set(Collections.unmodifiableMap(map));
+        final Object[] state = localState.get();
+        localState.set(getMap(state).copyAndPut(key, value).getBackingArray());
     }
 
     public void putAll(final Map<String, String> m) {
-        if (!useMap) {
-            return;
-        }
-        Map<String, String> map = localMap.get();
-        map = map == null ? new HashMap<>(m.size()) : new HashMap<>(map);
-        for (final Map.Entry<String, String> e : m.entrySet()) {
-            map.put(e.getKey(), e.getValue());
-        }
-        localMap.set(Collections.unmodifiableMap(map));
+        final Object[] state = localState.get();
+        localState.set(getMap(state).copyAndPutAll(m).getBackingArray());
     }
 
     @Override
     public String get(final String key) {
-        final Map<String, String> map = localMap.get();
-        return map == null ? null : map.get(key);
+        final Object[] state = localState.get();
+        return state == null ? null : getMap(state).get(key);
     }
 
     @Override
     public void remove(final String key) {
-        final Map<String, String> map = localMap.get();
-        if (map != null) {
-            final Map<String, String> copy = new HashMap<>(map);
-            copy.remove(key);
-            localMap.set(Collections.unmodifiableMap(copy));
+        final Object[] state = localState.get();
+        if (state != null) {
+            localState.set(getMap(state).copyAndRemove(key).getBackingArray());
         }
     }
 
     public void removeAll(final Iterable<String> keys) {
-        final Map<String, String> map = localMap.get();
-        if (map != null) {
-            final Map<String, String> copy = new HashMap<>(map);
-            for (final String key : keys) {
-                copy.remove(key);
-            }
-            localMap.set(Collections.unmodifiableMap(copy));
+        final Object[] state = localState.get();
+        if (state != null) {
+            
localState.set(getMap(state).copyAndRemoveAll(keys).getBackingArray());
         }
     }
 
     @Override
     public void clear() {
-        localMap.remove();
+        localState.remove();
     }
 
     @Override
@@ -131,81 +116,72 @@ public class DefaultThreadContextMap implements 
ThreadContextMap, ReadOnlyString
 
     @Override
     public boolean containsKey(final String key) {
-        final Map<String, String> map = localMap.get();
-        return map != null && map.containsKey(key);
+        final Object[] state = localState.get();
+        return state != null && getMap(state).containsKey(key);
     }
 
     @Override
     public <V> void forEach(final BiConsumer<String, ? super V> action) {
-        final Map<String, String> map = localMap.get();
-        if (map == null) {
+        final Object[] state = localState.get();
+        if (state == null) {
             return;
         }
-        for (final Map.Entry<String, String> entry : map.entrySet()) {
-            // BiConsumer should be able to handle values of any type V. In 
our case the values are of type String.
-            @SuppressWarnings("unchecked")
-            final V value = (V) entry.getValue();
-            action.accept(entry.getKey(), value);
-        }
+        getMap(state).forEach(action);
     }
 
     @Override
     public <V, S> void forEach(final TriConsumer<String, ? super V, S> action, 
final S state) {
-        final Map<String, String> map = localMap.get();
-        if (map == null) {
+        final Object[] localState = this.localState.get();
+        if (localState == null) {
             return;
         }
-        for (final Map.Entry<String, String> entry : map.entrySet()) {
-            // TriConsumer should be able to handle values of any type V. In 
our case the values are of type String.
-            @SuppressWarnings("unchecked")
-            final V value = (V) entry.getValue();
-            action.accept(entry.getKey(), value, state);
-        }
+        getMap(localState).forEach(action, state);
     }
 
     @SuppressWarnings("unchecked")
     @Override
     public <V> V getValue(final String key) {
-        final Map<String, String> map = localMap.get();
-        return (V) (map == null ? null : map.get(key));
+        return (V) get(key);
     }
 
     @Override
     public Map<String, String> getCopy() {
-        final Map<String, String> map = localMap.get();
-        return map == null ? new HashMap<>() : new HashMap<>(map);
+        final Object[] state = localState.get();
+        if (state == null) {
+            return new HashMap<>(0);
+        }
+        return new HashMap<>(getMap(state));
     }
 
     @Override
     public Map<String, String> getImmutableMapOrNull() {
-        return localMap.get();
+        final Object[] state = localState.get();
+        return (state == null ? null : getMap(state));
     }
 
     @Override
     public boolean isEmpty() {
-        final Map<String, String> map = localMap.get();
-        return map == null || map.isEmpty();
+        return size() == 0;
     }
 
     @Override
     public int size() {
-        final Map<String, String> map = localMap.get();
-        return map == null ? 0 : map.size();
+        final Object[] state = localState.get();
+        return getMap(state).size();
     }
 
     @Override
     public String toString() {
-        final Map<String, String> map = localMap.get();
-        return map == null ? "{}" : map.toString();
+        final Object[] state = localState.get();
+        return state == null ? "{}" : getMap(state).toString();
     }
 
     @Override
     public int hashCode() {
         final int prime = 31;
         int result = 1;
-        final Map<String, String> map = this.localMap.get();
-        result = prime * result + ((map == null) ? 0 : map.hashCode());
-        result = prime * result + Boolean.valueOf(this.useMap).hashCode();
+        final Object[] state = localState.get();
+        result = prime * result + ((state == null) ? 0 : 
getMap(state).hashCode());
         return result;
     }
 
@@ -217,17 +193,11 @@ public class DefaultThreadContextMap implements 
ThreadContextMap, ReadOnlyString
         if (obj == null) {
             return false;
         }
-        if (obj instanceof DefaultThreadContextMap) {
-            final DefaultThreadContextMap other = (DefaultThreadContextMap) 
obj;
-            if (this.useMap != other.useMap) {
-                return false;
-            }
-        }
         if (!(obj instanceof ThreadContextMap)) {
             return false;
         }
         final ThreadContextMap other = (ThreadContextMap) obj;
-        final Map<String, String> map = this.localMap.get();
+        final Map<String, String> map = getMap(localState.get());
         final Map<String, String> otherMap = other.getImmutableMapOrNull();
         return Objects.equals(map, otherMap);
     }
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
index e79626f25a..6de97d6235 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
@@ -18,17 +18,23 @@ package org.apache.logging.log4j.spi;
 
 import java.util.HashMap;
 import java.util.Map;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
 
 /**
  * {@code ThreadContextMap} implementation used when either of system 
properties {@code disableThreadContextMap} or .
  * {@code disableThreadContext} is {@code true}. This implementation does 
nothing.
  *
  * @since 2.7
- * @deprecated since 2.24.0. Return the {@value Provider#NO_OP_CONTEXT_MAP} 
constant in
- * {@link Provider#getThreadContextMap()} instead.
  */
-@Deprecated
+@NullMarked
 public class NoOpThreadContextMap implements ThreadContextMap {
+
+    /**
+     * @since 2.24.0
+     */
+    public static final ThreadContextMap INSTANCE = new NoOpThreadContextMap();
+
     @Override
     public void clear() {}
 
@@ -38,7 +44,7 @@ public class NoOpThreadContextMap implements ThreadContextMap 
{
     }
 
     @Override
-    public String get(final String key) {
+    public @Nullable String get(final String key) {
         return null;
     }
 
@@ -48,7 +54,7 @@ public class NoOpThreadContextMap implements ThreadContextMap 
{
     }
 
     @Override
-    public Map<String, String> getImmutableMapOrNull() {
+    public @Nullable Map<String, String> getImmutableMapOrNull() {
         return null;
     }
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java
index 1a56596035..9a79024f70 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java
@@ -23,10 +23,10 @@ import java.util.Properties;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.simple.SimpleLoggerContextFactory;
 import org.apache.logging.log4j.status.StatusLogger;
-import org.apache.logging.log4j.util.Constants;
-import org.apache.logging.log4j.util.Lazy;
 import org.apache.logging.log4j.util.LoaderUtil;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Service class used to bind the Log4j API with an implementation.
@@ -39,6 +39,7 @@ import org.apache.logging.log4j.util.PropertiesUtil;
  *     be dropped in a future version.
  * </p>
  */
+@NullMarked
 public class Provider {
     /**
      * Constant inlined by the compiler
@@ -72,75 +73,27 @@ public class Provider {
      */
     public static final String PROVIDER_PROPERTY_NAME = "log4j.provider";
 
-    /**
-     * Constant used to disable the {@link ThreadContextMap}.
-     * <p>
-     *     <strong>Warning:</strong> the value of this constant does not point 
to a concrete class name.
-     * </p>
-     * @see #getThreadContextMap
-     */
-    protected static final String NO_OP_CONTEXT_MAP = "NoOp";
-
-    /**
-     * Constant used to select a web application-safe implementation of {@link 
ThreadContextMap}.
-     * <p>
-     *     This implementation only binds JRE classes to {@link ThreadLocal} 
variables.
-     * </p>
-     * <p>
-     *     <strong>Warning:</strong> the value of this constant does not point 
to a concrete class name.
-     * </p>
-     * @see #getThreadContextMap
-     */
-    protected static final String WEB_APP_CONTEXT_MAP = "WebApp";
-
-    /**
-     * Constant used to select a copy-on-write implementation of {@link 
ThreadContextMap}.
-     * <p>
-     *     <strong>Warning:</strong> the value of this constant does not point 
to a concrete class name.
-     * </p>
-     * @see #getThreadContextMap
-     */
-    protected static final String COPY_ON_WRITE_CONTEXT_MAP = "CopyOnWrite";
-
-    /**
-     * Constant used to select a garbage-free implementation of {@link 
ThreadContextMap}.
-     * <p>
-     *     This implementation must ensure that common operations don't create 
new object instances. The drawback is
-     *     the necessity to bind custom classes to {@link ThreadLocal} 
variables.
-     * </p>
-     * <p>
-     *     <strong>Warning:</strong> the value of this constant does not point 
to a concrete class name.
-     * </p>
-     * @see #getThreadContextMap
-     */
-    protected static final String GARBAGE_FREE_CONTEXT_MAP = "GarbageFree";
-
-    // Property keys relevant for context map selection
     private static final String DISABLE_CONTEXT_MAP = 
"log4j2.disableThreadContextMap";
     private static final String DISABLE_THREAD_CONTEXT = 
"log4j2.disableThreadContext";
-    private static final String THREAD_CONTEXT_MAP_PROPERTY = 
"log4j2.threadContextMap";
-    private static final String GC_FREE_THREAD_CONTEXT_PROPERTY = 
"log4j2.garbagefree.threadContextMap";
 
-    private static final Integer DEFAULT_PRIORITY = -1;
+    private static final int DEFAULT_PRIORITY = -1;
     private static final Logger LOGGER = StatusLogger.getLogger();
 
-    private final Integer priority;
+    private final int priority;
     // LoggerContextFactory
     @Deprecated
-    private final String className;
+    private final @Nullable String className;
 
-    private final Class<? extends LoggerContextFactory> 
loggerContextFactoryClass;
-    private final Lazy<LoggerContextFactory> loggerContextFactoryLazy = 
Lazy.lazy(this::createLoggerContextFactory);
+    private final @Nullable Class<? extends LoggerContextFactory> 
loggerContextFactoryClass;
     // ThreadContextMap
     @Deprecated
-    private final String threadContextMap;
+    private final @Nullable String threadContextMap;
 
-    private final Class<? extends ThreadContextMap> threadContextMapClass;
-    private final Lazy<ThreadContextMap> threadContextMapLazy = 
Lazy.lazy(this::createThreadContextMap);
-    private final String versions;
+    private final @Nullable Class<? extends ThreadContextMap> 
threadContextMapClass;
+    private final @Nullable String versions;
 
     @Deprecated
-    private final URL url;
+    private final @Nullable URL url;
 
     @Deprecated
     private final WeakReference<ClassLoader> classLoader;
@@ -154,7 +107,7 @@ public class Provider {
         this.url = url;
         this.classLoader = new WeakReference<>(classLoader);
         final String weight = props.getProperty(FACTORY_PRIORITY);
-        priority = weight == null ? DEFAULT_PRIORITY : Integer.valueOf(weight);
+        priority = weight == null ? DEFAULT_PRIORITY : 
Integer.parseInt(weight);
         className = props.getProperty(LOGGER_CONTEXT_FACTORY);
         threadContextMap = props.getProperty(THREAD_CONTEXT_MAP);
         loggerContextFactoryClass = null;
@@ -167,7 +120,7 @@ public class Provider {
      * @param versions Minimal API version required, should be set to {@link 
#CURRENT_VERSION}.
      * @since 2.24.0
      */
-    public Provider(final Integer priority, final String versions) {
+    public Provider(final @Nullable Integer priority, final String versions) {
         this(priority, versions, null, null);
     }
 
@@ -175,12 +128,12 @@ public class Provider {
      * @param priority A positive number specifying the provider's priority or 
{@code null} if default,
      * @param versions Minimal API version required, should be set to {@link 
#CURRENT_VERSION},
      * @param loggerContextFactoryClass A public exported implementation of 
{@link LoggerContextFactory} or {@code
-     * null} if {@link #createLoggerContextFactory()} is also implemented.
+     * null} if {@link #getLoggerContextFactory()} is also implemented.
      */
     public Provider(
-            final Integer priority,
+            final @Nullable Integer priority,
             final String versions,
-            final Class<? extends LoggerContextFactory> 
loggerContextFactoryClass) {
+            final @Nullable Class<? extends LoggerContextFactory> 
loggerContextFactoryClass) {
         this(priority, versions, loggerContextFactoryClass, null);
     }
 
@@ -188,15 +141,15 @@ public class Provider {
      * @param priority A positive number specifying the provider's priority or 
{@code null} if default,
      * @param versions Minimal API version required, should be set to {@link 
#CURRENT_VERSION},
      * @param loggerContextFactoryClass A public exported implementation of 
{@link LoggerContextFactory} or {@code
-     * null} if {@link #createLoggerContextFactory()} is also implemented,
+     * null} if {@link #getLoggerContextFactory()} is also implemented,
      * @param threadContextMapClass A public exported implementation of {@link 
ThreadContextMap} or {@code null} if
-     * {@link #createThreadContextMap()} is implemented.
+     * {@link #getThreadContextMapInstance()} is implemented.
      */
     public Provider(
-            final Integer priority,
+            final @Nullable Integer priority,
             final String versions,
-            final Class<? extends LoggerContextFactory> 
loggerContextFactoryClass,
-            final Class<? extends ThreadContextMap> threadContextMapClass) {
+            final @Nullable Class<? extends LoggerContextFactory> 
loggerContextFactoryClass,
+            final @Nullable Class<? extends ThreadContextMap> 
threadContextMapClass) {
         this.priority = priority != null ? priority : DEFAULT_PRIORITY;
         this.versions = versions;
         this.loggerContextFactoryClass = loggerContextFactoryClass;
@@ -213,7 +166,7 @@ public class Provider {
      * @return A String containing the Log4j versions supported.
      */
     public String getVersions() {
-        return versions;
+        return versions != null ? versions : "";
     }
 
     /**
@@ -233,7 +186,7 @@ public class Provider {
      * @return the class name of a LoggerContextFactory implementation or 
{@code null} if unspecified.
      * @see #loadLoggerContextFactory()
      */
-    public String getClassName() {
+    public @Nullable String getClassName() {
         return loggerContextFactoryClass != null ? 
loggerContextFactoryClass.getName() : className;
     }
 
@@ -241,9 +194,8 @@ public class Provider {
      * Loads the {@link LoggerContextFactory} class specified by this Provider.
      *
      * @return the LoggerContextFactory implementation class or {@code null} 
if unspecified or a loader error occurred.
-     * @see #createLoggerContextFactory()
      */
-    public Class<? extends LoggerContextFactory> loadLoggerContextFactory() {
+    public @Nullable Class<? extends LoggerContextFactory> 
loadLoggerContextFactory() {
         if (loggerContextFactoryClass != null) {
             return loggerContextFactoryClass;
         }
@@ -271,65 +223,30 @@ public class Provider {
         return null;
     }
 
-    private LoggerContextFactory createLoggerContextFactory() {
-        final Class<? extends LoggerContextFactory> factoryClass = 
loadLoggerContextFactory();
-        if (factoryClass != null) {
-            try {
-                return LoaderUtil.newInstanceOf(factoryClass);
-            } catch (final Exception e) {
-                LOGGER.error(
-                        "Unable to create instance of class {} specified in 
{}", factoryClass.getName(), getUrl(), e);
-            }
-        }
-        LOGGER.warn("Falling back to {}", SimpleLoggerContextFactory.INSTANCE);
-        return SimpleLoggerContextFactory.INSTANCE;
-    }
-
     /**
      * @return The logger context factory to be used by {@link 
org.apache.logging.log4j.LogManager}.
      * @since 2.24.0
      */
     public LoggerContextFactory getLoggerContextFactory() {
-        return loggerContextFactoryLazy.get();
+        final Class<?> implementation = loadLoggerContextFactory();
+        if (implementation != null) {
+            try {
+                return 
LoaderUtil.newInstanceOf(implementation.asSubclass(LoggerContextFactory.class));
+            } catch (final ReflectiveOperationException e) {
+                LOGGER.error("Failed to instantiate logger context factory 
{}.", implementation.getName(), e);
+            }
+        }
+        LOGGER.error("Falling back to simple logger context factory: {}", 
SimpleLoggerContextFactory.class.getName());
+        return SimpleLoggerContextFactory.INSTANCE;
     }
 
     /**
-     * Gets the class name of the {@link ThreadContextMap} implementation of 
this Provider.
-     * <p>
-     *     This method should return one of the internal implementations:
-     *     <ol>
-     *         <li>{@code null} if {@link #loadThreadContextMap} is 
implemented,</li>
-     *         <li>{@link #NO_OP_CONTEXT_MAP},</li>
-     *         <li>{@link #WEB_APP_CONTEXT_MAP},</li>
-     *         <li>{@link #COPY_ON_WRITE_CONTEXT_MAP},</li>
-     *         <li>{@link #GARBAGE_FREE_CONTEXT_MAP}.</li>
-     *     </ol>
-     * </p>
+     * Gets the class name of the {@link 
org.apache.logging.log4j.spi.ThreadContextMap} implementation of this Provider.
+     *
      * @return the class name of a ThreadContextMap implementation
-     * @see #loadThreadContextMap()
      */
-    public String getThreadContextMap() {
-        if (threadContextMapClass != null) {
-            return threadContextMapClass.getName();
-        }
-        // Field value
-        if (threadContextMap != null) {
-            return threadContextMap;
-        }
-        // Properties
-        final PropertiesUtil props = PropertiesUtil.getProperties();
-        if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || 
props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) {
-            return NO_OP_CONTEXT_MAP;
-        }
-        final String threadContextMapClass = 
props.getStringProperty(THREAD_CONTEXT_MAP_PROPERTY);
-        if (threadContextMapClass != null) {
-            return threadContextMapClass;
-        }
-        // Default based on properties
-        if (props.getBooleanProperty(GC_FREE_THREAD_CONTEXT_PROPERTY)) {
-            return GARBAGE_FREE_CONTEXT_MAP;
-        }
-        return Constants.ENABLE_THREADLOCALS ? COPY_ON_WRITE_CONTEXT_MAP : 
WEB_APP_CONTEXT_MAP;
+    public @Nullable String getThreadContextMap() {
+        return threadContextMapClass != null ? threadContextMapClass.getName() 
: threadContextMap;
     }
 
     /**
@@ -337,17 +254,18 @@ public class Provider {
      *
      * @return the {@code ThreadContextMap} implementation class or {@code 
null} if unspecified or a loading error
      * occurred.
-     * @see #createThreadContextMap()
      */
-    public Class<? extends ThreadContextMap> loadThreadContextMap() {
+    public @Nullable Class<? extends ThreadContextMap> loadThreadContextMap() {
         if (threadContextMapClass != null) {
             return threadContextMapClass;
         }
-        final String threadContextMap = getThreadContextMap();
+        if (threadContextMap == null) {
+            return null;
+        }
         final ClassLoader loader = classLoader.get();
         // Support for deprecated {@code META-INF/log4j-provider.properties} 
format.
         // In the remaining cases {@code loader == null}.
-        if (loader == null || threadContextMap == null) {
+        if (loader == null) {
             return null;
         }
         try {
@@ -358,78 +276,34 @@ public class Provider {
                 LOGGER.error(
                         "Class {} specified in {} does not extend {}",
                         threadContextMap,
-                        getUrl(),
+                        url,
                         ThreadContextMap.class.getName());
             }
         } catch (final Exception e) {
-            LOGGER.error("Unable to load class {} specified in {}", 
threadContextMap, url.toString(), e);
+            LOGGER.error("Unable to load class {} specified in {}", 
threadContextMap, url, e);
         }
         return null;
     }
 
     /**
-     * Creates a {@link ThreadContextMap} using the legacy {@link 
#loadThreadContextMap()} and
-     * {@link #getThreadContextMap()} methods:
-     * <ol>
-     *     <li>calls {@link #loadThreadContextMap},</li>
-     *     <li>if the previous call returns {@code null}, it calls {@link 
#getThreadContextMap} to instantiate one of
-     *     the internal implementations,</li>
-     *     <li>it returns a no-op map otherwise.</li>
-     * </ol>
+     * @return The thread context map to be used by {@link 
org.apache.logging.log4j.ThreadContext}.
+     * @since 2.24.0
      */
-    @SuppressWarnings("deprecation")
-    ThreadContextMap createThreadContextMap() {
-        final Class<? extends ThreadContextMap> threadContextMapClass = 
loadThreadContextMap();
-        if (threadContextMapClass != null) {
-            try {
-                return LoaderUtil.newInstanceOf(threadContextMapClass);
-            } catch (final Exception e) {
-                LOGGER.error(
-                        "Unable to create instance of class {} specified in 
{}",
-                        threadContextMapClass.getName(),
-                        getUrl(),
-                        e);
-            }
+    public ThreadContextMap getThreadContextMapInstance() {
+        final PropertiesUtil props = PropertiesUtil.getProperties();
+        if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || 
props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) {
+            return NoOpThreadContextMap.INSTANCE;
         }
-        // Standard Log4j API implementations are internal and can be only 
specified by name:
         final String threadContextMap = getThreadContextMap();
         if (threadContextMap != null) {
-            /*
-             * The constructors are called explicitly to improve GraalVM 
support.
-             *
-             * The class names of the package-private implementations from 
version 2.23.1 must be recognized even
-             * if the class is moved.
-             */
             switch (threadContextMap) {
-                case NO_OP_CONTEXT_MAP:
-                case "org.apache.logging.log4j.spi.NoOpThreadContextMap":
-                    return new NoOpThreadContextMap();
-                case WEB_APP_CONTEXT_MAP:
-                case "org.apache.logging.log4j.spi.DefaultThreadContextMap":
-                    return new DefaultThreadContextMap();
-                case GARBAGE_FREE_CONTEXT_MAP:
                 case 
"org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap":
                     return new GarbageFreeSortedArrayThreadContextMap();
-                case COPY_ON_WRITE_CONTEXT_MAP:
                 case 
"org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap":
                     return new CopyOnWriteSortedArrayThreadContextMap();
             }
         }
-        LOGGER.warn("Falling back to {}", 
NoOpThreadContextMap.class.getName());
-        return new NoOpThreadContextMap();
-    }
-
-    // Used for testing
-    void resetThreadContextMap() {
-        threadContextMapLazy.set(null);
-    }
-
-    /**
-     * @return The thread context map to be used by {@link 
org.apache.logging.log4j.ThreadContext}.
-     * @since 2.24.0
-     */
-    public ThreadContextMap getThreadContextMapInstance() {
-        return threadContextMapLazy.get();
+        return new DefaultThreadContextMap();
     }
 
     /**
@@ -447,7 +321,7 @@ public class Provider {
      * @deprecated since 2.24.0, without replacement.
      */
     @Deprecated
-    public URL getUrl() {
+    public @Nullable URL getUrl() {
         return url;
     }
 
@@ -455,7 +329,7 @@ public class Provider {
     public String toString() {
         final StringBuilder result =
                 new StringBuilder("Provider 
'").append(getClass().getName()).append("'");
-        if (!DEFAULT_PRIORITY.equals(priority)) {
+        if (priority != DEFAULT_PRIORITY) {
             result.append("\n\tpriority = ").append(priority);
         }
         final String threadContextMap = getThreadContextMap();
@@ -485,24 +359,18 @@ public class Provider {
         if (this == o) {
             return true;
         }
-        if (!(o instanceof Provider)) {
-            return false;
+        if (o instanceof Provider) {
+            final Provider provider = (Provider) o;
+            return Objects.equals(priority, provider.priority)
+                    && Objects.equals(className, provider.className)
+                    && Objects.equals(loggerContextFactoryClass, 
provider.loggerContextFactoryClass)
+                    && Objects.equals(versions, provider.versions);
         }
-
-        final Provider provider = (Provider) o;
-
-        return Objects.equals(priority, provider.priority)
-                && Objects.equals(className, provider.className)
-                && Objects.equals(loggerContextFactoryClass, 
provider.loggerContextFactoryClass)
-                && Objects.equals(versions, provider.versions);
+        return false;
     }
 
     @Override
     public int hashCode() {
-        int result = priority != null ? priority.hashCode() : 0;
-        result = 31 * result + (className != null ? className.hashCode() : 0);
-        result = 31 * result + (loggerContextFactoryClass != null ? 
loggerContextFactoryClass.hashCode() : 0);
-        result = 31 * result + (versions != null ? versions.hashCode() : 0);
-        return result;
+        return Objects.hash(priority, className, loggerContextFactoryClass, 
versions);
     }
 }
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
index b00bd97985..418beb4583 100644
--- 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
@@ -48,12 +48,12 @@ public final class ThreadContextMapFactory {
      * and when Log4j is reconfigured.
      */
     public static void init() {
-        ProviderUtil.getProvider().resetThreadContextMap();
+        ProviderUtil.getProvider().getThreadContextMapInstance();
     }
 
     private ThreadContextMapFactory() {}
 
     public static ThreadContextMap createThreadContextMap() {
-        return ProviderUtil.getProvider().createThreadContextMap();
+        return ProviderUtil.getProvider().getThreadContextMapInstance();
     }
 }
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java
index a98bce9fdc..20f6791a2f 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AbstractAsyncThreadContextTestBase.java
@@ -26,24 +26,26 @@ import java.nio.file.Path;
 import java.util.concurrent.TimeUnit;
 import java.util.function.LongSupplier;
 import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.ThreadContextTestAccess;
 import org.apache.logging.log4j.core.config.ConfigurationFactory;
 import org.apache.logging.log4j.core.config.LoggerConfig;
 import org.apache.logging.log4j.core.impl.Log4jContextFactory;
+import org.apache.logging.log4j.core.impl.ThreadContextTestAccess;
 import org.apache.logging.log4j.core.jmx.RingBufferAdmin;
 import org.apache.logging.log4j.core.selector.ClassLoaderContextSelector;
 import org.apache.logging.log4j.core.selector.ContextSelector;
 import org.apache.logging.log4j.core.test.CoreLoggerContexts;
-import org.apache.logging.log4j.spi.DefaultThreadContextMap;
 import org.apache.logging.log4j.spi.LoggerContext;
 import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
+import org.apache.logging.log4j.spi.ThreadContextMap;
 import org.apache.logging.log4j.test.TestProperties;
 import org.apache.logging.log4j.test.junit.Log4jStaticResources;
 import org.apache.logging.log4j.test.junit.UsingStatusListener;
 import org.apache.logging.log4j.test.junit.UsingTestProperties;
+import org.apache.logging.log4j.util.ProviderUtil;
 import org.apache.logging.log4j.util.Unbox;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.parallel.ResourceLock;
@@ -89,26 +91,29 @@ public abstract class AbstractAsyncThreadContextTestBase {
     }
 
     protected enum ContextImpl {
-        WEBAPP,
-        GARBAGE_FREE,
-        COPY_ON_WRITE;
+        WEBAPP("WebApp", 
"org.apache.logging.log4j.spi.DefaultThreadContextMap"),
+        GARBAGE_FREE("GarbageFree", 
"org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap"),
+        COPY_ON_WRITE("CopyOnWrite", 
"org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap");
+
+        private final String threadContextMap;
+        private final String implClass;
+
+        ContextImpl(final String threadContextMap, final String implClass) {
+            this.threadContextMap = threadContextMap;
+            this.implClass = implClass;
+        }
 
         void init() {
-            final String PACKAGE = "org.apache.logging.log4j.spi.";
-            props.setProperty("log4j2.threadContextMap", PACKAGE + 
implClassSimpleName());
+            props.setProperty("log4j2.threadContextMap", threadContextMap);
             ThreadContextTestAccess.init();
         }
 
-        public String implClassSimpleName() {
-            switch (this) {
-                case WEBAPP:
-                    return DefaultThreadContextMap.class.getSimpleName();
-                case GARBAGE_FREE:
-                    return "GarbageFreeSortedArrayThreadContextMap";
-                case COPY_ON_WRITE:
-                    return "CopyOnWriteSortedArrayThreadContextMap";
-            }
-            throw new IllegalStateException("Unknown state " + this);
+        public String getImplClassSimpleName() {
+            return StringUtils.substringAfterLast(implClass, '.');
+        }
+
+        public String getImplClass() {
+            return implClass;
         }
     }
 
@@ -116,16 +121,12 @@ public abstract class AbstractAsyncThreadContextTestBase {
         asyncMode.initSelector();
         asyncMode.initConfigFile();
 
-        contextImpl.init();
         // Verify that we are using the requested context map
-        if (contextImpl == ContextImpl.WEBAPP) {
-            assertThat(ThreadContext.getThreadContextMap()).isNull();
-        } else {
-            assertThat(ThreadContext.getThreadContextMap())
-                    .isNotNull()
-                    .extracting(o -> o.getClass().getSimpleName())
-                    .isEqualTo(contextImpl.implClassSimpleName());
-        }
+        contextImpl.init();
+        final ThreadContextMap threadContextMap = 
ProviderUtil.getProvider().getThreadContextMapInstance();
+        assertThat(threadContextMap.getClass().getName())
+                .as("Check `ThreadContextMap` implementation")
+                .isEqualTo(contextImpl.getImplClass());
     }
 
     private LongSupplier remainingCapacity(final LoggerContext loggerContext, 
final LoggerConfig loggerConfig) {
@@ -197,25 +198,25 @@ public abstract class AbstractAsyncThreadContextTestBase {
     private static String contextMap() {
         final ReadOnlyThreadContextMap impl = 
ThreadContext.getThreadContextMap();
         return impl == null
-                ? ContextImpl.WEBAPP.implClassSimpleName()
+                ? ContextImpl.WEBAPP.getImplClassSimpleName()
                 : impl.getClass().getSimpleName();
     }
 
     private void checkResult(final Path file, final String loggerContextName, 
final ContextImpl contextImpl)
             throws IOException {
-        final String contextDesc = contextImpl + " " + 
contextImpl.implClassSimpleName() + " " + loggerContextName;
+        final String contextDesc = contextImpl + " " + 
contextImpl.getImplClassSimpleName() + " " + loggerContextName;
         try (final BufferedReader reader = Files.newBufferedReader(file)) {
             String expect;
             for (int i = 0; i < LINE_COUNT; i++) {
                 final String line = reader.readLine();
                 if ((i & 1) == 1) {
-                    expect =
-                            "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, 
configProp=configValue, configProp2=configValue2, count="
-                                    + i + "} " + contextDesc + " i=" + i;
+                    expect = "INFO c.f.Bar mapvalue [stackvalue] 
{KEY=mapvalue, configProp=configValue,"
+                            + " configProp2=configValue2, count="
+                            + i + "} " + contextDesc + " i=" + i;
                 } else {
-                    expect =
-                            "INFO c.f.Bar mapvalue [stackvalue] {KEY=mapvalue, 
configProp=configValue, configProp2=configValue2} "
-                                    + contextDesc + " i=" + i;
+                    expect = "INFO c.f.Bar mapvalue [stackvalue] 
{KEY=mapvalue, configProp=configValue,"
+                            + " configProp2=configValue2} "
+                            + contextDesc + " i=" + i;
                 }
                 assertThat(line).as("Log file '%s'", 
file.getFileName()).isEqualTo(expect);
             }
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
index 9465460823..c94e5e1143 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
@@ -18,7 +18,6 @@ package org.apache.logging.log4j.core.impl;
 
 import static java.util.Arrays.asList;
 import static java.util.concurrent.Executors.newSingleThreadExecutor;
-import static org.apache.logging.log4j.ThreadContext.getThreadContextMap;
 import static 
org.apache.logging.log4j.core.impl.ContextDataInjectorFactory.createInjector;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.allOf;
@@ -33,9 +32,9 @@ import java.util.Collection;
 import java.util.concurrent.ExecutionException;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.ContextDataInjector;
-import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
-import org.apache.logging.log4j.test.ThreadContextUtilityClass;
+import org.apache.logging.log4j.spi.ThreadContextMap;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.ProviderUtil;
 import org.apache.logging.log4j.util.SortedArrayStringMap;
 import org.apache.logging.log4j.util.StringMap;
 import org.junit.After;
@@ -51,26 +50,24 @@ public class ThreadContextDataInjectorTest {
     @Parameters(name = "{0}")
     public static Collection<String[]> threadContextMapClassNames() {
         return asList(new String[][] {
-            {
-                
"org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap",
-                
"org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap"
-            },
-            {
-                
"org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap",
-                
"org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap"
-            },
-            {"org.apache.logging.log4j.spi.DefaultThreadContextMap", null}
+            
{"org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap"},
+            
{"org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap"},
+            {"org.apache.logging.log4j.spi.DefaultThreadContextMap"}
         });
     }
 
     @Parameter
     public String threadContextMapClassName;
 
-    @Parameter(value = 1)
-    public String readOnlythreadContextMapClassName;
+    private static void resetThreadContextMap() {
+        PropertiesUtil.getProperties().reload();
+        final Log4jProvider provider = (Log4jProvider) 
ProviderUtil.getProvider();
+        provider.resetThreadContextMap();
+        ThreadContext.init();
+    }
 
     @Before
-    public void before() {
+    public void before() throws ReflectiveOperationException {
         System.setProperty("log4j2.threadContextMap", 
threadContextMapClassName);
     }
 
@@ -83,13 +80,11 @@ public class ThreadContextDataInjectorTest {
     }
 
     private void testContextDataInjector() {
-        final ReadOnlyThreadContextMap readOnlythreadContextMap = 
getThreadContextMap();
+        final ThreadContextMap threadContextMap = 
ProviderUtil.getProvider().getThreadContextMapInstance();
         assertThat(
                 "thread context map class name",
-                (readOnlythreadContextMap == null)
-                        ? null
-                        : readOnlythreadContextMap.getClass().getName(),
-                is(equalTo(readOnlythreadContextMapClassName)));
+                threadContextMap.getClass().getName(),
+                is(equalTo(threadContextMapClassName)));
 
         final ContextDataInjector contextDataInjector = createInjector(true);
         final StringMap stringMap = 
contextDataInjector.injectContextData(null, new SortedArrayStringMap());
@@ -122,9 +117,8 @@ public class ThreadContextDataInjectorTest {
 
     private void prepareThreadContext(final boolean 
isThreadContextMapInheritable) {
         System.setProperty("log4j2.isThreadContextMapInheritable", 
Boolean.toString(isThreadContextMapInheritable));
-        PropertiesUtil.getProperties().reload();
-        ThreadContextUtilityClass.reset();
-        ThreadContext.remove("baz");
+        resetThreadContextMap();
+        ThreadContext.clearMap();
         ThreadContext.put("foo", "bar");
     }
 
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextTestAccess.java
similarity index 80%
rename from 
log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java
rename to 
log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextTestAccess.java
index ecf3e9f6c6..661023a46f 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/ThreadContextTestAccess.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextTestAccess.java
@@ -14,7 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j;
+package org.apache.logging.log4j.core.impl;
+
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.util.ProviderUtil;
 
 /**
  * <p>
@@ -29,6 +32,8 @@ public final class ThreadContextTestAccess {
     }
 
     public static void init() {
+        final Log4jProvider provider = (Log4jProvider) 
ProviderUtil.getProvider();
+        provider.resetThreadContextMap();
         ThreadContext.init();
     }
 }
diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml
index 05a6de655d..1d376c994b 100644
--- a/log4j-core/pom.xml
+++ b/log4j-core/pom.xml
@@ -53,6 +53,8 @@
       -->
     <bnd-multi-release>true</bnd-multi-release>
     <bnd-extra-package-options>
+      <!-- Annotations only -->
+      org.jspecify.*;resolution:=optional,
       <!-- External optional dependencies -->
       com.conversantmedia.util.concurrent;resolution:=optional;
       com.fasterxml.jackson.*;resolution:=optional,
@@ -122,6 +124,11 @@
       <scope>provided</scope>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>org.jspecify</groupId>
+      <artifactId>jspecify</artifactId>
+      <scope>provided</scope>
+    </dependency>
     <!-- Used for OSGi bundle support -->
     <dependency>
       <groupId>org.osgi</groupId>
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java
index 31d71cb5f4..222eafc858 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java
@@ -19,16 +19,183 @@ package org.apache.logging.log4j.core.impl;
 import aQute.bnd.annotation.Resolution;
 import aQute.bnd.annotation.spi.ServiceConsumer;
 import aQute.bnd.annotation.spi.ServiceProvider;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.spi.DefaultThreadContextMap;
+import org.apache.logging.log4j.spi.LoggerContextFactory;
+import org.apache.logging.log4j.spi.NoOpThreadContextMap;
 import org.apache.logging.log4j.spi.Provider;
 import org.apache.logging.log4j.spi.ScopedContextProvider;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Constants;
+import org.apache.logging.log4j.util.Lazy;
+import org.apache.logging.log4j.util.LoaderUtil;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Binding for the Log4j API.
  */
 @ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL)
 @ServiceConsumer(value = ScopedContextProvider.class, resolution = 
Resolution.OPTIONAL)
+@NullMarked
 public class Log4jProvider extends Provider {
+
+    /**
+     * Constant used to disable the {@link ThreadContextMap}.
+     * <p>
+     *     <strong>Warning:</strong> the value of this constant does not point 
to a concrete class name.
+     * </p>
+     * @see #getThreadContextMap
+     */
+    private static final String NO_OP_CONTEXT_MAP = "NoOp";
+
+    /**
+     * Constant used to select a web application-safe implementation of {@link 
ThreadContextMap}.
+     * <p>
+     *     This implementation only binds JRE classes to {@link ThreadLocal} 
variables.
+     * </p>
+     * <p>
+     *     <strong>Warning:</strong> the value of this constant does not point 
to a concrete class name.
+     * </p>
+     * @see #getThreadContextMap
+     */
+    private static final String WEB_APP_CONTEXT_MAP = "WebApp";
+
+    /**
+     * Constant used to select a copy-on-write implementation of {@link 
ThreadContextMap}.
+     * <p>
+     *     <strong>Warning:</strong> the value of this constant does not point 
to a concrete class name.
+     * </p>
+     * @see #getThreadContextMap
+     */
+    private static final String COPY_ON_WRITE_CONTEXT_MAP = "CopyOnWrite";
+
+    /**
+     * Constant used to select a garbage-free implementation of {@link 
ThreadContextMap}.
+     * <p>
+     *     This implementation must ensure that common operations don't create 
new object instances. The drawback is
+     *     the necessity to bind custom classes to {@link ThreadLocal} 
variables.
+     * </p>
+     * <p>
+     *     <strong>Warning:</strong> the value of this constant does not point 
to a concrete class name.
+     * </p>
+     * @see #getThreadContextMap
+     */
+    private static final String GARBAGE_FREE_CONTEXT_MAP = "GarbageFree";
+
+    // Property keys relevant for context map selection
+    private static final String DISABLE_CONTEXT_MAP = 
"log4j2.disableThreadContextMap";
+    private static final String DISABLE_THREAD_CONTEXT = 
"log4j2.disableThreadContext";
+    private static final String THREAD_CONTEXT_MAP_PROPERTY = 
"log4j2.threadContextMap";
+    private static final String GC_FREE_THREAD_CONTEXT_PROPERTY = 
"log4j2.garbagefree.threadContextMap";
+
+    // Name of the context map implementations
+    private static final String WEB_APP_CLASS_NAME = 
"org.apache.logging.log4j.spi.DefaultThreadContextMap";
+    private static final String GARBAGE_FREE_CLASS_NAME =
+            
"org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap";
+    private static final String COPY_ON_WRITE_CLASS_NAME =
+            
"org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap";
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    private final Lazy<LoggerContextFactory> loggerContextFactoryLazy = 
Lazy.lazy(Log4jContextFactory::new);
+    private final Lazy<ThreadContextMap> threadContextMapLazy = 
Lazy.lazy(this::createThreadContextMap);
+
     public Log4jProvider() {
         super(10, CURRENT_VERSION, Log4jContextFactory.class);
     }
+
+    @Override
+    public LoggerContextFactory getLoggerContextFactory() {
+        return loggerContextFactoryLazy.get();
+    }
+
+    @Override
+    public ThreadContextMap getThreadContextMapInstance() {
+        return threadContextMapLazy.get();
+    }
+
+    private ThreadContextMap createThreadContextMap() {
+        // Properties
+        final PropertiesUtil props = PropertiesUtil.getProperties();
+        if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || 
props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) {
+            return NoOpThreadContextMap.INSTANCE;
+        }
+        String threadContextMapClass = 
props.getStringProperty(THREAD_CONTEXT_MAP_PROPERTY);
+        // Default based on properties
+        if (threadContextMapClass == null) {
+            if (props.getBooleanProperty(GC_FREE_THREAD_CONTEXT_PROPERTY)) {
+                threadContextMapClass = GARBAGE_FREE_CONTEXT_MAP;
+            } else if (Constants.ENABLE_THREADLOCALS) {
+                threadContextMapClass = COPY_ON_WRITE_CONTEXT_MAP;
+            } else {
+                threadContextMapClass = WEB_APP_CONTEXT_MAP;
+            }
+        }
+        /*
+         * The constructors are called explicitly to improve GraalVM support.
+         *
+         * The class names of the package-private implementations from version 
2.23.1 must be recognized even
+         * if the class is moved.
+         */
+        switch (threadContextMapClass) {
+            case NO_OP_CONTEXT_MAP:
+                return NoOpThreadContextMap.INSTANCE;
+            case WEB_APP_CONTEXT_MAP:
+            case WEB_APP_CLASS_NAME:
+                return new DefaultThreadContextMap();
+            case GARBAGE_FREE_CONTEXT_MAP:
+            case 
"org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap":
+            case COPY_ON_WRITE_CONTEXT_MAP:
+            case 
"org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap":
+                return super.getThreadContextMapInstance();
+            default:
+                try {
+                    return 
LoaderUtil.newCheckedInstanceOf(threadContextMapClass, ThreadContextMap.class);
+                } catch (final Exception e) {
+                    LOGGER.error("Unable to create instance of class {}.", 
threadContextMapClass, e);
+                }
+        }
+        LOGGER.warn("Falling back to {}.", 
NoOpThreadContextMap.class.getName());
+        return NoOpThreadContextMap.INSTANCE;
+    }
+
+    @Override
+    public @Nullable String getThreadContextMap() {
+        // Properties
+        final PropertiesUtil props = PropertiesUtil.getProperties();
+        if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || 
props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) {
+            return NoOpThreadContextMap.class.getName();
+        }
+        String threadContextMapClass = 
props.getStringProperty(THREAD_CONTEXT_MAP_PROPERTY);
+        // Default based on properties
+        if (threadContextMapClass == null) {
+            if (props.getBooleanProperty(GC_FREE_THREAD_CONTEXT_PROPERTY)) {
+                threadContextMapClass = GARBAGE_FREE_CONTEXT_MAP;
+            } else if (Constants.ENABLE_THREADLOCALS) {
+                threadContextMapClass = COPY_ON_WRITE_CONTEXT_MAP;
+            } else {
+                threadContextMapClass = WEB_APP_CONTEXT_MAP;
+            }
+        }
+        switch (threadContextMapClass) {
+            case NO_OP_CONTEXT_MAP:
+                return NoOpThreadContextMap.class.getName();
+            case WEB_APP_CONTEXT_MAP:
+                return DefaultThreadContextMap.class.getName();
+            case GARBAGE_FREE_CONTEXT_MAP:
+                return GARBAGE_FREE_CLASS_NAME;
+            case COPY_ON_WRITE_CONTEXT_MAP:
+                return COPY_ON_WRITE_CLASS_NAME;
+            default:
+                return threadContextMapClass;
+        }
+    }
+
+    // Used in tests
+    void resetThreadContextMap() {
+        threadContextMapLazy.set(null);
+    }
 }
diff --git a/log4j-osgi-test/pom.xml b/log4j-osgi-test/pom.xml
index d8e241ce79..60930eb317 100644
--- a/log4j-osgi-test/pom.xml
+++ b/log4j-osgi-test/pom.xml
@@ -203,7 +203,7 @@
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>
           <classpathDependencyExcludes>
-            <exclude>org.osgi:org.osgi.framework</exclude>
+            <exclude>org.osgi:org.osgi.core</exclude>
           </classpathDependencyExcludes>
           <systemPropertyVariables>
             <!-- PAX logging has a copy of Log4j2 API-->
diff --git 
a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextVsScopedContextBenchmark.java
 
b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextVsScopedContextBenchmark.java
index c510e4f10c..76ac3d4d82 100644
--- 
a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextVsScopedContextBenchmark.java
+++ 
b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextVsScopedContextBenchmark.java
@@ -30,7 +30,6 @@ import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.LoggerConfig;
 import org.apache.logging.log4j.core.layout.PatternLayout;
-import org.apache.logging.log4j.internal.map.StringArrayThreadContextMap;
 import org.apache.logging.log4j.perf.appender.StringAppender;
 import org.apache.logging.log4j.spi.CopyOnWriteOpenHashMapThreadContextMap;
 import org.apache.logging.log4j.spi.DefaultThreadContextMap;
@@ -92,7 +91,6 @@ public class ThreadContextVsScopedContextBenchmark {
     private static final Logger LOGGER = 
LogManager.getLogger(ThreadContextVsScopedContextBenchmark.class);
 
     private static final String DEFAULT_CONTEXT_MAP = "Default";
-    private static final String STRING_ARRAY_MAP = "StringArray";
     private static final String COPY_OPENHASH_MAP = "CopyOpenHash";
     private static final String COPY_ARRAY_MAP = "CopySortedArray";
     private static final String NO_GC_OPENHASH_MAP = "NoGcOpenHash";
@@ -101,7 +99,6 @@ public class ThreadContextVsScopedContextBenchmark {
 
     static {
         IMPLEMENTATIONS.put(DEFAULT_CONTEXT_MAP, 
DefaultThreadContextMap.class);
-        IMPLEMENTATIONS.put(STRING_ARRAY_MAP, 
StringArrayThreadContextMap.class);
         IMPLEMENTATIONS.put(COPY_OPENHASH_MAP, 
CopyOnWriteOpenHashMapThreadContextMap.class);
         IMPLEMENTATIONS.put(
                 COPY_ARRAY_MAP,
diff --git a/log4j-to-jul/pom.xml b/log4j-to-jul/pom.xml
index 0c4babfe54..25caed1639 100644
--- a/log4j-to-jul/pom.xml
+++ b/log4j-to-jul/pom.xml
@@ -28,7 +28,20 @@
   <name>Apache Log4j to JUL Bridge</name>
   <description>The Apache Log4j binding between Log4j 2 API and 
java.util.logging (JUL).</description>
   <inceptionYear>2022</inceptionYear>
+
+  <properties>
+    <bnd-extra-package-options>
+      <!-- Annotations only -->
+      org.jspecify.*;resolution:=optional
+    </bnd-extra-package-options>
+  </properties>
+
   <dependencies>
+    <dependency>
+      <groupId>org.jspecify</groupId>
+      <artifactId>jspecify</artifactId>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>org.osgi</groupId>
       <artifactId>org.osgi.core</artifactId>
diff --git 
a/log4j-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java 
b/log4j-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java
index 0f371f43ef..7497e484f8 100644
--- a/log4j-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java
+++ b/log4j-to-jul/src/main/java/org/apache/logging/log4j/tojul/JULProvider.java
@@ -19,19 +19,23 @@ package org.apache.logging.log4j.tojul;
 import aQute.bnd.annotation.Resolution;
 import aQute.bnd.annotation.spi.ServiceProvider;
 import org.apache.logging.log4j.spi.LoggerContextFactory;
+import org.apache.logging.log4j.spi.NoOpThreadContextMap;
 import org.apache.logging.log4j.spi.Provider;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.jspecify.annotations.NullMarked;
 
 /**
  * Bind the Log4j API to JUL.
  *
  * @author <a href="http://www.vorburger.ch";>Michael Vorburger.ch</a> for 
Google
  */
+@NullMarked
 @ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL)
 public class JULProvider extends Provider {
     private static final LoggerContextFactory CONTEXT_FACTORY = new 
JULLoggerContextFactory();
 
     public JULProvider() {
-        super(20, CURRENT_VERSION);
+        super(20, CURRENT_VERSION, JULLoggerContextFactory.class, 
NoOpThreadContextMap.class);
     }
 
     @Override
@@ -40,8 +44,8 @@ public class JULProvider extends Provider {
     }
 
     @Override
-    public String getThreadContextMap() {
+    public ThreadContextMap getThreadContextMapInstance() {
         // JUL does not provide an MDC implementation
-        return NO_OP_CONTEXT_MAP;
+        return NoOpThreadContextMap.INSTANCE;
     }
 }
diff --git a/log4j-to-slf4j/pom.xml b/log4j-to-slf4j/pom.xml
index fdcb4888a9..a983debe93 100644
--- a/log4j-to-slf4j/pom.xml
+++ b/log4j-to-slf4j/pom.xml
@@ -40,6 +40,8 @@
       -->
     <slf4j.support.range>[1.7,3)</slf4j.support.range>
     <bnd-extra-package-options>
+      <!-- Annotations only -->
+      org.jspecify.*;resolution:=optional,
       <!-- This bridge also support SLF4J 2.x -->
       org.slf4j.*;version="${slf4j.support.range}"
     </bnd-extra-package-options>
@@ -63,6 +65,11 @@
       <artifactId>org.osgi.core</artifactId>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>org.jspecify</groupId>
+      <artifactId>jspecify</artifactId>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
diff --git 
a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java 
b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
index 318d7937f6..52a11a5560 100644
--- a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
+++ b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
@@ -21,10 +21,12 @@ import aQute.bnd.annotation.spi.ServiceProvider;
 import org.apache.logging.log4j.spi.LoggerContextFactory;
 import org.apache.logging.log4j.spi.Provider;
 import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.jspecify.annotations.NullMarked;
 
 /**
  * Bind the Log4j API to SLF4J.
  */
+@NullMarked
 @ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL)
 public class SLF4JProvider extends Provider {
 
@@ -32,7 +34,7 @@ public class SLF4JProvider extends Provider {
     private static final ThreadContextMap THREAD_CONTEXT_MAP = new 
MDCContextMap();
 
     public SLF4JProvider() {
-        super(15, CURRENT_VERSION);
+        super(15, CURRENT_VERSION, SLF4JLoggerContextFactory.class, 
MDCContextMap.class);
     }
 
     @Override
diff --git a/src/changelog/.2.x.x/2330_add_faster_web_app_context_map.xml 
b/src/changelog/.2.x.x/2330_add_faster_web_app_context_map.xml
index 34c1bd653d..f3076419d0 100644
--- a/src/changelog/.2.x.x/2330_add_faster_web_app_context_map.xml
+++ b/src/changelog/.2.x.x/2330_add_faster_web_app_context_map.xml
@@ -4,5 +4,5 @@
        xsi:schemaLocation="https://logging.apache.org/xml/ns 
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
        type="added">
   <issue id="2330" link="https://github.com/apache/logging-log4j2/pull/2330"/>
-  <description format="asciidoc">Add a faster `ThreadContextMap` for web app 
users: 
`org.apache.logging.log4j.internal.map.StringArrayThreadContextMap`.</description>
+  <description format="asciidoc">Add a faster `DefaultThreadContextMap` 
implementation.</description>
 </entry>


Reply via email to