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

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

commit b26bb883a95491312de39009de5e4ebc954a05c5
Author: John Engebretson <[email protected]>
AuthorDate: Wed Feb 28 14:14:55 2024 +0000

    Creating faster ThreadContextMap based on a String[] in the ThreadLocal
---
 .../log4j/spi/StringArrayThreadContextMapTest.java | 265 ++++++++++++
 .../log4j/spi/UnmodifiableArrayBackedMapTest.java  | 308 +++++++++++++
 .../org/apache/logging/log4j/ThreadContext.java    |   9 +-
 .../log4j/spi/StringArrayThreadContextMap.java     | 253 +++++++++++
 .../logging/log4j/spi/ThreadContextMapFactory.java |   1 +
 .../log4j/spi/UnmodifiableArrayBackedMap.java      | 479 +++++++++++++++++++++
 .../org/apache/logging/log4j/spi/package-info.java |   2 +-
 7 files changed, 1314 insertions(+), 3 deletions(-)

diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java
new file mode 100644
index 0000000000..601927bf6c
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/StringArrayThreadContextMapTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.spi;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+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.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.ClearSystemProperty;
+
+/**
+ * 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();
+        assertEquals(null, map1.get("test"));
+        map1.put("test", "test");
+        assertEquals("test", map1.get("test"));
+        assertEquals(null, map1.get("not_present"));
+    }
+
+    @Test
+    public void testDoesNothingIfConstructedWithUseMapIsFalse() {
+        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap(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 StringArrayThreadContextMap map = new 
StringArrayThreadContextMap(true);
+        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(true);
+        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.spi.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"));
+    }
+
+    @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");
+    }
+
+    @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(true);
+        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(true);
+        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(true);
+        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(true);
+        assertTrue(map.isEmpty());
+        assertNull(map.getImmutableMapOrNull());
+    }
+
+    @Test
+    public void testGetImmutableMapReturnsImmutableMapIfNonEmpty() {
+        final StringArrayThreadContextMap map = new 
StringArrayThreadContextMap(true);
+        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(true);
+        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(true);
+        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
+    @ClearSystemProperty(key = StringArrayThreadContextMap.INHERITABLE_MAP)
+    @InitializesThreadContext
+    public void testThreadLocalNotInheritableByDefault() {
+        ThreadContextMapFactory.init();
+        final ThreadLocal<Object[]> threadLocal = 
StringArrayThreadContextMap.createThreadLocalMap(true);
+        assertFalse(threadLocal instanceof InheritableThreadLocal<?>);
+    }
+
+    @Test
+    @SetTestProperty(key = StringArrayThreadContextMap.INHERITABLE_MAP, value 
= "true")
+    @InitializesThreadContext
+    public void testThreadLocalInheritableIfConfigured() {
+        ThreadContextMapFactory.init();
+        try {
+            final ThreadLocal<Object[]> threadLocal = 
StringArrayThreadContextMap.createThreadLocalMap(true);
+            assertTrue(threadLocal instanceof InheritableThreadLocal<?>);
+        } finally {
+            System.clearProperty(StringArrayThreadContextMap.INHERITABLE_MAP);
+        }
+    }
+}
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java
new file mode 100644
index 0000000000..af8f2f85be
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMapTest.java
@@ -0,0 +1,308 @@
+/*
+ * 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.spi;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+public class UnmodifiableArrayBackedMapTest {
+    private static final int TEST_DATA_SIZE = 5;
+
+    private HashMap<String, String> getTestParameters() {
+        return getTestParameters(TEST_DATA_SIZE);
+    }
+
+    private HashMap<String, String> getTestParameters(int numParams) {
+        HashMap<String, String> params = new LinkedHashMap<>();
+        for (int i = 0; i < numParams; i++) {
+            params.put("" + i, "value" + i);
+        }
+
+        return params;
+    }
+
+    @Test
+    public void testReads() {
+        assertEquals(UnmodifiableArrayBackedMap.EMPTY_MAP.get("test"), null);
+        HashMap<String, String> params = getTestParameters();
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+        for (Map.Entry<String, String> entry : params.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue();
+            assertTrue(testMap.containsKey(key));
+            assertTrue(testMap.containsValue(value));
+            assertEquals(testMap.get(key), params.get(key));
+        }
+        assertFalse(testMap.containsKey("not_present"));
+        assertFalse(testMap.containsValue("not_present"));
+        assertEquals(null, testMap.get("not_present"));
+    }
+
+    @Test
+    public void testCopyAndRemoveAll() {
+        HashMap<String, String> initialMapContents = getTestParameters(15);
+        initialMapContents.put("extra_key", "extra_value");
+
+        HashSet<String> keysToRemove = new LinkedHashSet<>();
+        keysToRemove.add("3");
+        keysToRemove.add("11");
+        keysToRemove.add("definitely_not_found");
+
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(initialMapContents);
+        testMap = testMap.copyAndRemoveAll(keysToRemove);
+        assertEquals(14, testMap.size());
+
+        assertFalse(testMap.containsKey("3"));
+        assertFalse(testMap.containsValue("value3"));
+        assertFalse(testMap.containsKey("11"));
+        assertFalse(testMap.containsValue("value11"));
+
+        assertTrue(testMap.containsKey("extra_key"));
+        assertTrue(testMap.containsValue("extra_value"));
+        assertTrue(testMap.containsKey("1"));
+        assertTrue(testMap.containsValue("value1"));
+        assertTrue(testMap.containsKey("0"));
+        assertTrue(testMap.containsValue("value0"));
+        assertTrue(testMap.containsKey("14"));
+        assertTrue(testMap.containsValue("value14"));
+
+        testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(initialMapContents);
+        UnmodifiableArrayBackedMap testMapWithArrayListRemoval =
+                testMap.copyAndRemoveAll(new ArrayList<>(keysToRemove));
+        UnmodifiableArrayBackedMap testMapWithSetRemoval = 
testMap.copyAndRemoveAll(keysToRemove);
+        assertEquals(testMapWithSetRemoval, testMapWithArrayListRemoval);
+
+        testMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
+        
assertEquals(testMap.copyAndRemoveAll(initialMapContents.keySet()).size(), 0);
+
+        testMap = UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPut("test", 
"test");
+        
assertEquals(testMap.copyAndRemoveAll(initialMapContents.keySet()).size(), 1);
+        testMap = testMap.copyAndRemoveAll(Collections.singleton("not found"));
+        assertEquals(testMap.copyAndRemoveAll(testMap.keySet()).size(), 0);
+        testMap = testMap.copyAndRemoveAll(Collections.singleton("test"));
+        assertEquals(testMap.copyAndRemoveAll(testMap.keySet()).size(), 0);
+    }
+
+    @Test
+    public void testCopyAndPut() {
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP;
+        testMap = testMap.copyAndPut("1", "value1");
+        assertTrue(testMap.containsKey("1"));
+        assertEquals(testMap.get("1"), "value1");
+
+        testMap = testMap.copyAndPut("1", "another value");
+        assertTrue(testMap.containsKey("1"));
+        assertEquals(testMap.get("1"), "another value");
+
+        HashMap<String, String> newValues = getTestParameters();
+        testMap = testMap.copyAndPutAll(newValues);
+        assertEquals(testMap.get("1"), "value1");
+        assertEquals(testMap.get("4"), "value4");
+    }
+
+    @Test
+    public void testInstanceCopy() {
+        HashMap<String, String> params = getTestParameters();
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+
+        UnmodifiableArrayBackedMap testMap2 = new 
UnmodifiableArrayBackedMap(testMap);
+        assertEquals(testMap, testMap2);
+    }
+
+    @Test
+    public void testEntrySetIteratorAndSize() {
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
+        Set<Map.Entry<String, String>> entrySet = testMap.entrySet();
+        int numEntriesFound = 0;
+        for (@SuppressWarnings("unused") Map.Entry<String, String> entry : 
entrySet) {
+            numEntriesFound++;
+        }
+
+        assertEquals(testMap.size(), numEntriesFound);
+        assertEquals(testMap.size(), entrySet.size());
+    }
+
+    @Test
+    public void testEntrySetMutatorsBlocked() {
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
+        Set<Map.Entry<String, String>> entrySet = testMap.entrySet();
+        for (Map.Entry<String, String> entry : entrySet) {
+            try {
+                entry.setValue("test");
+                fail("Entry.setValue() wasn't blocked");
+            } catch (UnsupportedOperationException e) {
+            }
+        }
+        for (@SuppressWarnings("unused") Map.Entry<String, String> entry : 
entrySet) {
+            try {
+                entrySet.add(null);
+                fail("EntrySet.add() wasn't blocked");
+            } catch (UnsupportedOperationException e) {
+            }
+        }
+        for (@SuppressWarnings("unused") Map.Entry<String, String> entry : 
entrySet) {
+            try {
+                entrySet.addAll(new HashSet<>());
+                fail("EntrySet.addAll() wasn't blocked");
+            } catch (UnsupportedOperationException e) {
+            }
+        }
+    }
+
+    @Test
+    public void testNullValue() {
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP;
+        testMap = testMap.copyAndPut("key", null);
+        assertTrue(testMap.containsKey("key"));
+        assertTrue(testMap.containsValue(null));
+        assertTrue(testMap.size() == 1);
+        assertEquals(testMap.get("key"), null);
+    }
+
+    @Test
+    public void testMutatorsBlocked() {
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
+        try {
+            testMap.put("a", "a");
+            fail("put() wasn't blocked");
+        } catch (UnsupportedOperationException e) {
+        }
+
+        try {
+            testMap.putAll(new HashMap<>());
+            fail("putAll() wasn't blocked");
+        } catch (UnsupportedOperationException e) {
+        }
+
+        try {
+            testMap.remove("1");
+            fail("remove() wasn't blocked");
+        } catch (UnsupportedOperationException e) {
+        }
+
+        try {
+            testMap.clear();
+            fail("clear() wasn't blocked");
+        } catch (UnsupportedOperationException e) {
+        }
+    }
+
+    @Test
+    public void testCopyAndRemove() {
+        HashMap<String, String> params = getTestParameters();
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+        testMap = testMap.copyAndRemove("2");
+        testMap = testMap.copyAndRemove("not_present");
+        assertEquals(4, testMap.size());
+        assertFalse(testMap.containsKey("2"));
+        assertTrue(testMap.containsKey("1"));
+        assertFalse(testMap.containsValue("value2"));
+    }
+
+    /**
+     * Tests various situations with .equals(). Test tries comparisons in both
+     * directions, to make sure that 
HashMap.equals(UnmodifiableArrayBackedMap) work
+     * as well as UnmodifiableArrayBackedMap.equals(HashMap).
+     */
+    @Test
+    public void testEqualsHashCodeWithIdenticalContent() {
+        HashMap<String, String> params = getTestParameters();
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+        assertEquals(params, testMap);
+        assertEquals(testMap, params);
+        assertEquals(params.hashCode(), testMap.hashCode());
+    }
+
+    @Test
+    public void testEqualsWhenOneValueDiffers() {
+        HashMap<String, String> params = getTestParameters();
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+        assertNotEquals(params, testMap.copyAndPut("1", "different value"));
+        assertNotEquals(testMap.copyAndPut("1", "different value"), params);
+    }
+
+    @Test
+    public void testEqualsHashCodeWithOneKeyRemoved() {
+        HashMap<String, String> params = getTestParameters();
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+
+        params.remove("1");
+        assertNotEquals(params, testMap);
+        assertNotEquals(testMap, params);
+
+        testMap = testMap.copyAndRemove("1").copyAndRemove("2");
+        assertNotEquals(params, testMap);
+        assertNotEquals(testMap, params);
+    }
+
+    @Test
+    public void testEqualsHashCodeWithOneEmptyMap() {
+        HashMap<String, String> params = getTestParameters();
+        UnmodifiableArrayBackedMap testMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+        // verify empty maps are not equal to non-empty maps
+        assertNotEquals(params, UnmodifiableArrayBackedMap.EMPTY_MAP);
+        assertNotEquals(new HashMap<>(), testMap);
+        assertNotEquals(UnmodifiableArrayBackedMap.EMPTY_MAP, params);
+        assertNotEquals(testMap, new HashMap<>());
+    }
+
+    @Test
+    public void testImmutability() {
+        HashMap<String, String> params = getTestParameters();
+        UnmodifiableArrayBackedMap originalMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(params);
+        UnmodifiableArrayBackedMap modifiedMap = 
originalMap.copyAndPutAll(getTestParameters());
+        assertEquals(originalMap, params);
+
+        modifiedMap = modifiedMap.copyAndRemoveAll(modifiedMap.keySet());
+        assertTrue(modifiedMap.isEmpty());
+
+        assertEquals(originalMap, params);
+    }
+
+    @Test
+    public void testState() {
+        UnmodifiableArrayBackedMap originalMap;
+        UnmodifiableArrayBackedMap newMap;
+
+        originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP;
+        newMap = 
UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+        assertEquals(originalMap, newMap);
+
+        originalMap = 
UnmodifiableArrayBackedMap.EMPTY_MAP.copyAndPutAll(getTestParameters());
+        newMap = 
UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+        assertEquals(originalMap, newMap);
+
+        originalMap = UnmodifiableArrayBackedMap.EMPTY_MAP
+                .copyAndPutAll(getTestParameters())
+                .copyAndRemove("1");
+        newMap = 
UnmodifiableArrayBackedMap.getInstance(originalMap.getBackingArray());
+        assertEquals(originalMap, newMap);
+    }
+}
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 37a1c0d1fc..b885757287 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
@@ -30,6 +30,7 @@ import org.apache.logging.log4j.spi.DefaultThreadContextMap;
 import org.apache.logging.log4j.spi.DefaultThreadContextStack;
 import org.apache.logging.log4j.spi.NoOpThreadContextMap;
 import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
