New ScopedContextMap implementation for better performance
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/ScopedContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/ScopedContextMap.java
index af02fcd..c50c0cf 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/ScopedContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/ScopedContextMap.java
@@ -19,41 +19,17 @@
 import java.io.Serializable;
 import java.util.AbstractMap;
 import java.util.AbstractSet;
+import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 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>
- *
+ * Multiple maps may share the same object array to avoid copying.
  */
 public class ScopedContextMap extends AbstractMap<String, Object> implements Serializable {
     /**
@@ -61,7 +37,7 @@
      * contains an index in the array, then getKey() and getValue() retrieve from
      * the array. Blocks modifications.
      */
-    private class UnmodifiableEntry implements Entry<String, Object> {
+    private class MapEntry implements Entry<String, Object> {
         /**
          * This field is functionally final, but marking it as such can cause
          * performance problems. Consider marking it final after
@@ -69,18 +45,37 @@
          */
         private int index;
 
-        public UnmodifiableEntry(int index) {
+        private String key;
+        private Object value;
+
+        public MapEntry(int index) {
             this.index = index;
         }
 
         @Override
         public String getKey() {
-            return (String) backingArray[getArrayIndexForKey(index)];
+            return key;
+        }
+
+        public void setKey(String key) {
+            this.key = key;
         }
 
         @Override
         public Object getValue() {
-            return backingArray[getArrayIndexForValue(index)];
+            return value;
+        }
+
+        @Override
+        public Object setValue(Object value) {
+            Object old = this.value;
+            this.value = value;
+            return old;
+        }
+
+        public void clear() {
+            this.key = null;
+            this.value = null;
         }
 
         /**
@@ -88,15 +83,8 @@
          * exactly matches HashMap.
          */
         public int hashCode() {
-            String key = (String) backingArray[getArrayIndexForKey(index)];
-            Object value = backingArray[getArrayIndexForValue(index)];
             return Objects.hashCode(key) ^ Objects.hashCode(value);
         }
-
-        @Override
-        public String setValue(Object value) {
-            throw new UnsupportedOperationException("Cannot update Entry instances in UnmodifiableArrayBackedMap");
-        }
     }
 
     /**
@@ -105,6 +93,8 @@
      */
     private class UnmodifiableEntryIterator implements Iterator<Entry<String, Object>> {
         private int index;
+        private Iterator<Integer> skip = skipList.iterator();
+        private Integer skipItem = skip.next();
 
         @Override
         public boolean hasNext() {
@@ -113,7 +103,11 @@
 
         @Override
         public Entry<String, Object> next() {
-            return new UnmodifiableEntry(index++);
+            while (skipItem != null && index < numEntries && skipItem == index) {
+                index++;
+                skipItem = skip.next();
+            }
+            return index < numEntries ? backingArray[index] : null;
         }
     }
 
@@ -140,38 +134,20 @@
 
         @Override
         public int size() {
-            return numEntries;
-        }
-    }
-
-    private static final long serialVersionUID = 6849423432534211514L;
-
-    public static final ScopedContextMap EMPTY_MAP = new ScopedContextMap(0);
-
-    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;
-    }
-
-    static ScopedContextMap getInstance(Object[] backingArray) {
-        if (backingArray == null || backingArray.length == 1) {
-            return EMPTY_MAP;
-        } else {
-            return new ScopedContextMap(backingArray);
+            return numEntries - skipList.size();
         }
     }
 
     public static ScopedContextMap getInstance(String key, Object value) {
-        ScopedContextMap newMap = new ScopedContextMap(1);
+        ScopedContextMap newMap = new ScopedContextMap(1, new ArrayList<>());
         newMap.add(key, value);
         return newMap;
     }
 
+    public static ScopedContextMap emptyMap() {
+        return new ScopedContextMap(0, new ArrayList<>());
+    }
+
     /**
      * Creates a new instance that contains the entries in the Map.
      *
@@ -180,12 +156,11 @@
      */
     public static ScopedContextMap getInstance(Map<String, Object> entriesToAdd) {
         // create a new array that can hold the maximum output size
-        ScopedContextMap newMap = new ScopedContextMap(entriesToAdd.size());
+        ScopedContextMap newMap = new ScopedContextMap(entriesToAdd.size(), new ArrayList<>());
 
         for (Entry<String, Object> entry : entriesToAdd.entrySet()) {
             newMap.add(entry.getKey(), entry.getValue());
         }
-        newMap.updateNumEntriesInArray();
         return newMap;
     }
 
@@ -194,28 +169,33 @@
      * performance problems. Consider marking it final after
      * https://bugs.openjdk.org/browse/JDK-8324186 is solved.
      */
-    private Object[] backingArray;
+    private MapEntry[] backingArray;
+
+    private List<Integer> skipList;
 
     private int numEntries;
 
-    public ScopedContextMap(int capacity) {
-        this.backingArray = new Object[capacity * 2 + 1];
-        this.backingArray[0] = 0;
+    private ScopedContextMap(int capacity, List<Integer> skipList) {
+        this.backingArray = new MapEntry[capacity];
+        for (int i = 0; i < backingArray.length; ++i) {
+            this.backingArray[i] = new MapEntry(i);
+        }
+        this.skipList = new ArrayList<>(skipList);
     }
 
-    private ScopedContextMap(Object[] backingArray) {
-        this.numEntries = (backingArray == null ? 0 : (int) backingArray[0]);
-        this.backingArray = backingArray;
-    }
-
-    ScopedContextMap(ScopedContextMap other) {
-        this.backingArray = other.backingArray;
-        this.numEntries = other.numEntries;
+    private ScopedContextMap(ScopedContextMap other, int entriesToAdd) {
+        this(other.backingArray.length + entriesToAdd, other.skipList);
+        System.arraycopy(other.backingArray, 0, this.backingArray, 0, other.backingArray.length);
+        numEntries = other.numEntries;
     }
 
     private void add(String key, Object value) {
-        backingArray[getArrayIndexForKey(numEntries)] = key;
-        backingArray[getArrayIndexForValue(numEntries)] = value;
+        Integer itemIndex = itemIndex(key);
+        if (itemIndex != null) {
+            skipList.add(itemIndex);
+        }
+        backingArray[numEntries].key = key;
+        backingArray[numEntries].value = value;
         numEntries++;
     }
 
@@ -233,19 +213,27 @@
     }
 
     public boolean containsKey(String 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;
+        return itemIndex(key) != null;
     }
 
-    Object[] getBackingArray() {
-        return backingArray;
+    private Integer itemIndex(String key) {
+        int hashCode = key.hashCode();
+        Iterator<Integer> skip = skipList.iterator();
+        Integer skipItem = nextSkipItem(skip);
+        for (int i = 0; i < numEntries; i++) {
+            if (skipItem != null && skipItem == i) {
+                skipItem = nextSkipItem(skip);
+                continue;
+            }
+            if (backingArray[i].key.equals(key)) {
+                return i;
+            }
+        }
+        return null;
+    }
+
+    private Integer nextSkipItem(Iterator<Integer> iter) {
+        return iter.hasNext() ? iter.next() : null;
     }
 
     /**
@@ -254,13 +242,11 @@
      */
     @Override
     public boolean containsValue(Object value) {
+        if (value == null) {
+            return false;
+        }
         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)) {
+            if (value.equals(backingArray[i].value)) {
                 return true;
             }
         }
@@ -276,14 +262,8 @@
      * @return A new Map.
      */
     public ScopedContextMap copyAndPut(String key, Object value) {
-        ScopedContextMap newMap = new ScopedContextMap(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();
+        ScopedContextMap newMap = new ScopedContextMap(this, numEntries + 1);
+        newMap.add(key, value);
         return newMap;
     }
 
@@ -295,175 +275,26 @@
      * @return The new Map.
      */
     public ScopedContextMap copyAndPutAll(Map<String, Object> entriesToAdd) {
-        // create a new array that can hold the maximum output size
-        ScopedContextMap newMap = new ScopedContextMap(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);
-        }
-
+        ScopedContextMap newMap = new ScopedContextMap(this, numEntries + entriesToAdd.size());
         for (Entry<String, Object> entry : entriesToAdd.entrySet()) {
-            String key = entry.getKey();
-            Object 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.add(entry.getKey(), entry.getValue());
         }
-
-        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 The key to remove.
-     * @return The new Map.
-     */
-    public ScopedContextMap copyAndRemove(String key) {
-        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;
-        } else if (numEntries == 1) {
-            // we have 1 item and we're about to remove it
-            return EMPTY_MAP;
-        }
-        ScopedContextMap newMap = new ScopedContextMap(numEntries);
-        if (indexToRemove > 0) {
-            // copy entries before the removed one
-            System.arraycopy(backingArray, 1, newMap.backingArray, 1, indexToRemove * 2);
-        }
-        if (indexToRemove + 1 < numEntries) {
-            // 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 keysToRemoveIterable the keys to remove.
-     * @return The new Map.
-     */
-    ScopedContextMap 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
-        ScopedContextMap newMap = new ScopedContextMap(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;
-        }
-
-        if (numEntriesKept == 0) {
-            return EMPTY_MAP;
-        }
-
-        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;
-    }
-
-    /**
      * This version of forEach is defined on the Map interface.
      */
     @Override
     public void forEach(java.util.function.BiConsumer<? super String, ? super Object> action) {
+        Iterator<Integer> skip = skipList.iterator();
+        Integer skipItem = nextSkipItem(skip);
         for (int i = 0; i < numEntries; i++) {
-            // BiConsumer should be able to handle values of any type V. In our case the values are of type String.
-            final String key = (String) backingArray[getArrayIndexForKey(i)];
-            final Object value = backingArray[getArrayIndexForValue(i)];
-            action.accept(key, value);
+            if (skipItem != null && skipItem == i) {
+                skipItem = nextSkipItem(skip);
+                continue;
+            }
+            action.accept(backingArray[i].key, backingArray[i].value);
         }
     }
 
@@ -474,6 +305,7 @@
 
     /**
      * Scans the array to find a matching key. Linear-time.
+     * Looks in reverse order because newer values for a key are appended to the end.
      */
     @Override
     public Object get(Object key) {
@@ -481,36 +313,14 @@
             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 backingArray[getArrayIndexForValue(i)];
+        for (int i = numEntries - 1; i >= 0; i--) {
+            if (backingArray[i].key.equals(key)) {
+                return backingArray[i].value;
             }
         }
         return null;
     }
 
-    /**
-     * Find an existing entry (if any) and overwrites the value, if found
-     *
-     * @param key The key to add.
-     * @param value The value.
-     */
-    private void addOrOverwriteKey(String key, Object 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 void putAll(Map<? extends String, ? extends Object> m) {
         throw new UnsupportedOperationException("putAll() is not supported, use copyAndPutAll instead");
@@ -523,10 +333,6 @@
 
     @Override
     public int size() {
-        return numEntries;
-    }
-
-    public Map<String, Object> toMap() {
-        return this;
+        return numEntries - skipList.size();
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractScopedContextProvider.java
index b9d599c..5d3c8bd 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractScopedContextProvider.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractScopedContextProvider.java
@@ -200,7 +200,7 @@
                 if (map != null && !map.isEmpty()) {
                     contextMap = ((ScopedContextMap) parent.contextMap).copyAndPutAll(map);
                 } else {
-                    contextMap = new ScopedContextMap(0);
+                    contextMap = ScopedContextMap.emptyMap();
                 }
             }
         }
@@ -217,11 +217,7 @@
             if (parent == null) {
                 contextMap = ScopedContextMap.getInstance(key, value);
             } else {
-                if (value == null) {
-                    contextMap = ((ScopedContextMap) parent.contextMap).copyAndRemove(key);
-                } else {
-                    contextMap = ((ScopedContextMap) parent.contextMap).copyAndPut(key, value);
-                }
+                contextMap = ((ScopedContextMap) parent.contextMap).copyAndPut(key, value);
             }
         }
 
@@ -229,7 +225,7 @@
                 final AbstractScopedContextProvider provider, final Instance parent, final boolean withThreadContext) {
             this.provider = provider != null ? provider : parent.provider;
             this.parent = parent;
-            contextMap = parent == null ? new ScopedContextMap(0) : parent.contextMap;
+            contextMap = parent == null ? ScopedContextMap.emptyMap() : parent.contextMap;
             this.withThreadContext = withThreadContext;
         }
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java
index 63b20d5..0b1f454 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java
@@ -16,8 +16,6 @@
  */
 package org.apache.logging.log4j.spi.internal;
 
-import java.util.ArrayDeque;
-import java.util.Deque;
 import java.util.Optional;
 import org.apache.logging.log4j.spi.AbstractScopedContextProvider;
 import org.apache.logging.log4j.spi.ScopedContextProvider;
@@ -29,8 +27,9 @@
 public class DefaultScopedContextProvider extends AbstractScopedContextProvider {
 
     public static final ScopedContextProvider INSTANCE = new DefaultScopedContextProvider();
+    private static final int ENTRIES = 4;
 
-    private final ThreadLocal<Deque<Instance>> scopedContext = new ThreadLocal<>();
+    private final ThreadLocal<InstanceArray> scopedContext = new ThreadLocal<>();
 
     /**
      * Returns an immutable Map containing all the key/value pairs as Object objects.
@@ -38,8 +37,8 @@
      */
     @Override
     protected Optional<Instance> getContext() {
-        final Deque<Instance> stack = scopedContext.get();
-        return stack != null ? Optional.of(stack.getFirst()) : Optional.empty();
+        final InstanceArray array = scopedContext.get();
+        return array != null ? Optional.of(array.array[array.index]) : Optional.empty();
     }
 
     /**
@@ -48,12 +47,12 @@
      */
     @Override
     protected void addScopedContext(final Instance context) {
-        Deque<Instance> stack = scopedContext.get();
-        if (stack == null) {
-            stack = new ArrayDeque<>();
-            scopedContext.set(stack);
+        InstanceArray array = scopedContext.get();
+        if (array == null) {
+            array = new InstanceArray();
+            scopedContext.set(array);
         }
-        stack.addFirst(context);
+        array.add(context);
     }
 
     /**
@@ -61,14 +60,50 @@
      */
     @Override
     protected void removeScopedContext() {
-        final Deque<Instance> stack = scopedContext.get();
-        if (stack != null) {
-            if (!stack.isEmpty()) {
-                stack.removeFirst();
+        final InstanceArray array = scopedContext.get();
+        if (array != null) {
+            if (!array.isEmpty()) {
+                array.remove();
             }
-            if (stack.isEmpty()) {
+            if (array.isEmpty()) {
                 scopedContext.remove();
             }
         }
     }
+
+    private static class InstanceArray {
+        public int index;
+        public Instance[] array;
+
+        public InstanceArray() {
+            this(ENTRIES);
+        }
+
+        public InstanceArray(int capacity) {
+            this.index = -1;
+            array = new Instance[capacity];
+        }
+
+        public boolean isEmpty() {
+            return index < 0;
+        }
+
+        public void add(Instance instance) {
+            int next = ++index;
+            if (next == array.length) {
+                expand();
+            }
+            array[next] = instance;
+        }
+
+        public void remove() {
+            array[index--] = null;
+        }
+
+        private void expand() {
+            Instance[] newArray = new Instance[array.length + ENTRIES];
+            System.arraycopy(array, 0, newArray, 0, array.length);
+            array = newArray;
+        }
+    }
 }
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 9db6754..c510e4f 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
@@ -44,6 +44,7 @@
 import org.openjdk.jmh.annotations.Scope;
 import org.openjdk.jmh.annotations.Setup;
 import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
 import org.openjdk.jmh.infra.Blackhole;
 
 /**
@@ -90,37 +91,80 @@
 
     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";
+    private static final String NO_GC_ARRAY_MAP = "NoGcSortedArray";
+    private static final Map<String, Class<? extends ThreadContextMap>> IMPLEMENTATIONS = new HashMap<>();
+
+    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,
+                CopyOnWriteOpenHashMapThreadContextMap.SUPER); // CopyOnWriteSortedArrayThreadContextMap.class);
+        IMPLEMENTATIONS.put(NO_GC_OPENHASH_MAP, GarbageFreeOpenHashMapThreadContextMap.class);
+        IMPLEMENTATIONS.put(
+                NO_GC_ARRAY_MAP,
+                GarbageFreeOpenHashMapThreadContextMap.SUPER); // GarbageFreeSortedArrayThreadContextMap.class);
+    }
+
     @State(Scope.Benchmark)
-    public static class ThreadContextState {
-        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";
-        private static final String NO_GC_ARRAY_MAP = "NoGcSortedArray";
-        private static final Map<String, Class<? extends ThreadContextMap>> IMPLEMENTATIONS = new HashMap<>();
+    public static class ReadThreadContextState {
 
-        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,
-                    CopyOnWriteOpenHashMapThreadContextMap.SUPER); // CopyOnWriteSortedArrayThreadContextMap.class);
-            // IMPLEMENTATIONS.put(NO_GC_OPENHASH_MAP, GarbageFreeOpenHashMapThreadContextMap.class);
-            IMPLEMENTATIONS.put(
-                    NO_GC_ARRAY_MAP,
-                    GarbageFreeOpenHashMapThreadContextMap.SUPER); // GarbageFreeSortedArrayThreadContextMap.class);
-        }
-
-        @Param({"Default", "CopyOpenHash", "CopySortedArray", "NoGcOpenHash", "NoGcSortedArray", "StringArray"})
+        @Param({"Default", "CopySortedArray", "NoGcSortedArray", "StringArray"})
         public String threadContextMapAlias;
 
-        public StringAppender appender;
-
         @Setup
         public void setup() {
-            LoggerContext context = (LoggerContext) LogManager.getContext(false);
+            System.setProperty(
+                    "log4j2.threadContextMap",
+                    IMPLEMENTATIONS.get(threadContextMapAlias).getName());
+            for (int i = 0; i < VALUES.length; i++) {
+                ThreadContext.put(KEYS[i], VALUES[i]);
+            }
+        }
+
+        @TearDown
+        public void teardown() {
+            ThreadContext.clearMap();
+        }
+    }
+
+    @State(Scope.Benchmark)
+    public static class ThreadContextState {
+
+        @Param({"Default", "CopySortedArray", "NoGcSortedArray", "StringArray"})
+        public String threadContextMapAlias;
+
+        @Setup
+        public void setup() {
+            System.setProperty(
+                    "log4j2.threadContextMap",
+                    IMPLEMENTATIONS.get(threadContextMapAlias).getName());
+        }
+
+        @TearDown
+        public void teardown() {
+            ThreadContext.clearMap();
+        }
+    }
+
+    @State(Scope.Benchmark)
+    public static class LogThreadContextState extends ReadThreadContextState {
+
+        public StringAppender appender;
+        public LoggerContext context;
+        public int counter;
+
+        @Setup
+        @Override
+        public void setup() {
+            super.setup();
+            context = (LoggerContext) LogManager.getContext(false);
             Configuration config = context.getConfiguration();
             PatternLayout layout = PatternLayout.newBuilder()
                     .withConfiguration(config)
@@ -138,21 +182,115 @@
             root.addAppender(appender, Level.DEBUG, null);
             root.setLevel(Level.DEBUG);
             context.updateLoggers();
+        }
 
-            System.setProperty(
-                    "log4j2.threadContextMap",
-                    IMPLEMENTATIONS.get(threadContextMapAlias).getName());
+        @TearDown
+        public void teardown() {
+            System.out.println("Last entry: " + appender.getMessage());
+            context.stop();
+            counter = 0;
+            super.teardown();
         }
     }
 
     @State(Scope.Benchmark)
     public static class ScopedContextState {
+        public Instance instance;
 
-        public StringAppender appender;
+        @Setup
+        public void setup() {}
+    }
+
+    @State(Scope.Benchmark)
+    public static class ReadScopedContextState {
+
+        public Instance instance;
+        public Worker worker;
 
         @Setup
         public void setup() {
-            LoggerContext context = (LoggerContext) LogManager.getContext(false);
+            instance = ScopedContext.where(KEYS[0], VALUES[0]);
+            for (int i = 1; i < VALUES.length; i++) {
+                instance = instance.where(KEYS[i], VALUES[i]);
+            }
+            worker = new Worker();
+        }
+
+        @TearDown
+        public void teardown() {
+            instance = null;
+            worker = null;
+        }
+
+        private static class Worker implements Runnable {
+
+            public void run() {
+                for (int i = 0; i < VALUES.length; i++) {
+                    ScopedContext.get(KEYS[i]);
+                }
+            }
+        }
+    }
+
+    @State(Scope.Benchmark)
+    public static class LogScopedContextState extends ReadScopedContextState {
+
+        public StringAppender appender;
+        public LoggerContext context;
+        public int counter;
+        public Worker worker;
+
+        @Setup
+        public void setup() {
+            super.setup();
+            context = (LoggerContext) LogManager.getContext(false);
+            Configuration config = context.getConfiguration();
+            PatternLayout layout = PatternLayout.newBuilder()
+                    .withConfiguration(config)
+                    .withPattern("%X %m%n")
+                    .build();
+            appender = StringAppender.createAppender("String", layout, null);
+            appender.start();
+            config.getAppenders().forEach((name, app) -> app.stop());
+            config.getAppenders().clear();
+            config.addAppender(appender);
+            final LoggerConfig root = config.getRootLogger();
+            root.getAppenders().forEach((name, appender) -> {
+                root.removeAppender(name);
+            });
+            root.addAppender(appender, Level.DEBUG, null);
+            root.setLevel(Level.DEBUG);
+            context.updateLoggers();
+            worker = new Worker();
+        }
+
+        @TearDown
+        public void teardown() {
+            System.out.println("Last entry: " + appender.getMessage());
+            context.stop();
+            counter = 0;
+            worker = null;
+            super.teardown();
+        }
+
+        private class Worker implements Runnable {
+
+            public void run() {
+                LOGGER.info("log count: {}", counter++);
+            }
+        }
+    }
+
+    @State(Scope.Benchmark)
+    public static class LogBaselineState {
+
+        public StringAppender appender;
+        public LoggerContext context;
+        public int counter;
+
+        @Setup
+        public void setup() {
+            context = (LoggerContext) LogManager.getContext(false);
             Configuration config = context.getConfiguration();
             PatternLayout layout = PatternLayout.newBuilder()
                     .withConfiguration(config)
@@ -171,21 +309,34 @@
             root.setLevel(Level.DEBUG);
             context.updateLoggers();
         }
+
+        @TearDown
+        public void teardown() {
+            System.out.println("Last entry: " + appender.getMessage());
+            context.stop();
+            counter = 0;
+        }
     }
 
     @Benchmark
-    public void threadContextMap(final Blackhole blackhole, ThreadContextState state) {
-        try {
-            for (int i = 0; i < VALUES.length; i++) {
-                ThreadContext.put(KEYS[i], VALUES[i]);
-            }
-            for (int j = 0; j < LOOP_COUNT; ++j) {
-                for (int i = 0; i < VALUES.length; i++) {
-                    blackhole.consume(ThreadContext.get(KEYS[i]));
-                }
-            }
-        } finally {
-            ThreadContext.clearMap();
+    public void populateThreadContext(final Blackhole blackhole, ThreadContextState state) {
+        for (int i = 0; i < VALUES.length; i++) {
+            ThreadContext.put(KEYS[i], VALUES[i]);
+        }
+    }
+
+    @Benchmark
+    public void populateScopedContext(final Blackhole blackhole, ScopedContextState state) {
+        state.instance = ScopedContext.where(KEYS[0], VALUES[0]);
+        for (int i = 1; i < VALUES.length; i++) {
+            state.instance = state.instance.where(KEYS[i], VALUES[i]);
+        }
+    }
+
+    @Benchmark
+    public void threadContextMap(final Blackhole blackhole, ReadThreadContextState state) {
+        for (int i = 0; i < VALUES.length; i++) {
+            blackhole.consume(ThreadContext.get(KEYS[i]));
         }
     }
 
@@ -193,119 +344,21 @@
      * This is equivalent to the typical ScopedContext case.
      */
     @Benchmark
-    public void logThreadContextMap(final Blackhole blackhole, ThreadContextState state) {
-        try {
-            for (int i = 0; i < VALUES.length; i++) {
-                ThreadContext.put(KEYS[i], VALUES[i]);
-            }
-            for (int j = 0; j < LOOP_COUNT; ++j) {
-                LOGGER.info("log count: {}", j);
-            }
-        } finally {
-            ThreadContext.clearMap();
-        }
+    public void logThreadContextMap(final Blackhole blackhole, LogThreadContextState state) {
+        LOGGER.info("log count: {}", state.counter++);
     }
 
     @Benchmark
-    public void nestedThreadContextMap(final Blackhole blackhole, ThreadContextState state) {
-        try {
-            for (int i = 0; i < VALUES.length; i++) {
-                ThreadContext.put(KEYS[i], VALUES[i]);
+    public void nestedThreadContextMap(final Blackhole blackhole, LogThreadContextState state) {
+        for (int i = 0; i < 10; ++i) {
+            LOGGER.info("outer log count: {}", i);
+        }
+        try (final CloseableThreadContext.Instance ignored = CloseableThreadContext.put(KEYS[8], NESTED[0])
+                .put(KEYS[9], NESTED[1])
+                .put(KEYS[10], NESTED[2])) {
+            for (int i = 0; i < 100; ++i) {
+                LOGGER.info("inner log count: {}", i);
             }
-            for (int j = 0; j < 100; ++j) {
-                int count = j * 100;
-                for (int i = 0; i < 10; ++i) {
-                    LOGGER.info("log count: {}", count + i);
-                }
-                try (final CloseableThreadContext.Instance ignored = CloseableThreadContext.put(KEYS[8], NESTED[0])
-                        .put(KEYS[9], NESTED[1])
-                        .put(KEYS[10], NESTED[2])) {
-                    for (int i = 0; i < 100; ++i) {
-                        LOGGER.info("log count: {}", count + i);
-                    }
-                }
-            }
-        } finally {
-            ThreadContext.clearMap();
-        }
-    }
-
-    /*
-     * Measure the more typical case of calling logging many times within a context.
-     */
-    @Benchmark
-    public void scopedContextTypical(final Blackhole blackhole, ScopedContextState state) {
-        Instance instance = ScopedContext.where(KEYS[0], VALUES[0]);
-        for (int i = 1; i < VALUES.length; i++) {
-            instance = instance.where(KEYS[i], VALUES[i]);
-        }
-        instance.run(() -> {
-            for (int j = 0; j < LOOP_COUNT; ++j) {
-                for (int i = 0; i < VALUES.length; i++) {
-                    ScopedContext.get(KEYS[i]);
-                }
-            }
-        });
-    }
-
-    /*
-     * Worst case - only logging 1 record per context.
-     */
-    @Benchmark
-    public void scopedContextWorst(final Blackhole blackhole, ScopedContextState state) {
-        Instance instance = ScopedContext.where(KEYS[0], VALUES[0]);
-        for (int i = 1; i < VALUES.length; i++) {
-            instance = instance.where(KEYS[i], VALUES[i]);
-        }
-        for (int j = 0; j < LOOP_COUNT; ++j) {
-            instance.run(() -> {
-                for (int i = 0; i < VALUES.length; i++) {
-                    ScopedContext.get(KEYS[i]);
-                }
-            });
-        }
-    }
-
-    /*
-     * Log the baseline - no context variables.
-     */
-    @Benchmark
-    public void logBaseline(final Blackhole blackhole, ScopedContextState state) {
-        for (int j = 0; j < LOOP_COUNT; ++j) {
-            LOGGER.info("log count: {}", j);
-        }
-    }
-
-    /*
-     * Measure the more typical case of calling logging many times within a context.
-     */
-    @Benchmark
-    public void logScopedContextTypical(final Blackhole blackhole, ScopedContextState state) {
-        Instance instance = ScopedContext.where(KEYS[0], VALUES[0]);
-        for (int i = 1; i < VALUES.length; i++) {
-            instance = instance.where(KEYS[i], VALUES[i]);
-        }
-        instance.run(() -> {
-            for (int j = 0; j < LOOP_COUNT; ++j) {
-                LOGGER.info("log count: {}", j);
-            }
-        });
-    }
-
-    /*
-     * Worst case - only logging 1 record per context.
-     */
-    @Benchmark
-    public void logScopedContextWorst(final Blackhole blackhole, ScopedContextState state) {
-        Instance instance = ScopedContext.where(KEYS[0], VALUES[0]);
-        for (int i = 1; i < VALUES.length; i++) {
-            instance = instance.where(KEYS[i], VALUES[i]);
-        }
-        for (int j = 0; j < LOOP_COUNT; ++j) {
-            final int count = j;
-            instance.run(() -> {
-                LOGGER.info("log count: {}", count);
-            });
         }
     }
 
@@ -313,26 +366,65 @@
      * Measure using nested contexts.
      */
     @Benchmark
-    public void nestedScopedContext(final Blackhole blackhole, ScopedContextState state) {
-        Instance instance = ScopedContext.where(KEYS[0], VALUES[0]);
-        for (int i = 1; i < VALUES.length; i++) {
-            instance = instance.where(KEYS[i], VALUES[i]);
-        }
-        instance.run(() -> {
-            for (int j = 0; j < 100; ++j) {
-                final int count = j * 100;
-                for (int i = 0; i < 10; ++i) {
-                    LOGGER.info("log count: {}", count + i);
-                }
-                ScopedContext.where(KEYS[8], NESTED[0])
-                        .where(KEYS[9], NESTED[1])
-                        .where(KEYS[10], NESTED[2])
-                        .run(() -> {
-                            for (int i = 0; i < 100; ++i) {
-                                LOGGER.info("log count: {}", count + i);
-                            }
-                        });
+    public void nestedScopedContext(final Blackhole blackhole, LogScopedContextState state) {
+        state.instance.run(() -> {
+            for (int i = 0; i < 10; ++i) {
+                LOGGER.info("log count: {}", i);
+            }
+            ScopedContext.where(KEYS[8], NESTED[0])
+                    .where(KEYS[9], NESTED[1])
+                    .where(KEYS[10], NESTED[2])
+                    .run(() -> {
+                        for (int i = 0; i < 100; ++i) {
+                            LOGGER.info("log count: {}", i);
+                        }
+                    });
+        });
+    }
+
+    /*
+     * Measure the more typical case of calling logging many times within a context.
+     */
+    @Benchmark
+    public void scopedContext(final Blackhole blackhole, ReadScopedContextState state) {
+        state.instance.run(() -> {
+            for (int i = 0; i < VALUES.length; i++) {
+                ScopedContext.get(KEYS[i]);
             }
         });
     }
+
+    /*
+     * Measure the more typical case of calling logging many times within a context.
+     */
+    @Benchmark
+    public void scopedContextMethod(final Blackhole blackhole, ReadScopedContextState state) {
+        state.instance.run(state.worker);
+    }
+
+    /*
+     * Log the baseline - no context variables.
+     */
+    @Benchmark
+    public void logBaseline(final Blackhole blackhole, LogBaselineState state) {
+        LOGGER.info("log count: {}", state.counter++);
+    }
+
+    /*
+     * Measure the more typical case of calling logging many times within a context.
+     */
+    @Benchmark
+    public void logScopedContext(final Blackhole blackhole, LogScopedContextState state) {
+        state.instance.run(() -> {
+            LOGGER.info("log count: {}", state.counter++);
+        });
+    }
+
+    /*
+     * Measure the more typical case of calling logging many times within a context.
+     */
+    @Benchmark
+    public void logScopedContextMethod(final Blackhole blackhole, LogScopedContextState state) {
+        state.instance.run(state.worker);
+    }
 }