+import org.apache.logging.log4j.spi.StringArrayThreadContextMap;
 import org.apache.logging.log4j.spi.ThreadContextMap;
 import org.apache.logging.log4j.spi.ThreadContextMap2;
 import org.apache.logging.log4j.spi.ThreadContextMapFactory;
@@ -211,9 +212,9 @@ public final class ThreadContext {
         ThreadContextMapFactory.init();
         contextMap = null;
         final PropertiesUtil managerProps = PropertiesUtil.getProperties();
-        final boolean disableAll = 
managerProps.getBooleanProperty(DISABLE_ALL);
+        boolean disableAll = managerProps.getBooleanProperty(DISABLE_ALL);
         useStack = !(managerProps.getBooleanProperty(DISABLE_STACK) || 
disableAll);
-        final boolean useMap = !(managerProps.getBooleanProperty(DISABLE_MAP) 
|| disableAll);
+        boolean useMap = !(managerProps.getBooleanProperty(DISABLE_MAP) || 
disableAll);
 
         contextStack = new DefaultThreadContextStack(useStack);
         if (!useMap) {
@@ -275,6 +276,8 @@ 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());
@@ -317,6 +320,8 @@ 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/spi/StringArrayThreadContextMap.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/StringArrayThreadContextMap.java
new file mode 100644
index 0000000000..025a247aac
--- /dev/null
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/StringArrayThreadContextMap.java
@@ -0,0 +1,253 @@
+/*
+ * 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.spi;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.PropertiesUtil;
+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 final boolean useMap;
+    private final ThreadLocal<Object[]> threadLocalMapState;
+
+    private static boolean inheritableMap;
+
+    static {
+        init();
+    }
+
+    // LOG4J2-479: by default, use a plain ThreadLocal, only use 
InheritableThreadLocal if configured.
+    // (This method is package protected for JUnit tests.)
+    static ThreadLocal<Object[]> createThreadLocalMap(final boolean 
isMapEnabled) {
+        if (inheritableMap) {
+            return new InheritableThreadLocal<Object[]>() {
+                @Override
+                protected Object[] childValue(final Object[] parentValue) {
+                    return parentValue;
+                }
+            };
+        }
+        // if not inheritable, return plain ThreadLocal with null as initial 
value
+        return new ThreadLocal<>();
+    }
+
+    static void init() {
+        inheritableMap = 
PropertiesUtil.getProperties().getBooleanProperty(INHERITABLE_MAP);
+    }
+
+    public StringArrayThreadContextMap() {
+        this(true);
+    }
+
+    public StringArrayThreadContextMap(final boolean useMap) {
+        this.useMap = useMap;
+        this.threadLocalMapState = createThreadLocalMap(useMap);
+    }
+
+    @Override
+    public void put(final String key, final String value) {
+        if (!useMap) {
+            return;
+        }
+        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) {
+        if (!useMap) {
+            return;
+        }
+        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);
+        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);
+        }
+    }
+
+    @Override
+    public <V, S> void forEach(final TriConsumer<String, ? super V, S> action, 
final S state) {
+        Object[] localState = threadLocalMapState.get();
+        if (localState == null) {
+            return;
+        }
+        UnmodifiableArrayBackedMap map = 
UnmodifiableArrayBackedMap.getInstance(localState);
+        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);
+        }
+    }
+
+    @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());
+        result = prime * result + Boolean.valueOf(this.useMap).hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (obj instanceof StringArrayThreadContextMap) {
+            final StringArrayThreadContextMap other = 
(StringArrayThreadContextMap) obj;
+            if (this.useMap != other.useMap) {
+                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/spi/ThreadContextMapFactory.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
index 566ae1463c..14c0fe632a 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
@@ -70,6 +70,7 @@ public final class ThreadContextMapFactory {
         CopyOnWriteSortedArrayThreadContextMap.init();
         GarbageFreeSortedArrayThreadContextMap.init();
         DefaultThreadContextMap.init();
+        StringArrayThreadContextMap.init();
         initPrivate();
     }
 
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java
new file mode 100644
index 0000000000..b5d583cc68
--- /dev/null
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/UnmodifiableArrayBackedMap.java
@@ -0,0 +1,479 @@
+/*
+ * 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.spi;
+
+import java.io.Serializable;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This class represents an immutable map, which stores its state inside a 
single Object[]:
+ * <ol>
+ * <li>[0] contains the number of entries</li>
+ * <li>Others contain alternating key-value pairs, for example [1]="1" and 
[2]="value_for_1"</li>
+ * </ol>
+ *
+ * Keys are calculated using (index * 2 + 1) and values are (index * 2 + 2).
+ *
+ * Performance:
+ * <ul>
+ * <li>Implements very low-cost copies: shallow-copy the array.</li>
+ * <li>Doesn't matter for mutable operations, since we don't allow them.</li>
+ * <li>Iterates very quickly, since it iterates directly across the array. This
+ * contrasts with HashMap's requirement to scan each bucket in the table and
+ * chase each pointer.</li>
+ * <li>Is linear on gets, puts, and removes, since the table must be scanned to
+ * find a matching key.</li>
+ * </ul>
+ *
+ * Allocation:
+ * <ul>
+ * <li>Zero on reads.</li>
+ * <li>Copy-and-modify operations allocate exactly two objects: the new array
+ * and the new Map instance. This is substantially better than HashMap, which
+ * requires a new Node for each entry.</li>
+ * </ul>
+ *
+ */
+class UnmodifiableArrayBackedMap extends AbstractMap<String, String> 
implements Serializable {
+    /**
+     * Implementation of Map.Entry. The implementation is simple since each 
instance
+     * contains an index in the array, then getKey() and getValue() retrieve 
from
+     * the array. Blocks modifications.
+     */
+    private class UnmodifiableEntry implements Map.Entry<String, String> {
+        private final int index;
+
+        public UnmodifiableEntry(int index) {
+            this.index = index;
+        }
+
+        @Override
+        public String getKey() {
+            return (String) backingArray[getArrayIndexForKey(index)];
+        }
+
+        @Override
+        public String getValue() {
+            return (String) backingArray[getArrayIndexForValue(index)];
+        }
+
+        /**
+         * Per spec, the hashcode is a function of the key and value. 
Calculation
+         * exactly matches HashMap.
+         */
+        public int hashCode() {
+            String key = (String) backingArray[getArrayIndexForKey(index)];
+            String value = (String) backingArray[getArrayIndexForValue(index)];
+            return Objects.hashCode(key) ^ Objects.hashCode(value);
+        }
+
+        @Override
+        public String setValue(String value) {
+            throw new UnsupportedOperationException("Cannot update Entry 
instances in UnmodifiableArrayBackedMap");
+        }
+    }
+
+    /**
+     * Simple Entry iterator, tracking solely the index in the array. Blocks
+     * modifications.
+     */
+    private class UnmodifiableEntryIterator implements 
Iterator<Map.Entry<String, String>> {
+        private int index;
+
+        @Override
+        public boolean hasNext() {
+            return index < numEntries;
+        }
+
+        @Override
+        public Entry<String, String> next() {
+            return new UnmodifiableEntry(index++);
+        }
+    }
+
+    /**
+     * Simple Entry set, providing a reference to UnmodifiableEntryIterator and
+     * blocking modifications.
+     */
+    private class UnmodifiableEntrySet extends AbstractSet<Map.Entry<String, 
String>> {
+
+        @Override
+        public boolean add(Entry<String, String> e) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean addAll(Collection<? extends Entry<String, String>> c) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Iterator<Entry<String, String>> iterator() {
+            return new UnmodifiableEntryIterator();
+        }
+
+        @Override
+        public int size() {
+            return numEntries;
+        }
+    }
+
+    private static final long serialVersionUID = 6849423432534211514L;
+
+    public static final UnmodifiableArrayBackedMap EMPTY_MAP = new 
UnmodifiableArrayBackedMap(0);
+
+    private final Object[] backingArray;
+    private int numEntries;
+
+    private static final int NUM_FIXED_ARRAY_ENTRIES = 1;
+
+    private static int getArrayIndexForKey(int entryIndex) {
+        return 2 * entryIndex + NUM_FIXED_ARRAY_ENTRIES;
+    }
+
+    private static int getArrayIndexForValue(int entryIndex) {
+        return 2 * entryIndex + 1 + NUM_FIXED_ARRAY_ENTRIES;
+    }
+
+    private UnmodifiableArrayBackedMap(int capacity) {
+        this.backingArray = new Object[capacity * 2 + 1];
+        this.backingArray[0] = 0;
+    }
+
+    static UnmodifiableArrayBackedMap getInstance(Object[] backingArray) {
+        if (backingArray == null || backingArray.length == 1) {
+            return EMPTY_MAP;
+        } else {
+            return new UnmodifiableArrayBackedMap(backingArray);
+        }
+    }
+
+    private UnmodifiableArrayBackedMap(Object[] backingArray) {
+        this.numEntries = (backingArray == null ? 0 : (int) backingArray[0]);
+        this.backingArray = backingArray;
+    }
+
+    UnmodifiableArrayBackedMap(UnmodifiableArrayBackedMap other) {
+        this.backingArray = other.backingArray;
+        this.numEntries = other.numEntries;
+    }
+
+    private void add(String key, String value) {
+        backingArray[getArrayIndexForKey(numEntries)] = key;
+        backingArray[getArrayIndexForValue(numEntries)] = value;
+        numEntries++;
+    }
+
+    @Override
+    public void clear() {
+        throw new UnsupportedOperationException("Instance cannot be cleared, 
reuse EMPTY_MAP instead.");
+    }
+
+    /**
+     * Scans the array to find a matching key. Linear performance.
+     */
+    @Override
+    public boolean containsKey(Object key) {
+        int hashCode = key.hashCode();
+        for (int i = 0; i < numEntries; i++) {
+            if (backingArray[getArrayIndexForKey(i)].hashCode() == hashCode
+                    && backingArray[getArrayIndexForKey(i)].equals(key)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    Object[] getBackingArray() {
+        return backingArray;
+    }
+
+    /**
+     * Scans the array to find a matching value, with linear time. Allows null
+     * parameter.
+     */
+    @Override
+    public boolean containsValue(Object value) {
+        for (int i = 0; i < numEntries; i++) {
+            Object valueInMap = backingArray[getArrayIndexForValue(i)];
+            if (value == null) {
+                if (valueInMap == null) {
+                    return true;
+                }
+            } else if (value.equals(valueInMap)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Creates a new instance that contains the same entries as this map, plus
+     * either the new entry or updated value passed in the parameters.
+     *
+     * @param key
+     * @param value
+     * @return
+     */
+    UnmodifiableArrayBackedMap copyAndPut(String key, String value) {
+        UnmodifiableArrayBackedMap newMap = new 
UnmodifiableArrayBackedMap(numEntries + 1);
+        // include the numEntries value (array index 0)
+        if (this.numEntries > 0) {
+            System.arraycopy(this.backingArray, 1, newMap.backingArray, 1, 
numEntries * 2);
+            newMap.numEntries = numEntries;
+        }
+        newMap.addOrOverwriteKey(key, value);
+        newMap.updateNumEntriesInArray();
+        return newMap;
+    }
+
+    /**
+     * Creates a new instance that contains the same entries as this map, plus 
the
+     * new entries or updated values passed in the parameters.
+     *
+     * @param key
+     * @param value
+     * @return
+     */
+    UnmodifiableArrayBackedMap copyAndPutAll(Map<String, String> entriesToAdd) 
{
+        // create a new array that can hold the maximum output size
+        UnmodifiableArrayBackedMap newMap = new 
UnmodifiableArrayBackedMap(numEntries + entriesToAdd.size());
+
+        // copy the contents of the current map (if any)
+        if (numEntries > 0) {
+            System.arraycopy(backingArray, 0, newMap.backingArray, 0, 
numEntries * 2 + 1);
+        }
+
+        for (Map.Entry<String, String> entry : entriesToAdd.entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue();
+            if (!this.isEmpty()) {
+                // The unique elements passed in may overlap the unique 
elements here - must
+                // check
+                newMap.addOrOverwriteKey(key, value);
+            } else {
+                // There is no chance of overlapping keys, we can simply add
+                newMap.add(key, value);
+            }
+        }
+
+        newMap.updateNumEntriesInArray();
+        return newMap;
+    }
+
+    /**
+     * Creates a new instance that contains the same entries as this map, 
minus the
+     * entry with the specified key (if such an entry exists).
+     *
+     * @param key
+     * @param value
+     * @return
+     */
+    UnmodifiableArrayBackedMap copyAndRemove(String key) {
+        UnmodifiableArrayBackedMap newMap = new 
UnmodifiableArrayBackedMap(numEntries);
+        int indexToRemove = -1;
+        for (int oldIndex = 0; oldIndex < numEntries; oldIndex++) {
+            if (backingArray[getArrayIndexForKey(oldIndex)].hashCode() == 
key.hashCode()
+                    && 
backingArray[getArrayIndexForKey(oldIndex)].equals(key)) {
+                indexToRemove = oldIndex;
+                break;
+            }
+        }
+
+        if (indexToRemove == -1) {
+            // key not found, no change necessary
+            return this;
+        }
+        if (indexToRemove > 0) {
+            // copy entries before the removed one
+            System.arraycopy(backingArray, 1, newMap.backingArray, 1, 
indexToRemove * 2);
+        }
+        if (indexToRemove < (numEntries + 1)) {
+            // copy entries after the removed one
+            int nextIndexToCopy = indexToRemove + 1;
+            int numRemainingEntries = numEntries - nextIndexToCopy;
+            System.arraycopy(
+                    backingArray,
+                    getArrayIndexForKey(nextIndexToCopy),
+                    newMap.backingArray,
+                    getArrayIndexForKey(indexToRemove),
+                    numRemainingEntries * 2);
+        }
+
+        newMap.numEntries = numEntries - 1;
+        newMap.updateNumEntriesInArray();
+        return newMap;
+    }
+
+    /**
+     * Creates a new instance that contains the same entries as this map, 
minus all
+     * of the keys passed in the arguments.
+     *
+     * @param key
+     * @param value
+     * @return
+     */
+    UnmodifiableArrayBackedMap copyAndRemoveAll(Iterable<String> 
keysToRemoveIterable) {
+        if (isEmpty()) {
+            // shortcut: if this map is empty, the result will continue to be 
empty
+            return EMPTY_MAP;
+        }
+
+        // now we build a Set of keys to remove
+        Set<String> keysToRemoveSet;
+        if (keysToRemoveIterable instanceof Set) {
+            // we already have a set, let's cast it and reuse it
+            keysToRemoveSet = (Set<String>) keysToRemoveIterable;
+        } else {
+            // iterate through the keys and build a set
+            keysToRemoveSet = new HashSet<>();
+            for (String key : keysToRemoveIterable) {
+                keysToRemoveSet.add(key);
+            }
+        }
+
+        int firstIndexToKeep = -1;
+        int lastIndexToKeep = -1;
+        int destinationIndex = 0;
+        int numEntriesKept = 0;
+        // build the new map
+        UnmodifiableArrayBackedMap newMap = new 
UnmodifiableArrayBackedMap(numEntries);
+        for (int indexInCurrentMap = 0; indexInCurrentMap < numEntries; 
indexInCurrentMap++) {
+            // for each key in this map, check whether it's in the set we 
built above
+            Object key = backingArray[getArrayIndexForKey(indexInCurrentMap)];
+            if (!keysToRemoveSet.contains(key)) {
+                // this key should be kept
+                if (firstIndexToKeep == -1) {
+                    firstIndexToKeep = indexInCurrentMap;
+                }
+                lastIndexToKeep = indexInCurrentMap;
+            } else if (lastIndexToKeep > 0) {
+                // we hit a remove, copy any keys that are known ready
+                int numEntriesToCopy = lastIndexToKeep - firstIndexToKeep + 1;
+                System.arraycopy(
+                        backingArray,
+                        getArrayIndexForKey(firstIndexToKeep),
+                        newMap.backingArray,
+                        getArrayIndexForKey(destinationIndex),
+                        numEntriesToCopy * 2);
+                firstIndexToKeep = -1;
+                lastIndexToKeep = -1;
+                destinationIndex += numEntriesToCopy;
+                numEntriesKept += numEntriesToCopy;
+            }
+        }
+
+        if (lastIndexToKeep > -1) {
+            // at least one key still requires copying
+            int numEntriesToCopy = lastIndexToKeep - firstIndexToKeep + 1;
+            System.arraycopy(
+                    backingArray,
+                    getArrayIndexForKey(firstIndexToKeep),
+                    newMap.backingArray,
+                    getArrayIndexForKey(destinationIndex),
+                    numEntriesToCopy * 2);
+            numEntriesKept += numEntriesToCopy;
+        }
+
+        newMap.numEntries = numEntriesKept;
+        newMap.updateNumEntriesInArray();
+
+        return newMap;
+    }
+
+    /**
+     * Copies the locally-tracked numEntries into the first array slot. 
Requires
+     * autoboxing so call should be minimized - for example, once per bulk 
update
+     * operation.
+     */
+    private void updateNumEntriesInArray() {
+        backingArray[0] = numEntries;
+    }
+
+    @Override
+    public Set<Entry<String, String>> entrySet() {
+        return new UnmodifiableEntrySet();
+    }
+
+    /**
+     * Scans the array to find a matching key. Linear-time.
+     */
+    @Override
+    public String get(Object key) {
+        if (numEntries == 0) {
+            return null;
+        }
+        int hashCode = key.hashCode();
+        for (int i = 0; i < numEntries; i++) {
+            if (backingArray[getArrayIndexForKey(i)].hashCode() == hashCode
+                    && backingArray[getArrayIndexForKey(i)].equals(key)) {
+                return (String) backingArray[getArrayIndexForValue(i)];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Find an existing entry (if any) and overwrites the value, if found
+     *
+     * @param key
+     * @param value
+     * @return
+     */
+    private void addOrOverwriteKey(String key, String value) {
+        int keyHashCode = key.hashCode();
+        for (int i = 0; i < numEntries; i++) {
+            if (backingArray[getArrayIndexForKey(i)].hashCode() == keyHashCode
+                    && backingArray[getArrayIndexForKey(i)].equals(key)) {
+                // found a match, overwrite then return
+                backingArray[getArrayIndexForValue(i)] = value;
+                return;
+            }
+        }
+
+        // no match found, add to the end
+        add(key, value);
+    }
+
+    @Override
+    public String put(String key, String value) {
+        throw new UnsupportedOperationException("put() is not supported, use 
copyAndPut instead");
+    }
+
+    @Override
+    public void putAll(Map<? extends String, ? extends String> m) {
+        throw new UnsupportedOperationException("putAll() is not supported, 
use copyAndPutAll instead");
+    }
+
+    @Override
+    public String remove(Object key) {
+        throw new UnsupportedOperationException("remove() is not supported, 
use copyAndRemove instead");
+    }
+
+    @Override
+    public int size() {
+        return numEntries;
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java
index 22200e3206..8fff7b8097 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java
@@ -19,7 +19,7 @@
  * API classes.
  */
 @Export
-@Version("2.20.1")
+@Version("2.24.0")
 package org.apache.logging.log4j.spi;
 
 import org.osgi.annotation.bundle.Export;

Reply via email to