You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by pk...@apache.org on 2024/02/27 09:19:41 UTC

(logging-log4j2) 04/04: Rollback `log4j-api` to version from `2.x`

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

pkarwasz pushed a commit to branch fix/rollback-log4j-api
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 42a76d2924530ccb6cb0d443e44cdb0f0e1e9324
Author: Piotr P. Karwasz <pi...@karwasz.org>
AuthorDate: Mon Feb 26 16:08:09 2024 +0100

    Rollback `log4j-api` to version from `2.x`
---
 .../log4j/test/ThreadContextUtilityClass.java      |   4 +
 .../java/org/apache/logging/log4j/LoggerTest.java  |  95 +++-----
 .../logging/log4j/NoopThreadContextTest.java       |  11 +-
 .../log4j/ThreadContextInheritanceTest.java        |  68 +++---
 .../apache/logging/log4j/util/LoaderUtilTest.java  |  61 ------
 log4j-api/pom.xml                                  |   8 +
 .../java/org/apache/logging/log4j/BridgeAware.java |   4 +-
 .../main/java/org/apache/logging/log4j/Level.java  |  30 ++-
 .../main/java/org/apache/logging/log4j/Logger.java | 132 +++++------
 .../main/java/org/apache/logging/log4j/Marker.java |   5 +-
 .../org/apache/logging/log4j/MarkerManager.java    | 154 +++++++------
 .../org/apache/logging/log4j/ThreadContext.java    | 119 +++++++---
 .../logging/log4j/internal/DefaultLogBuilder.java  | 177 +++++++--------
 .../log4j/message/AbstractMessageFactory.java      | 176 ++++++++++++++-
 .../logging/log4j/message/MessageFactory2.java     | 180 ++++++++++++++-
 .../log4j/message/ParameterizedMessageFactory.java | 192 ++++++++++++++++
 .../apache/logging/log4j/spi/AbstractLogger.java   | 174 +++++++++++----
 .../log4j/spi/CleanableThreadContextMap.java       |  20 +-
 .../CopyOnWriteSortedArrayThreadContextMap.java    |  36 ++-
 .../logging/log4j/spi/DefaultThreadContextMap.java | 244 +++++++++++++++++++++
 .../GarbageFreeSortedArrayThreadContextMap.java    |  44 ++--
 ...eadContextMap.java => LocationAwareLogger.java} |  22 +-
 .../apache/logging/log4j/spi/LoggingSystem.java    |   4 +-
 .../logging/log4j/spi/MessageFactory2Adapter.java  | 157 +++++++++++++
 .../logging/log4j/spi/NoOpThreadContextMap.java    |  22 +-
 .../logging/log4j/spi/ObjectThreadContextMap.java  |  35 ++-
 .../log4j/spi/ReadOnlyThreadContextMap.java        |   3 +-
 .../apache/logging/log4j/spi/ThreadContextMap.java |  87 --------
 .../logging/log4j/spi/ThreadContextMap2.java       |  34 ++-
 .../logging/log4j/spi/ThreadContextMapFactory.java | 146 ++++++++++++
 .../org/apache/logging/log4j/util/Constants.java   |  51 ++++-
 .../org/apache/logging/log4j/util/LoaderUtil.java  | 200 +++++++++++++----
 .../apache/logging/log4j/util/MessageSupplier.java |  18 +-
 .../apache/logging/log4j/util/PropertiesUtil.java  |  43 +++-
 .../logging/log4j/util/ProviderActivator.java      |  53 +++++
 .../apache/logging/log4j/util/ProviderUtil.java    | 178 +++++++++++++++
 .../org/apache/logging/log4j/util/Supplier.java    |  15 +-
 .../log4j/async/logger/AsyncThreadContextTest.java |  20 +-
 .../core/impl/ThreadContextDataInjectorTest.java   |  86 +++-----
 .../CopyOnWriteOpenHashMapThreadContextMap.java    |   4 +
 .../GarbageFreeOpenHashMapThreadContextMap.java    |  11 +-
 log4j-to-slf4j/pom.xml                             |   5 +
 .../slf4j/{SLF4JProvider.java => Activator.java}   |  20 +-
 .../org/apache/logging/slf4j/MDCContextMap.java    |   4 +-
 .../java/org/apache/logging/slf4j/SLF4JLogger.java |  14 +-
 .../org/apache/logging/slf4j/SLF4JProvider.java    |   2 +-
 .../java/org/apache/logging/slf4j/LoggerTest.java  |  40 +++-
 47 files changed, 2385 insertions(+), 823 deletions(-)

diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadContextUtilityClass.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadContextUtilityClass.java
index 61cd380829..c8424dc882 100644
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadContextUtilityClass.java
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/ThreadContextUtilityClass.java
@@ -111,4 +111,8 @@ public class ThreadContextUtilityClass {
         ThreadContext.put("testKey", "testValue");
         assertEquals("testValue", ThreadContext.get("testKey"));
     }
+
+    public static void reset() {
+        ThreadContext.init();
+    }
 }
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/LoggerTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/LoggerTest.java
index 7f4d6b8b37..db46820465 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/LoggerTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/LoggerTest.java
@@ -34,21 +34,19 @@ import org.apache.logging.log4j.message.JsonMessage;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.message.ObjectMessage;
-import org.apache.logging.log4j.message.ReusableMessageFactory;
+import org.apache.logging.log4j.message.ParameterizedMessageFactory;
 import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.logging.log4j.message.SimpleMessageFactory;
 import org.apache.logging.log4j.message.StringFormatterMessageFactory;
 import org.apache.logging.log4j.message.StructuredDataMessage;
+import org.apache.logging.log4j.spi.MessageFactory2Adapter;
 import org.apache.logging.log4j.test.TestLogger;
-import org.apache.logging.log4j.test.TestLoggerContextFactory;
-import org.apache.logging.log4j.test.junit.LoggerContextFactoryExtension;
 import org.apache.logging.log4j.test.junit.Resources;
 import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
 import org.apache.logging.log4j.util.Strings;
 import org.apache.logging.log4j.util.Supplier;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.api.parallel.ResourceAccessMode;
 import org.junit.jupiter.api.parallel.ResourceLock;
 import org.junitpioneer.jupiter.ReadsSystemProperty;
@@ -57,11 +55,7 @@ import org.junitpioneer.jupiter.ReadsSystemProperty;
 @ReadsSystemProperty
 public class LoggerTest {
 
-    @RegisterExtension
-    public static final LoggerContextFactoryExtension EXTENSION =
-            new LoggerContextFactoryExtension(new TestLoggerContextFactory());
-
-    private static class TestReusableMessageFactory {
+    private static class TestParameterizedMessageFactory {
         // empty
     }
 
@@ -82,7 +76,7 @@ public class LoggerTest {
         assertThat(
                 "Incorrect message 1",
                 results.get(0),
-                equalTo(" DEBUG org.apache.logging.log4j.LoggerTest.builder(LoggerTest.java:78) Hello"));
+                equalTo(" DEBUG org.apache.logging.log4j.LoggerTest.builder(LoggerTest.java:72) Hello"));
         assertThat("Incorrect message 2", results.get(1), equalTo("test ERROR Hello John"));
         assertThat(
                 "Incorrect message 3",
@@ -91,13 +85,13 @@ public class LoggerTest {
         assertThat(
                 "Throwable incorrect in message 3",
                 results.get(2),
-                containsString("org.apache.logging.log4j.LoggerTest.builder(LoggerTest.java:80)"));
+                containsString("org.apache.logging.log4j.LoggerTest.builder(LoggerTest.java:74)"));
     }
 
     @Test
     public void basicFlow() {
-        logger.traceEntry();
-        logger.traceExit();
+        logger.entry();
+        logger.exit();
         assertEquals(2, results.size());
         assertThat(results.get(0)).isEqualTo("ENTER[ FLOW ] TRACE Enter");
         assertThat(results.get(1)).isEqualTo("EXIT[ FLOW ] TRACE Exit");
@@ -172,19 +166,8 @@ public class LoggerTest {
     @Test
     public void flowTracingString_SupplierOfObjectMessages() {
         final EntryMessage msg = logger.traceEntry(
-                "doFoo(a={}, b={})",
-                new Supplier<Message>() {
-                    @Override
-                    public Message get() {
-                        return new ObjectMessage(1);
-                    }
-                },
-                new Supplier<Message>() {
-                    @Override
-                    public Message get() {
-                        return new ObjectMessage(2);
-                    }
-                });
+                "doFoo(a={}, b={})", (Supplier<Message>) () -> new ObjectMessage(1), (Supplier<Message>)
+                        () -> new ObjectMessage(2));
         logger.traceExit(msg, 3);
         assertThat(results).hasSize(2);
         assertThat(results.get(0)).startsWith("ENTER[ FLOW ] TRACE Enter").contains("doFoo(a=1, b=2)");
@@ -193,20 +176,8 @@ public class LoggerTest {
 
     @Test
     public void flowTracingString_SupplierOfStrings() {
-        final EntryMessage msg = logger.traceEntry(
-                "doFoo(a={}, b={})",
-                new Supplier<String>() {
-                    @Override
-                    public String get() {
-                        return "1";
-                    }
-                },
-                new Supplier<String>() {
-                    @Override
-                    public String get() {
-                        return "2";
-                    }
-                });
+        final EntryMessage msg =
+                logger.traceEntry("doFoo(a={}, b={})", (Supplier<String>) () -> "1", (Supplier<String>) () -> "2");
         logger.traceExit(msg, 3);
         assertThat(results).hasSize(2);
         assertThat(results.get(0)).startsWith("ENTER[ FLOW ] TRACE Enter").contains("doFoo(a=1, b=2)");
@@ -267,7 +238,6 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
     @ResourceLock(value = org.junit.jupiter.api.parallel.Resources.LOCALE, mode = ResourceAccessMode.READ)
     public void getFormatterLogger() {
         // The TestLogger logger was already created in an instance variable for this class.
@@ -286,7 +256,6 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
     @ResourceLock(value = org.junit.jupiter.api.parallel.Resources.LOCALE, mode = ResourceAccessMode.READ)
     public void getFormatterLogger_Class() {
         // The TestLogger logger was already created in an instance variable for this class.
@@ -303,12 +272,14 @@ public class LoggerTest {
                 testLogger.getEntries().get(0));
     }
 
-    private static void assertMessageFactoryInstanceOf(final MessageFactory factory, final Class<?> cls) {
+    private static void assertMessageFactoryInstanceOf(MessageFactory factory, final Class<?> cls) {
+        if (factory instanceof MessageFactory2Adapter) {
+            factory = ((MessageFactory2Adapter) factory).getOriginal();
+        }
         assertTrue(factory.getClass().isAssignableFrom(cls));
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
     @ResourceLock(value = org.junit.jupiter.api.parallel.Resources.LOCALE, mode = ResourceAccessMode.READ)
     public void getFormatterLogger_Object() {
         // The TestLogger logger was already created in an instance variable for this class.
@@ -326,7 +297,6 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
     @ResourceLock(value = org.junit.jupiter.api.parallel.Resources.LOCALE, mode = ResourceAccessMode.READ)
     public void getFormatterLogger_String() {
         final StringFormatterMessageFactory messageFactory = StringFormatterMessageFactory.INSTANCE;
@@ -343,13 +313,12 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
-    public void getLogger_Class_ReusableMessageFactory() {
+    public void getLogger_Class_ParameterizedMessageFactory() {
         // The TestLogger logger was already created in an instance variable for this class.
         // The message factory is only used when the logger is created.
-        final MessageFactory messageFactory = new ReusableMessageFactory();
+        final ParameterizedMessageFactory messageFactory = ParameterizedMessageFactory.INSTANCE;
         final TestLogger testLogger =
-                (TestLogger) LogManager.getLogger(TestReusableMessageFactory.class, messageFactory);
+                (TestLogger) LogManager.getLogger(TestParameterizedMessageFactory.class, messageFactory);
         assertNotNull(testLogger);
         assertEqualMessageFactory(messageFactory, testLogger);
         testLogger.debug("{}", Integer.MAX_VALUE);
@@ -358,7 +327,6 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
     public void getLogger_Class_StringFormatterMessageFactory() {
         // The TestLogger logger was already created in an instance variable for this class.
         // The message factory is only used when the logger is created.
@@ -374,13 +342,12 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
-    public void getLogger_Object_ReusableMessageFactory() {
+    public void getLogger_Object_ParameterizedMessageFactory() {
         // The TestLogger logger was already created in an instance variable for this class.
         // The message factory is only used when the logger is created.
-        final MessageFactory messageFactory = new ReusableMessageFactory();
+        final ParameterizedMessageFactory messageFactory = ParameterizedMessageFactory.INSTANCE;
         final TestLogger testLogger =
-                (TestLogger) LogManager.getLogger(new TestReusableMessageFactory(), messageFactory);
+                (TestLogger) LogManager.getLogger(new TestParameterizedMessageFactory(), messageFactory);
         assertNotNull(testLogger);
         assertEqualMessageFactory(messageFactory, testLogger);
         testLogger.debug("{}", Integer.MAX_VALUE);
@@ -389,12 +356,14 @@ public class LoggerTest {
     }
 
     private void assertEqualMessageFactory(final MessageFactory messageFactory, final TestLogger testLogger) {
-        final MessageFactory actual = testLogger.getMessageFactory();
+        MessageFactory actual = testLogger.getMessageFactory();
+        if (actual instanceof MessageFactory2Adapter) {
+            actual = ((MessageFactory2Adapter) actual).getOriginal();
+        }
         assertEquals(messageFactory, actual);
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
     public void getLogger_Object_StringFormatterMessageFactory() {
         // The TestLogger logger was already created in an instance variable for this class.
         // The message factory is only used when the logger is created.
@@ -411,7 +380,6 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
     public void getLogger_String_MessageFactoryMismatch() {
         final StringFormatterMessageFactory messageFactory = StringFormatterMessageFactory.INSTANCE;
         final TestLogger testLogger =
@@ -419,7 +387,7 @@ public class LoggerTest {
         assertNotNull(testLogger);
         assertEqualMessageFactory(messageFactory, testLogger);
         final TestLogger testLogger2 = (TestLogger)
-                LogManager.getLogger("getLogger_String_MessageFactoryMismatch", new ReusableMessageFactory());
+                LogManager.getLogger("getLogger_String_MessageFactoryMismatch", ParameterizedMessageFactory.INSTANCE);
         assertNotNull(testLogger2);
         // TODO: How to test?
         // This test context always creates new loggers, other test context impls I tried fail other tests.
@@ -432,11 +400,10 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
-    public void getLogger_String_ReusableMessageFactory() {
-        final MessageFactory messageFactory = new ReusableMessageFactory();
+    public void getLogger_String_ParameterizedMessageFactory() {
+        final ParameterizedMessageFactory messageFactory = ParameterizedMessageFactory.INSTANCE;
         final TestLogger testLogger =
-                (TestLogger) LogManager.getLogger("getLogger_String_ReusableMessageFactory", messageFactory);
+                (TestLogger) LogManager.getLogger("getLogger_String_ParameterizedMessageFactory", messageFactory);
         assertNotNull(testLogger);
         assertEqualMessageFactory(messageFactory, testLogger);
         testLogger.debug("{}", Integer.MAX_VALUE);
@@ -445,7 +412,6 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
     public void getLogger_String_SimpleMessageFactory() {
         final SimpleMessageFactory messageFactory = SimpleMessageFactory.INSTANCE;
         final TestLogger testLogger =
@@ -458,7 +424,6 @@ public class LoggerTest {
     }
 
     @Test
-    @ResourceLock("log4j2.TestLogger")
     public void getLogger_String_StringFormatterMessageFactory() {
         final StringFormatterMessageFactory messageFactory = StringFormatterMessageFactory.INSTANCE;
         final TestLogger testLogger =
@@ -607,7 +572,7 @@ public class LoggerTest {
     @Test
     @UsingThreadContextMap
     public void mdc() {
-        ThreadContext.put("TestYear", Integer.valueOf(2010).toString());
+        ThreadContext.put("TestYear", Integer.toString(2010));
         logger.debug("Debug message");
         final String testYear = ThreadContext.get("TestYear");
         assertNotNull(testYear, "Test Year is null");
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/NoopThreadContextTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/NoopThreadContextTest.java
index ef51fdf7d3..d4ce703b56 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/NoopThreadContextTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/NoopThreadContextTest.java
@@ -18,22 +18,19 @@ package org.apache.logging.log4j;
 
 import static org.junit.jupiter.api.Assertions.assertNull;
 
-import org.apache.logging.log4j.spi.LoggingSystemProperty;
-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.SetSystemProperty;
 
 /**
  * Tests {@link ThreadContext}.
  */
+@SetTestProperty(key = "log4j2.disableThreadContext", value = "true")
+@SetTestProperty(key = "log4j2.disableThreadContextMap", value = "true")
+@UsingThreadContextMap
 public class NoopThreadContextTest {
 
     @Test
-    @SetSystemProperty(key = LoggingSystemProperty.Constant.THREAD_CONTEXT_ENABLED, value = "false")
-    @SetSystemProperty(key = LoggingSystemProperty.Constant.THREAD_CONTEXT_MAP_ENABLED, value = "false")
-    @InitializesThreadContext
-    @UsingThreadContextMap
     public void testNoop() {
         ThreadContext.put("Test", "Test");
         final String value = ThreadContext.get("Test");
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
index 08f216ba90..96783ae600 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextInheritanceTest.java
@@ -21,11 +21,14 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.spi.DefaultThreadContextMap;
 import org.apache.logging.log4j.test.ThreadContextUtilityClass;
 import org.apache.logging.log4j.test.junit.InitializesThreadContext;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
 import org.apache.logging.log4j.test.junit.UsingThreadContextMap;
 import org.apache.logging.log4j.test.junit.UsingThreadContextStack;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junitpioneer.jupiter.SetSystemProperty;
@@ -33,12 +36,25 @@ import org.junitpioneer.jupiter.SetSystemProperty;
 /**
  * Tests {@link ThreadContext}.
  */
-@SetSystemProperty(key = LoggingSystemProperty.Constant.THREAD_CONTEXT_MAP_INHERITABLE, value = "true")
+@SetTestProperty(key = DefaultThreadContextMap.INHERITABLE_MAP, value = "true")
 @InitializesThreadContext
+@UsingThreadContextMap
+@UsingThreadContextStack
 public class ThreadContextInheritanceTest {
 
+    @BeforeAll
+    public static void setupClass() {
+        System.setProperty(DefaultThreadContextMap.INHERITABLE_MAP, "true");
+        ThreadContext.init();
+    }
+
+    @AfterAll
+    public static void tearDownClass() {
+        System.clearProperty(DefaultThreadContextMap.INHERITABLE_MAP);
+        ThreadContext.init();
+    }
+
     @Test
-    @UsingThreadContextStack
     public void testPush() {
         ThreadContext.push("Hello");
         ThreadContext.push("{} is {}", ThreadContextInheritanceTest.class.getSimpleName(), "running");
@@ -48,76 +64,74 @@ public class ThreadContextInheritanceTest {
     }
 
     @Test
-    @SetSystemProperty(key = LoggingSystemProperty.Constant.THREAD_CONTEXT_MAP_INHERITABLE, value = "true")
+    @SetSystemProperty(key = DefaultThreadContextMap.INHERITABLE_MAP, value = "true")
     @InitializesThreadContext
     public void testInheritanceSwitchedOn() throws Exception {
-        ThreadContext.put("Greeting", "Hello");
-        StringBuilder sb = new StringBuilder();
-        TestThread thread = new TestThread(sb);
-        thread.start();
-        thread.join();
-        String str = sb.toString();
-        assertEquals("Hello", str, "Unexpected ThreadContext value. Expected Hello. Actual " + str);
-        sb = new StringBuilder();
-        thread = new TestThread(sb);
-        thread.start();
-        thread.join();
-        str = sb.toString();
-        assertEquals("Hello", str, "Unexpected ThreadContext value. Expected Hello. Actual " + str);
+        System.setProperty(DefaultThreadContextMap.INHERITABLE_MAP, "true");
+        try {
+            ThreadContext.clearMap();
+            ThreadContext.put("Greeting", "Hello");
+            StringBuilder sb = new StringBuilder();
+            TestThread thread = new TestThread(sb);
+            thread.start();
+            thread.join();
+            String str = sb.toString();
+            assertEquals("Hello", str, "Unexpected ThreadContext value. Expected Hello. Actual " + str);
+            sb = new StringBuilder();
+            thread = new TestThread(sb);
+            thread.start();
+            thread.join();
+            str = sb.toString();
+            assertEquals("Hello", str, "Unexpected ThreadContext value. Expected Hello. Actual " + str);
+        } finally {
+            System.clearProperty(DefaultThreadContextMap.INHERITABLE_MAP);
+        }
     }
 
     @Test
     @Tag("performance")
-    @UsingThreadContextMap
     public void perfTest() {
         ThreadContextUtilityClass.perfTest();
     }
 
     @Test
-    @UsingThreadContextMap
     public void testGetContextReturnsEmptyMapIfEmpty() {
         ThreadContextUtilityClass.testGetContextReturnsEmptyMapIfEmpty();
     }
 
     @Test
-    @UsingThreadContextMap
     public void testGetContextReturnsMutableCopy() {
         ThreadContextUtilityClass.testGetContextReturnsMutableCopy();
     }
 
     @Test
-    @UsingThreadContextMap
     public void testGetImmutableContextReturnsEmptyMapIfEmpty() {
         ThreadContextUtilityClass.testGetImmutableContextReturnsEmptyMapIfEmpty();
     }
 
     @Test
-    @UsingThreadContextMap
     public void testGetImmutableContextReturnsImmutableMapIfNonEmpty() {
         ThreadContextUtilityClass.testGetImmutableContextReturnsImmutableMapIfNonEmpty();
     }
 
     @Test
-    @UsingThreadContextMap
     public void testGetImmutableContextReturnsImmutableMapIfEmpty() {
         ThreadContextUtilityClass.testGetImmutableContextReturnsImmutableMapIfEmpty();
     }
 
     @Test
-    @UsingThreadContextStack
     public void testGetImmutableStackReturnsEmptyStackIfEmpty() {
         ThreadContextUtilityClass.testGetImmutableStackReturnsEmptyStackIfEmpty();
     }
 
     @Test
-    @UsingThreadContextMap
     public void testPut() {
         ThreadContextUtilityClass.testPut();
     }
 
     @Test
-    @UsingThreadContextMap
     public void testRemove() {
+        ThreadContext.clearMap();
         assertNull(ThreadContext.get("testKey"));
         ThreadContext.put("testKey", "testValue");
         assertEquals("testValue", ThreadContext.get("testKey"));
@@ -128,8 +142,8 @@ public class ThreadContextInheritanceTest {
     }
 
     @Test
-    @UsingThreadContextMap
     public void testContainsKey() {
+        ThreadContext.clearMap();
         assertFalse(ThreadContext.containsKey("testKey"));
         ThreadContext.put("testKey", "testValue");
         assertTrue(ThreadContext.containsKey("testKey"));
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/util/LoaderUtilTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/util/LoaderUtilTest.java
deleted file mode 100644
index 12b22e1de1..0000000000
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/util/LoaderUtilTest.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.util;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.net.URL;
-import java.util.Collections;
-import java.util.Enumeration;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junitpioneer.jupiter.ReadsSystemProperty;
-
-@ReadsSystemProperty
-public class LoaderUtilTest {
-    @BeforeEach
-    @AfterEach
-    public void reset() {
-        LoaderUtil.forceTcclOnly = null;
-    }
-
-    @Test
-    public void systemClassLoader() {
-        final Thread thread = Thread.currentThread();
-        final ClassLoader tccl = thread.getContextClassLoader();
-
-        LoaderUtil.forceTcclOnly = true;
-        final ClassLoader loader = new ClassLoader(tccl) {
-            @Override
-            public Enumeration<URL> getResources(final String name) {
-                return Collections.emptyEnumeration();
-            }
-        };
-        thread.setContextClassLoader(loader);
-        try {
-            assertEquals(
-                    0, LoaderUtil.findUrlResources("Log4j-charsets.properties").size());
-
-            LoaderUtil.forceTcclOnly = false;
-            assertEquals(
-                    1, LoaderUtil.findUrlResources("Log4j-charsets.properties").size());
-        } finally {
-            thread.setContextClassLoader(tccl);
-        }
-    }
-}
diff --git a/log4j-api/pom.xml b/log4j-api/pom.xml
index 249887921c..c5a4505f76 100644
--- a/log4j-api/pom.xml
+++ b/log4j-api/pom.xml
@@ -52,4 +52,12 @@
 
   </properties>
 
+  <dependencies>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.framework</artifactId>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+
 </project>
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/BridgeAware.java b/log4j-api/src/main/java/org/apache/logging/log4j/BridgeAware.java
index 86b7c00d7e..4ee476c524 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/BridgeAware.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/BridgeAware.java
@@ -24,10 +24,10 @@ package org.apache.logging.log4j;
 public interface BridgeAware {
 
     /**
-     * Fully qualified class name of the entry point to the logging system. This
+     * To set fully qualified class name of the entry point to the logging system. This
      * class will not appear in the location information.
      *
-     * @param fqcn fully qualified class name of logging system entry point
+     * @param fqcn fully qualified class name
      */
     void setEntryPoint(final String fqcn);
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/Level.java b/log4j-api/src/main/java/org/apache/logging/log4j/Level.java
index 02c7d2f750..9f8e786326 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/Level.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/Level.java
@@ -19,6 +19,7 @@ package org.apache.logging.log4j;
 import static org.apache.logging.log4j.util.Strings.toRootUpperCase;
 
 import aQute.bnd.annotation.baseline.BaselineIgnore;
+import java.io.Serializable;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -27,8 +28,9 @@ import org.apache.logging.log4j.util.Strings;
 
 /**
  * Levels used for identifying the severity of an event. Levels are organized from most specific to least:
+ * <p>
  * <table>
- * <caption>Levels</caption>
+ * <caption>Level names with description</caption>
  * <tr>
  * <th>Name</th>
  * <th>Description</th>
@@ -66,14 +68,15 @@ import org.apache.logging.log4j.util.Strings;
  * <td>All events should be logged.</td>
  * </tr>
  * </table>
+ * </p>
  * <p>
  * Typically, configuring a level in a filter or on a logger will cause logging events of that level and those that are
  * more specific to pass through the filter. A special level, {@link #ALL}, is guaranteed to capture all levels when
  * used in logging configurations.
  * </p>
  */
-@BaselineIgnore("3.0.0")
-public final class Level implements Comparable<Level> {
+@BaselineIgnore("2.22.0")
+public final class Level implements Comparable<Level>, Serializable {
 
     private static final Level[] EMPTY_ARRAY = {};
 
@@ -119,14 +122,14 @@ public final class Level implements Comparable<Level> {
      */
     public static final Level ALL = new Level("ALL", StandardLevel.ALL.intLevel());
 
-    public static final String NAMESPACE = "Level";
-
     /**
      * Category to be used by custom levels.
      *
      * @since 2.1
      */
-    public static final String CATEGORY = NAMESPACE;
+    public static final String CATEGORY = "Level";
+
+    private static final long serialVersionUID = 1581082L;
 
     private final String name;
     private final int intLevel;
@@ -171,7 +174,7 @@ public final class Level implements Comparable<Level> {
      *
      * @param minLevel The minimum level to test.
      * @param maxLevel The maximum level to test.
-     * @return true if this level is in between the given levels
+     * @return True true if this level is in between the given levels
      * @since 2.4
      */
     public boolean isInRange(final Level minLevel, final Level maxLevel) {
@@ -190,7 +193,7 @@ public final class Level implements Comparable<Level> {
      *
      * @param level
      *            The level to test.
-     * @return True if this Level is less specific or the same as the given Level.
+     * @return True if this level Level is less specific or the same as the given Level.
      */
     public boolean isLessSpecificThan(final Level level) {
         return this.intLevel >= level.intLevel;
@@ -206,7 +209,7 @@ public final class Level implements Comparable<Level> {
      * </p>
      *
      * @param level The level to test.
-     * @return True if this Level is more specific or the same as the given Level.
+     * @return True if this level Level is more specific or the same as the given Level.
      */
     public boolean isMoreSpecificThan(final Level level) {
         return this.intLevel <= level.intLevel;
@@ -222,12 +225,12 @@ public final class Level implements Comparable<Level> {
 
     @Override
     public int compareTo(final Level other) {
-        return Integer.compare(intLevel, other.intLevel);
+        return intLevel < other.intLevel ? -1 : (intLevel > other.intLevel ? 1 : 0);
     }
 
     @Override
     public boolean equals(final Object other) {
-        return other == this;
+        return other instanceof Level && other == this;
     }
 
     public Class<Level> getDeclaringClass() {
@@ -363,4 +366,9 @@ public final class Level implements Comparable<Level> {
     public static <T extends Enum<T>> T valueOf(final Class<T> enumType, final String name) {
         return Enum.valueOf(enumType, name);
     }
+
+    // for deserialization
+    private Object readResolve() {
+        return Level.valueOf(this.name);
+    }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/Logger.java b/log4j-api/src/main/java/org/apache/logging/log4j/Logger.java
index 2ca3b07132..7dc872e21a 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/Logger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/Logger.java
@@ -20,6 +20,7 @@ import org.apache.logging.log4j.message.EntryMessage;
 import org.apache.logging.log4j.message.FlowMessageFactory;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.message.MessageFactory2;
 import org.apache.logging.log4j.util.MessageSupplier;
 import org.apache.logging.log4j.util.Supplier;
 
@@ -122,7 +123,6 @@ public interface Logger {
      * @param messageSupplier A function, which when called, produces the desired log message.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
     void debug(Marker marker, MessageSupplier messageSupplier);
 
     /**
@@ -135,7 +135,6 @@ public interface Logger {
      * @param throwable A Throwable or null.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
     void debug(Marker marker, MessageSupplier messageSupplier, Throwable throwable);
 
     /**
@@ -201,7 +200,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void debug(Marker marker, String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -223,7 +222,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void debug(Marker marker, Supplier<?> messageSupplier);
 
     /**
@@ -236,7 +235,7 @@ public interface Logger {
      * @param throwable A Throwable or null.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void debug(Marker marker, Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -330,7 +329,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void debug(String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -349,7 +348,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void debug(Supplier<?> messageSupplier);
 
     /**
@@ -361,7 +360,7 @@ public interface Logger {
      * @param throwable the {@code Throwable} to log, including its stack trace.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void debug(Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -684,10 +683,8 @@ public interface Logger {
      * logged.
      * @deprecated Use {@link #traceEntry()} instead which performs the same function.
      */
-    @Deprecated(forRemoval = true, since = "2.6")
-    default void entry() {
-        traceEntry();
-    }
+    @Deprecated
+    void entry();
 
     /**
      * Logs entry to a method along with its parameters (consider using one of the {@code traceEntry(...)} methods instead.)
@@ -708,10 +705,8 @@ public interface Logger {
      * @param params The parameters to the method.
      * @deprecated Use {@link #traceEntry(String, Object...)} instead which performs the same function.
      */
-    @Deprecated(forRemoval = true, since = "2.12")
-    default void entry(final Object... params) {
-        traceEntry(null, params);
-    }
+    @Deprecated
+    void entry(Object... params);
 
     /**
      * Logs a message with the specific Marker at the {@link Level#ERROR ERROR} level.
@@ -816,7 +811,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void error(Marker marker, String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -838,7 +833,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void error(Marker marker, Supplier<?> messageSupplier);
 
     /**
@@ -851,7 +846,7 @@ public interface Logger {
      * @param throwable A Throwable or null.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void error(Marker marker, Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -945,7 +940,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void error(String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -964,7 +959,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void error(Supplier<?> messageSupplier);
 
     /**
@@ -976,7 +971,7 @@ public interface Logger {
      * @param throwable the {@code Throwable} to log, including its stack trace.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void error(Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -1298,10 +1293,8 @@ public interface Logger {
      * Logs exit from a method. Used for methods that do not return anything.
      * @deprecated Use {@link #traceExit()} instead which performs the same function.
      */
-    @Deprecated(forRemoval = true, since = "2.6")
-    default void exit() {
-        traceExit();
-    }
+    @Deprecated
+    void exit();
 
     /**
      * Logs exiting from a method with the result. This may be coded as:
@@ -1315,10 +1308,8 @@ public interface Logger {
      * @return the result.
      * @deprecated Use {@link #traceExit(Object)} instead which performs the same function.
      */
-    @Deprecated(forRemoval = true, since = "2.6")
-    default <R> R exit(final R result) {
-        return traceExit(result);
-    }
+    @Deprecated
+    <R> R exit(R result);
 
     /**
      * Logs a message with the specific Marker at the {@link Level#FATAL FATAL} level.
@@ -1423,7 +1414,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void fatal(Marker marker, String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -1445,7 +1436,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void fatal(Marker marker, Supplier<?> messageSupplier);
 
     /**
@@ -1458,7 +1449,7 @@ public interface Logger {
      * @param throwable A Throwable or null.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void fatal(Marker marker, Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -1552,7 +1543,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void fatal(String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -1571,7 +1562,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void fatal(Supplier<?> messageSupplier);
 
     /**
@@ -1583,7 +1574,7 @@ public interface Logger {
      * @param throwable the {@code Throwable} to log, including its stack trace.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void fatal(Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -1911,13 +1902,13 @@ public interface Logger {
     /**
      * Gets the message factory used to convert message Objects and Strings/CharSequences into actual log Messages.
      *
-     * Since version 2.6, Log4j internally uses message factories that implement the {@link MessageFactory} interface.
+     * Since version 2.6, Log4j internally uses message factories that implement the {@link MessageFactory2} interface.
      * From version 2.6.2, the return type of this method was changed from {@link MessageFactory} to
-     * {@code <MF extends MessageFactory> MF}. The returned factory will always implement {@link MessageFactory},
-     * but the return type of this method could not be changed to {@link MessageFactory} without breaking binary
+     * {@code <MF extends MessageFactory> MF}. The returned factory will always implement {@link MessageFactory2},
+     * but the return type of this method could not be changed to {@link MessageFactory2} without breaking binary
      * compatibility.
      *
-     * @return the message factory, as an instance of {@link MessageFactory}
+     * @return the message factory, as an instance of {@link MessageFactory2}
      */
     <MF extends MessageFactory> MF getMessageFactory();
 
@@ -2039,7 +2030,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void info(Marker marker, String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -2061,7 +2052,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void info(Marker marker, Supplier<?> messageSupplier);
 
     /**
@@ -2074,7 +2065,7 @@ public interface Logger {
      * @param throwable A Throwable or null.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void info(Marker marker, Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -2168,7 +2159,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void info(String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -2187,7 +2178,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void info(Supplier<?> messageSupplier);
 
     /**
@@ -2199,7 +2190,7 @@ public interface Logger {
      * @param throwable the {@code Throwable} to log, including its stack trace.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void info(Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -2746,7 +2737,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void log(Level level, Marker marker, String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -2769,7 +2760,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void log(Level level, Marker marker, Supplier<?> messageSupplier);
 
     /**
@@ -2783,7 +2774,7 @@ public interface Logger {
      * @param throwable A Throwable or null.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void log(Level level, Marker marker, Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -2887,7 +2878,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void log(Level level, String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -2908,7 +2899,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void log(Level level, Supplier<?> messageSupplier);
 
     /**
@@ -2921,7 +2912,7 @@ public interface Logger {
      * @param throwable the {@code Throwable} to log, including its stack log.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void log(Level level, Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -3444,7 +3435,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void trace(Marker marker, String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -3467,7 +3458,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void trace(Marker marker, Supplier<?> messageSupplier);
 
     /**
@@ -3480,7 +3471,7 @@ public interface Logger {
      * @param throwable A Throwable or null.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void trace(Marker marker, Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -3576,7 +3567,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void trace(String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -3596,7 +3587,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void trace(Supplier<?> messageSupplier);
 
     /**
@@ -3608,7 +3599,7 @@ public interface Logger {
      * @param throwable the {@code Throwable} to log, including its stack trace.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void trace(Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -3976,7 +3967,7 @@ public interface Logger {
      *
      * @since 2.6
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     EntryMessage traceEntry(Supplier<?>... paramSuppliers);
 
     /**
@@ -3995,7 +3986,7 @@ public interface Logger {
      *
      * @since 2.6
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     EntryMessage traceEntry(String format, Supplier<?>... paramSuppliers);
 
     /**
@@ -4214,7 +4205,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void warn(Marker marker, String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -4236,7 +4227,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void warn(Marker marker, Supplier<?> messageSupplier);
 
     /**
@@ -4249,7 +4240,7 @@ public interface Logger {
      * @param throwable A Throwable or null.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void warn(Marker marker, Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -4343,7 +4334,7 @@ public interface Logger {
      * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void warn(String message, Supplier<?>... paramSuppliers);
 
     /**
@@ -4362,7 +4353,7 @@ public interface Logger {
      *            message factory.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void warn(Supplier<?> messageSupplier);
 
     /**
@@ -4374,7 +4365,7 @@ public interface Logger {
      * @param throwable the {@code Throwable} to log, including its stack warn.
      * @since 2.4
      */
-    @SuppressWarnings("removal")
+    @SuppressWarnings("deprecation")
     void warn(Supplier<?> messageSupplier, Throwable throwable);
 
     /**
@@ -4704,12 +4695,7 @@ public interface Logger {
      * @since 2.13.0
      */
     default void logMessage(
-            final Level level,
-            final Marker marker,
-            final String fqcn,
-            final StackTraceElement location,
-            final Message message,
-            final Throwable throwable) {
+            Level level, Marker marker, String fqcn, StackTraceElement location, Message message, Throwable throwable) {
         // noop
     }
 
@@ -4782,7 +4768,7 @@ public interface Logger {
      * @return a LogBuilder.
      * @since 2.13.0
      */
-    default LogBuilder atLevel(final Level level) {
+    default LogBuilder atLevel(Level level) {
         return LogBuilder.NOOP;
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/Marker.java b/log4j-api/src/main/java/org/apache/logging/log4j/Marker.java
index 049a12df4b..9be36461eb 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/Marker.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/Marker.java
@@ -16,7 +16,7 @@
  */
 package org.apache.logging.log4j;
 
-import aQute.bnd.annotation.baseline.BaselineIgnore;
+import java.io.Serializable;
 
 /**
  * Markers are objects that are used to add easily filterable information to log messages.
@@ -25,8 +25,7 @@ import aQute.bnd.annotation.baseline.BaselineIgnore;
  * more specific categories. An example might be a Marker named "Error" with children named "SystemError" and
  * "ApplicationError".
  */
-@BaselineIgnore("3.0.0")
-public interface Marker {
+public interface Marker extends Serializable {
 
     /**
      * Adds a Marker as a parent to this Marker.
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java b/log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java
index f64ed2b386..9cef9688e8 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java
@@ -16,12 +16,10 @@
  */
 package org.apache.logging.log4j;
 
-import aQute.bnd.annotation.baseline.BaselineIgnore;
+import com.google.errorprone.annotations.InlineMe;
 import java.util.Arrays;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
 import org.apache.logging.log4j.util.PerformanceSensitive;
 import org.apache.logging.log4j.util.StringBuilderFormattable;
 
@@ -70,6 +68,41 @@ public final class MarkerManager {
         return result;
     }
 
+    /**
+     * Retrieves or creates a Marker with the specified parent. The parent must have been previously created.
+     *
+     * @param name The name of the Marker.
+     * @param parent The name of the parent Marker.
+     * @return The Marker with the specified name.
+     * @throws IllegalArgumentException if the parent Marker does not exist.
+     * @deprecated Use the Marker add or set methods to add parent Markers. Will be removed by final GA release.
+     */
+    @Deprecated
+    public static Marker getMarker(final String name, final String parent) {
+        final Marker parentMarker = MARKERS.get(parent);
+        if (parentMarker == null) {
+            throw new IllegalArgumentException("Parent Marker " + parent + " has not been defined");
+        }
+        return getMarker(name).addParents(parentMarker);
+    }
+
+    /**
+     * Retrieves or creates a Marker with the specified parent.
+     *
+     * @param name The name of the Marker.
+     * @param parent The parent Marker.
+     * @return The Marker with the specified name.
+     * @throws IllegalArgumentException if any argument is {@code null}
+     * @deprecated Use the Marker add or set methods to add parent Markers. Will be removed by final GA release.
+     */
+    @InlineMe(
+            replacement = "MarkerManager.getMarker(name).addParents(parent)",
+            imports = "org.apache.logging.log4j.MarkerManager")
+    @Deprecated
+    public static Marker getMarker(final String name, final Marker parent) {
+        return getMarker(name).addParents(parent);
+    }
+
     /**
      * <em>Consider this class private, it is only public to satisfy Jackson for XML and JSON IO.</em>
      * <p>
@@ -81,15 +114,14 @@ public final class MarkerManager {
      * is moved to this package and would of course stay in its current module.</em>
      * </p>
      */
-    @BaselineIgnore("3.0.0")
     public static class Log4jMarker implements Marker, StringBuilderFormattable {
 
+        private static final long serialVersionUID = 100L;
+
         private final String name;
 
         private volatile Marker[] parents;
 
-        private final Lock lock = new ReentrantLock();
-
         /**
          * Required by JAXB and Jackson for XML and JSON IO.
          */
@@ -113,81 +145,73 @@ public final class MarkerManager {
             this.parents = null;
         }
 
+        // TODO: use java.util.concurrent
+
         @Override
-        public Marker addParents(final Marker... parentMarkers) {
+        public synchronized Marker addParents(final Marker... parentMarkers) {
             requireNonNull(parentMarkers, "A parent marker must be specified");
-            lock.lock();
-            try {
-                // It is not strictly necessary to copy the variable here, but it should perform better than
-                // accessing a volatile variable multiple times.
-                final Marker[] localParents = this.parents;
-                // Don't add a parent that is already in the hierarchy.
-                int count = 0;
-                int size = parentMarkers.length;
-                if (localParents != null) {
-                    for (final Marker parent : parentMarkers) {
-                        if (!(contains(parent, localParents) || parent.isInstanceOf(this))) {
-                            ++count;
-                        }
-                    }
-                    if (count == 0) {
-                        return this;
+            // It is not strictly necessary to copy the variable here but it should perform better than
+            // Accessing a volatile variable multiple times.
+            final Marker[] localParents = this.parents;
+            // Don't add a parent that is already in the hierarchy.
+            int count = 0;
+            int size = parentMarkers.length;
+            if (localParents != null) {
+                for (final Marker parent : parentMarkers) {
+                    if (!(contains(parent, localParents) || parent.isInstanceOf(this))) {
+                        ++count;
                     }
-                    size = localParents.length + count;
                 }
-                final Marker[] markers = new Marker[size];
-                if (localParents != null) {
-                    // It's perfectly OK to call arraycopy in a synchronized context; it's still faster
-                    // noinspection CallToNativeMethodWhileLocked
-                    System.arraycopy(localParents, 0, markers, 0, localParents.length);
+                if (count == 0) {
+                    return this;
                 }
-                int index = localParents == null ? 0 : localParents.length;
-                for (final Marker parent : parentMarkers) {
-                    if (localParents == null || !(contains(parent, localParents) || parent.isInstanceOf(this))) {
-                        markers[index++] = parent;
-                    }
+                size = localParents.length + count;
+            }
+            final Marker[] markers = new Marker[size];
+            if (localParents != null) {
+                // It's perfectly OK to call arraycopy in a synchronized context; it's still faster
+                // noinspection CallToNativeMethodWhileLocked
+                System.arraycopy(localParents, 0, markers, 0, localParents.length);
+            }
+            int index = localParents == null ? 0 : localParents.length;
+            for (final Marker parent : parentMarkers) {
+                if (localParents == null || !(contains(parent, localParents) || parent.isInstanceOf(this))) {
+                    markers[index++] = parent;
                 }
-                this.parents = markers;
-            } finally {
-                lock.unlock();
             }
+            this.parents = markers;
             return this;
         }
 
         @Override
-        public boolean remove(final Marker parent) {
+        public synchronized boolean remove(final Marker parent) {
             requireNonNull(parent, "A parent marker must be specified");
-            lock.lock();
-            try {
-                final Marker[] localParents = this.parents;
-                if (localParents == null) {
-                    return false;
-                }
-                final int localParentsLength = localParents.length;
-                if (localParentsLength == 1) {
-                    if (localParents[0].equals(parent)) {
-                        parents = null;
-                        return true;
-                    }
-                    return false;
+            final Marker[] localParents = this.parents;
+            if (localParents == null) {
+                return false;
+            }
+            final int localParentsLength = localParents.length;
+            if (localParentsLength == 1) {
+                if (localParents[0].equals(parent)) {
+                    parents = null;
+                    return true;
                 }
-                int index = 0;
-                final Marker[] markers = new Marker[localParentsLength - 1];
-                // noinspection ForLoopReplaceableByForEach
-                for (int i = 0; i < localParentsLength; i++) {
-                    final Marker marker = localParents[i];
-                    if (!marker.equals(parent)) {
-                        if (index == localParentsLength - 1) {
-                            // no need to swap array
-                            return false;
-                        }
-                        markers[index++] = marker;
+                return false;
+            }
+            int index = 0;
+            final Marker[] markers = new Marker[localParentsLength - 1];
+            // noinspection ForLoopReplaceableByForEach
+            for (int i = 0; i < localParentsLength; i++) {
+                final Marker marker = localParents[i];
+                if (!marker.equals(parent)) {
+                    if (index == localParentsLength - 1) {
+                        // no need to swap array
+                        return false;
                     }
+                    markers[index++] = marker;
                 }
-                parents = markers;
-            } finally {
-                lock.unlock();
             }
+            parents = markers;
             return true;
         }
 
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 8a5a6a6b18..9d5e785386 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
@@ -16,28 +16,31 @@
  */
 package org.apache.logging.log4j;
 
-import aQute.bnd.annotation.baseline.BaselineIgnore;
+import java.io.Serializable;
 import java.util.AbstractCollection;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
+import java.util.NoSuchElementException;
 import org.apache.logging.log4j.message.ParameterizedMessage;
-import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.spi.CleanableThreadContextMap;
+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.ThreadContextMap;
+import org.apache.logging.log4j.spi.ThreadContextMap2;
+import org.apache.logging.log4j.spi.ThreadContextMapFactory;
 import org.apache.logging.log4j.spi.ThreadContextStack;
-import org.apache.logging.log4j.util.InternalApi;
-import org.apache.logging.log4j.util.Strings;
+import org.apache.logging.log4j.util.PropertiesUtil;
 
 /**
  * The ThreadContext allows applications to store information either in a Map or a Stack.
  * <p>
  * <b><em>The MDC is managed on a per thread basis</em></b>. To enable automatic inheritance of <i>copies</i> of the MDC
- * to newly created threads, enable the {@link LoggingSystemProperty#THREAD_CONTEXT_MAP_INHERITABLE}
+ * to newly created threads, enable the {@value org.apache.logging.log4j.spi.DefaultThreadContextMap#INHERITABLE_MAP}
  * Log4j system property.
  * </p>
  * @see <a href="https://logging.apache.org/log4j/2.x/manual/thread-context.html">Thread Context Manual</a>
@@ -49,14 +52,18 @@ public final class ThreadContext {
      */
     private static class EmptyThreadContextStack extends AbstractCollection<String> implements ThreadContextStack {
 
+        private static final long serialVersionUID = 1L;
+
+        private static final Iterator<String> EMPTY_ITERATOR = new EmptyIterator<>();
+
         @Override
         public String pop() {
-            return Strings.EMPTY;
+            return null;
         }
 
         @Override
         public String peek() {
-            return Strings.EMPTY;
+            return null;
         }
 
         @Override
@@ -128,7 +135,7 @@ public final class ThreadContext {
 
         @Override
         public Iterator<String> iterator() {
-            return Collections.emptyIterator();
+            return EMPTY_ITERATOR;
         }
 
         @Override
@@ -142,6 +149,29 @@ public final class ThreadContext {
         }
     }
 
+    /**
+     * An empty iterator. Since Java 1.7 added the Collections.emptyIterator() method, we have to make do.
+     *
+     * @param <E> the type of the empty iterator
+     */
+    private static class EmptyIterator<E> implements Iterator<E> {
+
+        @Override
+        public boolean hasNext() {
+            return false;
+        }
+
+        @Override
+        public E next() {
+            throw new NoSuchElementException("This is an empty iterator!");
+        }
+
+        @Override
+        public void remove() {
+            // no-op
+        }
+    }
+
     /**
      * Empty, immutable Map.
      */
@@ -157,6 +187,11 @@ public final class ThreadContext {
     @SuppressWarnings("PublicStaticCollectionField")
     public static final ThreadContextStack EMPTY_STACK = new EmptyThreadContextStack();
 
+    private static final String DISABLE_MAP = "log4j2.disableThreadContextMap";
+    private static final String DISABLE_STACK = "log4j2.disableThreadContextStack";
+    private static final String DISABLE_ALL = "log4j2.disableThreadContext";
+
+    private static boolean useStack;
     private static ThreadContextMap contextMap;
     private static ThreadContextStack contextStack;
     private static ReadOnlyThreadContextMap readOnlyContextMap;
@@ -172,10 +207,20 @@ public final class ThreadContext {
     /**
      * <em>Consider private, used for testing.</em>
      */
-    @InternalApi
     public static void init() {
-        contextMap = LoggingSystem.createContextMap();
-        contextStack = LoggingSystem.createContextStack();
+        ThreadContextMapFactory.init();
+        contextMap = null;
+        final PropertiesUtil managerProps = PropertiesUtil.getProperties();
+        final boolean disableAll = managerProps.getBooleanProperty(DISABLE_ALL);
+        useStack = !(managerProps.getBooleanProperty(DISABLE_STACK) || disableAll);
+        final boolean useMap = !(managerProps.getBooleanProperty(DISABLE_MAP) || disableAll);
+
+        contextStack = new DefaultThreadContextStack(useStack);
+        if (!useMap) {
+            contextMap = new NoOpThreadContextMap();
+        } else {
+            contextMap = ThreadContextMapFactory.createThreadContextMap();
+        }
         if (contextMap instanceof ReadOnlyThreadContextMap) {
             readOnlyContextMap = (ReadOnlyThreadContextMap) contextMap;
         } else {
@@ -226,7 +271,15 @@ public final class ThreadContext {
      * @since 2.7
      */
     public static void putAll(final Map<String, String> m) {
-        contextMap.putAll(m);
+        if (contextMap instanceof ThreadContextMap2) {
+            ((ThreadContextMap2) contextMap).putAll(m);
+        } else if (contextMap instanceof DefaultThreadContextMap) {
+            ((DefaultThreadContextMap) contextMap).putAll(m);
+        } else {
+            for (final Map.Entry<String, String> entry : m.entrySet()) {
+                contextMap.put(entry.getKey(), entry.getValue());
+            }
+        }
     }
 
     /**
@@ -260,7 +313,15 @@ public final class ThreadContext {
      * @since 2.8
      */
     public static void removeAll(final Iterable<String> keys) {
-        contextMap.removeAll(keys);
+        if (contextMap instanceof CleanableThreadContextMap) {
+            ((CleanableThreadContextMap) contextMap).removeAll(keys);
+        } else if (contextMap instanceof DefaultThreadContextMap) {
+            ((DefaultThreadContextMap) contextMap).removeAll(keys);
+        } else {
+            for (final String key : keys) {
+                contextMap.remove(key);
+            }
+        }
     }
 
     /**
@@ -311,8 +372,16 @@ public final class ThreadContext {
      * Returns a read-only view of the internal data structure used to store thread context key-value pairs,
      * or {@code null} if the internal data structure does not implement the
      * {@code ReadOnlyThreadContextMap} interface.
+     * <p>
+     * The {@link DefaultThreadContextMap} implementation does not implement {@code ReadOnlyThreadContextMap}, so by
+     * default this method returns {@code null}.
+     * </p>
      *
      * @return the internal data structure used to store thread context key-value pairs or {@code null}
+     * @see ThreadContextMapFactory
+     * @see DefaultThreadContextMap
+     * @see org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap
+     * @see org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap
      * @since 2.8
      */
     public static ReadOnlyThreadContextMap getThreadContextMap() {
@@ -358,11 +427,9 @@ public final class ThreadContext {
      * Sets this thread's stack.
      *
      * @param stack The stack to use.
-     * @throws NullPointerException if stack is null
      */
     public static void setStack(final Collection<String> stack) {
-        Objects.requireNonNull(stack, "No stack provided");
-        if (stack.isEmpty()) {
+        if (stack.isEmpty() || !useStack) {
             return;
         }
         contextStack.clear();
@@ -416,7 +483,6 @@ public final class ThreadContext {
      * </p>
      *
      * @param message The new diagnostic context information.
-     * @throws UnsupportedOperationException if the context stack is disabled
      */
     public static void push(final String message) {
         contextStack.push(message);
@@ -433,7 +499,6 @@ public final class ThreadContext {
      *
      * @param message The new diagnostic context information.
      * @param args Parameters for the message.
-     * @throws UnsupportedOperationException if the context stack is disabled
      */
     public static void push(final String message, final Object... args) {
         contextStack.push(ParameterizedMessage.format(message, args));
@@ -490,7 +555,6 @@ public final class ThreadContext {
      *
      * @see #getDepth
      * @param depth The number of elements to keep.
-     * @throws IllegalArgumentException if depth is negative
      */
     public static void trim(final int depth) {
         contextStack.trim(depth);
@@ -499,20 +563,20 @@ public final class ThreadContext {
     /**
      * The ThreadContext Stack interface.
      */
-    @BaselineIgnore("3.0.0")
-    public interface ContextStack extends Collection<String> {
+    public interface ContextStack extends Serializable, Collection<String> {
 
         /**
-         * Returns the element at the top of the stack. If the stack is empty, then the empty string is returned.
+         * Returns the element at the top of the stack.
          *
          * @return The element at the top of the stack.
+         * @throws java.util.NoSuchElementException if the stack is empty.
          */
         String pop();
 
         /**
-         * Returns the element at the top of the stack without removing it or the empty string if the stack is empty.
+         * Returns the element at the top of the stack without removing it or null if the stack is empty.
          *
-         * @return the element at the top of the stack or the empty string if the stack is empty.
+         * @return the element at the top of the stack or null if the stack is empty.
          */
         String peek();
 
@@ -520,8 +584,6 @@ public final class ThreadContext {
          * Pushes an element onto the stack.
          *
          * @param message The element to add.
-         * @throws NullPointerException if message is null
-         * @throws UnsupportedOperationException if the context stack is disabled
          */
         void push(String message);
 
@@ -543,7 +605,6 @@ public final class ThreadContext {
          * Trims elements from the end of the stack.
          *
          * @param depth The maximum number of items in the stack to keep.
-         * @throws IllegalArgumentException if depth is negative
          */
         void trim(int depth);
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/DefaultLogBuilder.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/DefaultLogBuilder.java
index e0e6b4362c..608f0ad21e 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/DefaultLogBuilder.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/DefaultLogBuilder.java
@@ -20,38 +20,39 @@ import java.util.Arrays;
 import org.apache.logging.log4j.BridgeAware;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogBuilder;
+import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.logging.log4j.spi.ExtendedLogger;
-import org.apache.logging.log4j.spi.recycler.Recycler;
-import org.apache.logging.log4j.spi.recycler.RecyclerAware;
-import org.apache.logging.log4j.util.InternalApi;
+import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.LambdaUtil;
 import org.apache.logging.log4j.util.StackLocatorUtil;
-import org.apache.logging.log4j.util.Strings;
 import org.apache.logging.log4j.util.Supplier;
 
 /**
  * Collects data for a log event and then logs it. This class should be considered private.
  */
-@InternalApi
-public class DefaultLogBuilder implements BridgeAware, LogBuilder, RecyclerAware<LogBuilder> {
+public class DefaultLogBuilder implements BridgeAware, LogBuilder {
 
+    private static Message EMPTY_MESSAGE = new SimpleMessage("");
     private static final String FQCN = DefaultLogBuilder.class.getName();
-    private static final Message EMPTY_MESSAGE = new SimpleMessage(Strings.EMPTY);
+    private static final Logger LOGGER = StatusLogger.getLogger();
 
     private ExtendedLogger logger;
     private Level level;
     private Marker marker;
     private Throwable throwable;
     private StackTraceElement location;
+    private volatile boolean inUse;
+    private long threadId;
     private String fqcn = FQCN;
-    private Recycler<LogBuilder> recycler;
 
     public DefaultLogBuilder(final ExtendedLogger logger, final Level level) {
         this.logger = logger;
         this.level = level;
+        this.threadId = Thread.currentThread().getId();
+        this.inUse = level != null;
     }
 
     public DefaultLogBuilder() {
@@ -74,6 +75,7 @@ public class DefaultLogBuilder implements BridgeAware, LogBuilder, RecyclerAware
         this.marker = null;
         this.throwable = null;
         this.location = null;
+        this.inUse = true;
         return this;
     }
 
@@ -96,14 +98,18 @@ public class DefaultLogBuilder implements BridgeAware, LogBuilder, RecyclerAware
     }
 
     @Override
-    public LogBuilder withLocation(final StackTraceElement location) {
+    public LogBuilder withLocation(StackTraceElement location) {
         this.location = location;
         return this;
     }
 
+    public boolean isInUse() {
+        return inUse;
+    }
+
     @Override
-    public void log(final Message message) {
-        if (isEnabled(message)) {
+    public void log(Message message) {
+        if (isValid() && isEnabled(message)) {
             logMessage(message);
         }
     }
@@ -112,7 +118,7 @@ public class DefaultLogBuilder implements BridgeAware, LogBuilder, RecyclerAware
     @SuppressWarnings("deprecation")
     public Message logAndGet(final Supplier<Message> messageSupplier) {
         Message message = null;
-        if (isEnabled(message = messageSupplier.get())) {
+        if (isValid() && isEnabled(message = messageSupplier.get())) {
             logMessage(message);
         }
         return message;
@@ -120,21 +126,21 @@ public class DefaultLogBuilder implements BridgeAware, LogBuilder, RecyclerAware
 
     @Override
     public void log(final CharSequence message) {
-        if (isEnabled(message)) {
+        if (isValid() && isEnabled(message)) {
             logMessage(logger.getMessageFactory().newMessage(message));
         }
     }
 
     @Override
-    public void log(final String message) {
-        if (isEnabled(message)) {
+    public void log(String message) {
+        if (isValid() && isEnabled(message)) {
             logMessage(logger.getMessageFactory().newMessage(message));
         }
     }
 
     @Override
-    public void log(final String message, final Object... params) {
-        if (isEnabled(message, params)) {
+    public void log(String message, Object... params) {
+        if (isValid() && isEnabled(message, params)) {
             logMessage(logger.getMessageFactory().newMessage(message, params));
         }
     }
@@ -143,7 +149,7 @@ public class DefaultLogBuilder implements BridgeAware, LogBuilder, RecyclerAware
     @SuppressWarnings("deprecation")
     public void log(String message, Supplier<?>... params) {
         final Object[] objs;
-        if (isEnabled(message, objs = LambdaUtil.getAll(params))) {
+        if (isValid() && isEnabled(message, objs = LambdaUtil.getAll(params))) {
             logMessage(logger.getMessageFactory().newMessage(message, objs));
         }
     }
@@ -155,147 +161,129 @@ public class DefaultLogBuilder implements BridgeAware, LogBuilder, RecyclerAware
     }
 
     @Override
-    public void log(final Object message) {
-        if (isEnabled(message)) {
+    public void log(Object message) {
+        if (isValid() && isEnabled(message)) {
             logMessage(logger.getMessageFactory().newMessage(message));
         }
     }
 
     @Override
-    public void log(final String message, final Object p0) {
-        if (isEnabled(message, p0)) {
+    public void log(String message, Object p0) {
+        if (isValid() && isEnabled(message, p0)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0));
         }
     }
 
     @Override
-    public void log(final String message, final Object p0, final Object p1) {
-        if (isEnabled(message, p0, p1)) {
+    public void log(String message, Object p0, Object p1) {
+        if (isValid() && isEnabled(message, p0, p1)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0, p1));
         }
     }
 
     @Override
-    public void log(final String message, final Object p0, final Object p1, final Object p2) {
-        if (isEnabled(message, p0, p1, p2)) {
+    public void log(String message, Object p0, Object p1, Object p2) {
+        if (isValid() && isEnabled(message, p0, p1, p2)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0, p1, p2));
         }
     }
 
     @Override
-    public void log(final String message, final Object p0, final Object p1, final Object p2, final Object p3) {
-        if (isEnabled(message, p0, p1, p2, p3)) {
+    public void log(String message, Object p0, Object p1, Object p2, Object p3) {
+        if (isValid() && isEnabled(message, p0, p1, p2, p3)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0, p1, p2, p3));
         }
     }
 
     @Override
-    public void log(
-            final String message, final Object p0, final Object p1, final Object p2, final Object p3, final Object p4) {
-        if (isEnabled(message, p0, p1, p2, p3, p4)) {
+    public void log(String message, Object p0, Object p1, Object p2, Object p3, Object p4) {
+        if (isValid() && isEnabled(message, p0, p1, p2, p3, p4)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0, p1, p2, p3, p4));
         }
     }
 
     @Override
-    public void log(
-            final String message,
-            final Object p0,
-            final Object p1,
-            final Object p2,
-            final Object p3,
-            final Object p4,
-            final Object p5) {
-        if (isEnabled(message, p0, p1, p2, p3, p4, p5)) {
+    public void log(String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5) {
+        if (isValid() && isEnabled(message, p0, p1, p2, p3, p4, p5)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0, p1, p2, p3, p4, p5));
         }
     }
 
     @Override
-    public void log(
-            final String message,
-            final Object p0,
-            final Object p1,
-            final Object p2,
-            final Object p3,
-            final Object p4,
-            final Object p5,
-            final Object p6) {
-        if (isEnabled(message, p0, p1, p2, p3, p4, p5, p6)) {
+    public void log(String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) {
+        if (isValid() && isEnabled(message, p0, p1, p2, p3, p4, p5, p6)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0, p1, p2, p3, p4, p5, p6));
         }
     }
 
     @Override
     public void log(
-            final String message,
-            final Object p0,
-            final Object p1,
-            final Object p2,
-            final Object p3,
-            final Object p4,
-            final Object p5,
-            final Object p6,
-            final Object p7) {
-        if (isEnabled(message, p0, p1, p2, p3, p4, p5, p6, p7)) {
+            String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) {
+        if (isValid() && isEnabled(message, p0, p1, p2, p3, p4, p5, p6, p7)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0, p1, p2, p3, p4, p5, p6, p7));
         }
     }
 
     @Override
     public void log(
-            final String message,
-            final Object p0,
-            final Object p1,
-            final Object p2,
-            final Object p3,
-            final Object p4,
-            final Object p5,
-            final Object p6,
-            final Object p7,
-            final Object p8) {
-        if (isEnabled(message, p0, p1, p2, p3, p4, p5, p6, p7, p8)) {
+            String message,
+            Object p0,
+            Object p1,
+            Object p2,
+            Object p3,
+            Object p4,
+            Object p5,
+            Object p6,
+            Object p7,
+            Object p8) {
+        if (isValid() && isEnabled(message, p0, p1, p2, p3, p4, p5, p6, p7, p8)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0, p1, p2, p3, p4, p5, p6, p7, p8));
         }
     }
 
     @Override
     public void log(
-            final String message,
-            final Object p0,
-            final Object p1,
-            final Object p2,
-            final Object p3,
-            final Object p4,
-            final Object p5,
-            final Object p6,
-            final Object p7,
-            final Object p8,
-            final Object p9) {
-        if (isEnabled(message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9)) {
+            String message,
+            Object p0,
+            Object p1,
+            Object p2,
+            Object p3,
+            Object p4,
+            Object p5,
+            Object p6,
+            Object p7,
+            Object p8,
+            Object p9) {
+        if (isValid() && isEnabled(message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9)) {
             logMessage(logger.getMessageFactory().newMessage(message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9));
         }
     }
 
     @Override
     public void log() {
-        if (isEnabled(EMPTY_MESSAGE)) {
+        if (isValid() && isEnabled(EMPTY_MESSAGE)) {
             logMessage(EMPTY_MESSAGE);
         }
     }
 
-    private void logMessage(final Message message) {
+    private void logMessage(Message message) {
         try {
             logger.logMessage(level, marker, fqcn, location, message, throwable);
         } finally {
-            this.level = null;
-            this.marker = null;
-            this.throwable = null;
-            this.location = null;
-            if (recycler != null) {
-                recycler.release(this);
-            }
+            inUse = false;
+        }
+    }
+
+    private boolean isValid() {
+        if (!inUse) {
+            LOGGER.warn("Attempt to reuse LogBuilder was ignored. {}", StackLocatorUtil.getCallerClass(2));
+            return false;
         }
+        if (this.threadId != Thread.currentThread().getId()) {
+            LOGGER.warn("LogBuilder can only be used on the owning thread. {}", StackLocatorUtil.getCallerClass(2));
+            return false;
+        }
+        return true;
     }
 
     protected boolean isEnabled(Message message) {
@@ -407,9 +395,4 @@ public class DefaultLogBuilder implements BridgeAware, LogBuilder, RecyclerAware
                 ? logger.isEnabled(level, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, throwable)
                 : logger.isEnabled(level, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
     }
-
-    @Override
-    public void setRecycler(Recycler<LogBuilder> recycler) {
-        this.recycler = recycler;
-    }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/AbstractMessageFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/AbstractMessageFactory.java
index cfefaded66..c39c65becc 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/AbstractMessageFactory.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/AbstractMessageFactory.java
@@ -16,11 +16,175 @@
  */
 package org.apache.logging.log4j.message;
 
+import java.io.Serializable;
+
 /**
- * Provides an abstract superclass for {@link MessageFactory}. This class is now unnecessary as all default
- * methods are provided by the (@link MessageFactory) interface.
- *
- * @deprecated MessageFactory has default methods that implement all the methods that were here.
+ * Provides an abstract superclass for {@link MessageFactory2} implementations with default implementations (and for
+ * {@link MessageFactory} by extension).
+ * <p>
+ * This class is immutable.
+ * </p>
+ * <p>
+ * <strong>Note to implementors:</strong>
+ * </p>
+ * <p>
+ * Subclasses can implement the {@link MessageFactory2} methods when they can most effectively build {@link Message}
+ * instances. If a subclass does not implement {@link MessageFactory2} methods, these calls are routed through
+ * {@link #newMessage(String, Object...)} in this class.
+ * </p>
  */
-@Deprecated(forRemoval = true, since = "3.0")
-public abstract class AbstractMessageFactory implements MessageFactory {}
+public abstract class AbstractMessageFactory implements MessageFactory2, Serializable {
+    private static final long serialVersionUID = -1307891137684031187L;
+
+    @Override
+    public Message newMessage(final CharSequence message) {
+        return new SimpleMessage(message);
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see org.apache.logging.log4j.message.MessageFactory#newMessage(java.lang.Object)
+     */
+    @Override
+    public Message newMessage(final Object message) {
+        return new ObjectMessage(message);
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see org.apache.logging.log4j.message.MessageFactory#newMessage(java.lang.String)
+     */
+    @Override
+    public Message newMessage(final String message) {
+        return new SimpleMessage(message);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(final String message, final Object p0) {
+        return newMessage(message, new Object[] {p0});
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(final String message, final Object p0, final Object p1) {
+        return newMessage(message, new Object[] {p0, p1});
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(final String message, final Object p0, final Object p1, final Object p2) {
+        return newMessage(message, new Object[] {p0, p1, p2});
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message, final Object p0, final Object p1, final Object p2, final Object p3) {
+        return newMessage(message, new Object[] {p0, p1, p2, p3});
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message, final Object p0, final Object p1, final Object p2, final Object p3, final Object p4) {
+        return newMessage(message, new Object[] {p0, p1, p2, p3, p4});
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5) {
+        return newMessage(message, new Object[] {p0, p1, p2, p3, p4, p5});
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6) {
+        return newMessage(message, new Object[] {p0, p1, p2, p3, p4, p5, p6});
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7) {
+        return newMessage(message, new Object[] {p0, p1, p2, p3, p4, p5, p6, p7});
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7,
+            final Object p8) {
+        return newMessage(message, new Object[] {p0, p1, p2, p3, p4, p5, p6, p7, p8});
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7,
+            final Object p8,
+            final Object p9) {
+        return newMessage(message, new Object[] {p0, p1, p2, p3, p4, p5, p6, p7, p8, p9});
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory2.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory2.java
index 5cdbb61cc8..755f89307c 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory2.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory2.java
@@ -19,8 +19,182 @@ package org.apache.logging.log4j.message;
 /**
  * Creates messages. Implementations can provide different message format syntaxes.
  *
- * @deprecated MessageFactory now contains all interface methods.
+ * @see ParameterizedMessageFactory
  * @since 2.6
  */
-@Deprecated(forRemoval = true, since = "3.0")
-public interface MessageFactory2 extends MessageFactory {}
+public interface MessageFactory2 extends MessageFactory {
+
+    /**
+     * Creates a new message for the specified CharSequence.
+     * @param charSequence the (potentially mutable) CharSequence
+     * @return a new message for the specified CharSequence
+     */
+    Message newMessage(CharSequence charSequence);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(String message, Object p0);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @param p1 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(String message, Object p0, Object p1);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @param p1 a message parameter
+     * @param p2 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(String message, Object p0, Object p1, Object p2);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @param p1 a message parameter
+     * @param p2 a message parameter
+     * @param p3 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @param p1 a message parameter
+     * @param p2 a message parameter
+     * @param p3 a message parameter
+     * @param p4 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3, Object p4);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @param p1 a message parameter
+     * @param p2 a message parameter
+     * @param p3 a message parameter
+     * @param p4 a message parameter
+     * @param p5 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @param p1 a message parameter
+     * @param p2 a message parameter
+     * @param p3 a message parameter
+     * @param p4 a message parameter
+     * @param p5 a message parameter
+     * @param p6 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @param p1 a message parameter
+     * @param p2 a message parameter
+     * @param p3 a message parameter
+     * @param p4 a message parameter
+     * @param p5 a message parameter
+     * @param p6 a message parameter
+     * @param p7 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(
+            String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @param p1 a message parameter
+     * @param p2 a message parameter
+     * @param p3 a message parameter
+     * @param p4 a message parameter
+     * @param p5 a message parameter
+     * @param p6 a message parameter
+     * @param p7 a message parameter
+     * @param p8 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(
+            String message,
+            Object p0,
+            Object p1,
+            Object p2,
+            Object p3,
+            Object p4,
+            Object p5,
+            Object p6,
+            Object p7,
+            Object p8);
+
+    /**
+     * Creates a new parameterized message.
+     *
+     * @param message a message template, the kind of message template depends on the implementation.
+     * @param p0 a message parameter
+     * @param p1 a message parameter
+     * @param p2 a message parameter
+     * @param p3 a message parameter
+     * @param p4 a message parameter
+     * @param p5 a message parameter
+     * @param p6 a message parameter
+     * @param p7 a message parameter
+     * @param p8 a message parameter
+     * @param p9 a message parameter
+     * @return a new message
+     * @see ParameterizedMessageFactory
+     */
+    Message newMessage(
+            String message,
+            Object p0,
+            Object p1,
+            Object p2,
+            Object p3,
+            Object p4,
+            Object p5,
+            Object p6,
+            Object p7,
+            Object p8,
+            Object p9);
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessageFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessageFactory.java
new file mode 100644
index 0000000000..4df77e3bca
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessageFactory.java
@@ -0,0 +1,192 @@
+/*
+ * 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.message;
+
+/**
+ * Creates {@link FormattedMessage} instances for {@link MessageFactory2} methods (and {@link MessageFactory} by
+ * extension.)
+ * <p>
+ * Enables the use of <code>{}</code> parameter markers in message strings.
+ * </p>
+ * <p>
+ * Creates {@link ParameterizedMessage} instances for {@link #newMessage(String, Object...)}.
+ * </p>
+ * <p>
+ * This class is immutable.
+ * </p>
+ *
+ * <p>
+ * <strong>Note to implementors:</strong>
+ * </p>
+ * <p>
+ * This class implements all {@link MessageFactory2} methods.
+ * </p>
+ */
+public final class ParameterizedMessageFactory extends AbstractMessageFactory {
+    /**
+     * Instance of ParameterizedMessageFactory.
+     */
+    public static final ParameterizedMessageFactory INSTANCE = new ParameterizedMessageFactory();
+
+    private static final long serialVersionUID = -8970940216592525651L;
+
+    /**
+     * Constructs a message factory.
+     */
+    public ParameterizedMessageFactory() {}
+
+    /**
+     * Creates {@link ParameterizedMessage} instances.
+     *
+     * @param message The message pattern.
+     * @param params The message parameters.
+     * @return The Message.
+     *
+     * @see MessageFactory#newMessage(String, Object...)
+     */
+    @Override
+    public Message newMessage(final String message, final Object... params) {
+        return new ParameterizedMessage(message, params);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(final String message, final Object p0) {
+        return new ParameterizedMessage(message, p0);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(final String message, final Object p0, final Object p1) {
+        return new ParameterizedMessage(message, p0, p1);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(final String message, final Object p0, final Object p1, final Object p2) {
+        return new ParameterizedMessage(message, p0, p1, p2);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message, final Object p0, final Object p1, final Object p2, final Object p3) {
+        return new ParameterizedMessage(message, p0, p1, p2, p3);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message, final Object p0, final Object p1, final Object p2, final Object p3, final Object p4) {
+        return new ParameterizedMessage(message, p0, p1, p2, p3, p4);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5) {
+        return new ParameterizedMessage(message, p0, p1, p2, p3, p4, p5);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6) {
+        return new ParameterizedMessage(message, p0, p1, p2, p3, p4, p5, p6);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7) {
+        return new ParameterizedMessage(message, p0, p1, p2, p3, p4, p5, p6, p7);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7,
+            final Object p8) {
+        return new ParameterizedMessage(message, p0, p1, p2, p3, p4, p5, p6, p7, p8);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7,
+            final Object p8,
+            final Object p9) {
+        return new ParameterizedMessage(message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
index f5210e1452..d9543f17b5 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
@@ -16,21 +16,26 @@
  */
 package org.apache.logging.log4j.spi;
 
+import java.io.Serializable;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogBuilder;
 import org.apache.logging.log4j.LoggingException;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.MarkerManager;
 import org.apache.logging.log4j.internal.DefaultLogBuilder;
+import org.apache.logging.log4j.message.DefaultFlowMessageFactory;
 import org.apache.logging.log4j.message.EntryMessage;
 import org.apache.logging.log4j.message.FlowMessageFactory;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.message.MessageFactory2;
+import org.apache.logging.log4j.message.ParameterizedMessageFactory;
+import org.apache.logging.log4j.message.ReusableMessageFactory;
 import org.apache.logging.log4j.message.StringFormattedMessage;
-import org.apache.logging.log4j.spi.recycler.Recycler;
-import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Constants;
 import org.apache.logging.log4j.util.LambdaUtil;
+import org.apache.logging.log4j.util.LoaderUtil;
 import org.apache.logging.log4j.util.MessageSupplier;
 import org.apache.logging.log4j.util.PerformanceSensitive;
 import org.apache.logging.log4j.util.StackLocatorUtil;
@@ -39,7 +44,7 @@ import org.apache.logging.log4j.util.Supplier;
 /**
  * Base implementation of a Logger. It is highly recommended that any Logger implementation extend this class.
  */
-public abstract class AbstractLogger implements ExtendedLogger {
+public abstract class AbstractLogger implements ExtendedLogger, LocationAwareLogger, Serializable {
     // Implementation note: many methods in this class are tuned for performance. MODIFY WITH CARE!
     // Specifically, try to keep the hot methods to 35 bytecodes or less:
     // this is within the MaxInlineSize threshold on Java 7 and Java 8 Hotspot and makes these methods
@@ -77,21 +82,38 @@ public abstract class AbstractLogger implements ExtendedLogger {
     public static final Marker CATCHING_MARKER =
             MarkerManager.getMarker("CATCHING").setParents(EXCEPTION_MARKER);
 
+    /**
+     * The default MessageFactory class.
+     */
+    public static final Class<? extends MessageFactory> DEFAULT_MESSAGE_FACTORY_CLASS =
+            ParameterizedMessageFactory.class;
+
+    /**
+     * The default FlowMessageFactory class.
+     */
+    public static final Class<? extends FlowMessageFactory> DEFAULT_FLOW_MESSAGE_FACTORY_CLASS =
+            DefaultFlowMessageFactory.class;
+
+    private static final long serialVersionUID = 2L;
+
     private static final String FQCN = AbstractLogger.class.getName();
     private static final String THROWING = "Throwing";
     private static final String CATCHING = "Catching";
 
     protected final String name;
-    private final MessageFactory messageFactory;
+    private final MessageFactory2 messageFactory;
     private final FlowMessageFactory flowMessageFactory;
     private static final ThreadLocal<int[]> recursionDepthHolder = new ThreadLocal<>(); // LOG4J2-1518, LOG4J2-2031
-    private final Recycler<DefaultLogBuilder> recycler;
+    private static final ThreadLocal<DefaultLogBuilder> logBuilder = ThreadLocal.withInitial(DefaultLogBuilder::new);
 
     /**
      * Creates a new logger named after this class (or subclass).
      */
     public AbstractLogger() {
-        this(null, null, null, null);
+        final String canonicalName = getClass().getCanonicalName();
+        this.name = canonicalName != null ? canonicalName : getClass().getName();
+        this.messageFactory = createDefaultMessageFactory();
+        this.flowMessageFactory = createDefaultFlowMessageFactory();
     }
 
     /**
@@ -100,7 +122,7 @@ public abstract class AbstractLogger implements ExtendedLogger {
      * @param name the logger name
      */
     public AbstractLogger(final String name) {
-        this(name, LoggingSystem.getMessageFactory(), null, null);
+        this(name, createDefaultMessageFactory());
     }
 
     /**
@@ -110,35 +132,15 @@ public abstract class AbstractLogger implements ExtendedLogger {
      * @param messageFactory the message factory, if null then use the default message factory.
      */
     public AbstractLogger(final String name, final MessageFactory messageFactory) {
-        this(name, messageFactory, null, null);
-    }
-
-    private AbstractLogger(
-            final String name,
-            final MessageFactory messageFactory,
-            final FlowMessageFactory flowMessageFactory,
-            final RecyclerFactory recyclerFactory) {
-        this.name = createNameFromClass(name, getClass());
-        this.messageFactory = messageFactory != null ? messageFactory : LoggingSystem.getMessageFactory();
-        this.flowMessageFactory =
-                flowMessageFactory != null ? flowMessageFactory : LoggingSystem.getFlowMessageFactory();
-        final RecyclerFactory effectiveRecyclerFactory =
-                recyclerFactory != null ? recyclerFactory : LoggingSystem.getRecyclerFactory();
-        this.recycler = effectiveRecyclerFactory.create(DefaultLogBuilder::new);
-    }
-
-    private static String createNameFromClass(String name, Class<?> clazz) {
-        if (name != null) {
-            return name;
-        }
-        final String canonicalName = clazz.getCanonicalName();
-        return canonicalName != null ? canonicalName : clazz.getName();
+        this.name = name;
+        this.messageFactory = messageFactory == null ? createDefaultMessageFactory() : narrow(messageFactory);
+        this.flowMessageFactory = createDefaultFlowMessageFactory();
     }
 
     /**
      * Checks that the message factory a logger was created with is the same as the given messageFactory. If they are
      * different log a warning to the {@linkplain StatusLogger}. A null MessageFactory translates to the default
-     * MessageFactory {@link LoggingSystem#getMessageFactory()}.
+     * MessageFactory {@link #DEFAULT_MESSAGE_FACTORY_CLASS}.
      *
      * @param logger The logger to check
      * @param messageFactory The message factory to check.
@@ -146,7 +148,6 @@ public abstract class AbstractLogger implements ExtendedLogger {
     public static void checkMessageFactory(final ExtendedLogger logger, final MessageFactory messageFactory) {
         final String name = logger.getName();
         final MessageFactory loggerMessageFactory = logger.getMessageFactory();
-        final MessageFactory currentMessageFactory = LoggingSystem.getMessageFactory();
         if (messageFactory != null && !loggerMessageFactory.equals(messageFactory)) {
             StatusLogger.getLogger()
                     .warn(
@@ -155,7 +156,7 @@ public abstract class AbstractLogger implements ExtendedLogger {
                             name,
                             loggerMessageFactory,
                             messageFactory);
-        } else if (messageFactory == null && loggerMessageFactory != currentMessageFactory) {
+        } else if (messageFactory == null && !loggerMessageFactory.getClass().equals(DEFAULT_MESSAGE_FACTORY_CLASS)) {
             StatusLogger.getLogger()
                     .warn(
                             "The Logger {} was created with the message factory {} and is now requested with a null "
@@ -163,7 +164,7 @@ public abstract class AbstractLogger implements ExtendedLogger {
                                     + "formatting.",
                             name,
                             loggerMessageFactory,
-                            currentMessageFactory.getClass().getName());
+                            DEFAULT_MESSAGE_FACTORY_CLASS.getName());
         }
     }
 
@@ -196,6 +197,30 @@ public abstract class AbstractLogger implements ExtendedLogger {
         return messageFactory.newMessage(CATCHING);
     }
 
+    private static MessageFactory2 createDefaultMessageFactory() {
+        try {
+            final MessageFactory result = LoaderUtil.newInstanceOf(DEFAULT_MESSAGE_FACTORY_CLASS);
+            return narrow(result);
+        } catch (final ReflectiveOperationException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private static MessageFactory2 narrow(final MessageFactory result) {
+        if (result instanceof MessageFactory2) {
+            return (MessageFactory2) result;
+        }
+        return new MessageFactory2Adapter(result);
+    }
+
+    private static FlowMessageFactory createDefaultFlowMessageFactory() {
+        try {
+            return LoaderUtil.newInstanceOf(DEFAULT_FLOW_MESSAGE_FACTORY_CLASS);
+        } catch (final ReflectiveOperationException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
     @Override
     public void debug(final Marker marker, final CharSequence message) {
         logIfEnabled(FQCN, Level.DEBUG, marker, message, null);
@@ -557,7 +582,6 @@ public abstract class AbstractLogger implements ExtendedLogger {
      * @param fqcn The fully qualified class name of the <b>caller</b>.
      * @param format Format String for the parameters.
      * @param paramSuppliers The Suppliers of the parameters.
-     * @return The EntryMessage.
      */
     @SuppressWarnings("deprecation")
     protected EntryMessage enter(final String fqcn, final String format, final Supplier<?>... paramSuppliers) {
@@ -573,13 +597,28 @@ public abstract class AbstractLogger implements ExtendedLogger {
         return entryMsg;
     }
 
+    /**
+     * Logs entry to a method with location information.
+     *
+     * @param fqcn The fully qualified class name of the <b>caller</b>.
+     * @param format The format String for the parameters.
+     * @param paramSuppliers The parameters to the method.
+     */
+    @Deprecated
+    protected EntryMessage enter(final String fqcn, final String format, final MessageSupplier... paramSuppliers) {
+        EntryMessage entryMsg = null;
+        if (isEnabled(Level.TRACE, ENTRY_MARKER, (Object) null, null)) {
+            logMessageSafely(fqcn, Level.TRACE, ENTRY_MARKER, entryMsg = entryMsg(format, paramSuppliers), null);
+        }
+        return entryMsg;
+    }
+
     /**
      * Logs entry to a method with location information.
      *
      * @param fqcn The fully qualified class name of the <b>caller</b>.
      * @param format The format String for the parameters.
      * @param params The parameters to the method.
-     * @return The EntryMessage.
      */
     protected EntryMessage enter(final String fqcn, final String format, final Object... params) {
         EntryMessage entryMsg = null;
@@ -598,8 +637,29 @@ public abstract class AbstractLogger implements ExtendedLogger {
      * Logs entry to a method with location information.
      *
      * @param fqcn The fully qualified class name of the <b>caller</b>.
-     * @param message the Message.
-     * @return The EntryMessage.
+     * @param messageSupplier The Supplier of the Message.
+     */
+    @Deprecated
+    protected EntryMessage enter(final String fqcn, final MessageSupplier messageSupplier) {
+        EntryMessage message = null;
+        if (isEnabled(Level.TRACE, ENTRY_MARKER, (Object) null, null)) {
+            logMessageSafely(
+                    fqcn,
+                    Level.TRACE,
+                    ENTRY_MARKER,
+                    message = flowMessageFactory.newEntryMessage(messageSupplier.get()),
+                    null);
+        }
+        return message;
+    }
+
+    /**
+     * Logs entry to a method with location information.
+     *
+     * @param fqcn
+     *            The fully qualified class name of the <b>caller</b>.
+     * @param message
+     *            the Message.
      * @since 2.6
      */
     protected EntryMessage enter(final String fqcn, final Message message) {
@@ -611,6 +671,18 @@ public abstract class AbstractLogger implements ExtendedLogger {
         return flowMessage;
     }
 
+    @Deprecated
+    @Override
+    public void entry() {
+        entry(FQCN, (Object[]) null);
+    }
+
+    @Deprecated
+    @Override
+    public void entry(final Object... params) {
+        entry(FQCN, params);
+    }
+
     /**
      * Logs entry to a method with location information.
      *
@@ -1001,6 +1073,18 @@ public abstract class AbstractLogger implements ExtendedLogger {
         logIfEnabled(FQCN, Level.ERROR, null, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
     }
 
+    @Deprecated
+    @Override
+    public void exit() {
+        exit(FQCN, (Object) null);
+    }
+
+    @Deprecated
+    @Override
+    public <R> R exit(final R result) {
+        return exit(FQCN, result);
+    }
+
     /**
      * Logs exiting from a method with the result and location information.
      *
@@ -1020,7 +1104,6 @@ public abstract class AbstractLogger implements ExtendedLogger {
      * Logs exiting from a method with the result and location information.
      *
      * @param fqcn The fully qualified class name of the <b>caller</b>.
-     * @param format The format string.
      * @param <R> The type of the parameter and object being returned.
      * @param result The result being returned from the method call.
      * @return the return value passed to this method.
@@ -2715,7 +2798,7 @@ public abstract class AbstractLogger implements ExtendedLogger {
             handleLogMessageException(ex, fqcn, message);
         } finally {
             decrementRecursionDepth();
-            messageFactory.recycle(message);
+            ReusableMessageFactory.release(message);
         }
     }
 
@@ -2758,7 +2841,7 @@ public abstract class AbstractLogger implements ExtendedLogger {
             logMessageTrackRecursion(fqcn, level, marker, message, throwable);
         } finally {
             // LOG4J2-1583 prevent scrambled logs when logging calls are nested (logging in toString())
-            messageFactory.recycle(message);
+            ReusableMessageFactory.release(message);
         }
     }
 
@@ -3754,7 +3837,12 @@ public abstract class AbstractLogger implements ExtendedLogger {
      * @since 2.20.0
      */
     protected LogBuilder getLogBuilder(final Level level) {
-        DefaultLogBuilder builder = recycler.acquire();
-        return builder.reset(this, level);
+        if (Constants.ENABLE_THREADLOCALS) {
+            final DefaultLogBuilder builder = logBuilder.get();
+            if (!builder.isInUse()) {
+                return builder.reset(this, level);
+            }
+        }
+        return new DefaultLogBuilder(this, level);
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CleanableThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CleanableThreadContextMap.java
index 9da75e5a37..b5d97454b2 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CleanableThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CleanableThreadContextMap.java
@@ -17,12 +17,22 @@
 package org.apache.logging.log4j.spi;
 
 /**
- * Legacy interface for backward compatibility with extensions to ThreadContextMap.
- * These methods have since been moved to default methods on ThreadContextMap.
+ * Extension service provider interface to implement additional custom MDC behavior for
+ * {@link org.apache.logging.log4j.ThreadContext}.
  *
  * @see ThreadContextMap
  * @since 2.8
- * @deprecated use {@link ThreadContextMap} directly
  */
-@Deprecated(forRemoval = true, since = "3.0")
-public interface CleanableThreadContextMap extends ThreadContextMap2 {}
+public interface CleanableThreadContextMap extends ThreadContextMap2 {
+
+    /**
+     * Removes all given context map keys from the current thread's context map.
+     *
+     * <p>If the current thread does not have a context map it is
+     * created as a side effect.</p>
+     *
+     * @param keys The keys.
+     * @since 2.8
+     */
+    void removeAll(final Iterable<String> keys);
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
index 5b3fef2aee..04fc06b9af 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
@@ -16,10 +16,6 @@
  */
 package org.apache.logging.log4j.spi;
 
-import static org.apache.logging.log4j.spi.LoggingSystem.THREAD_CONTEXT_DEFAULT_INITIAL_CAPACITY;
-import static org.apache.logging.log4j.spi.LoggingSystemProperty.THREAD_CONTEXT_INITIAL_CAPACITY;
-import static org.apache.logging.log4j.spi.LoggingSystemProperty.THREAD_CONTEXT_MAP_INHERITABLE;
-
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -38,7 +34,23 @@ import org.apache.logging.log4j.util.StringMap;
  *
  * @since 2.7
  */
-class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ThreadContextMap, CopyOnWrite {
+class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap, CopyOnWrite {
+
+    /**
+     * 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";
+
+    /**
+     * The default initial capacity.
+     */
+    protected static final int DEFAULT_INITIAL_CAPACITY = 16;
+
+    /**
+     * System property name that can be used to control the data structure's initial capacity.
+     */
+    protected static final String PROPERTY_NAME_INITIAL_CAPACITY = "log4j2.ThreadContext.initial.capacity";
 
     private static final StringMap EMPTY_CONTEXT_DATA = new SortedArrayStringMap(1);
 
@@ -47,13 +59,13 @@ class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
     }
 
     private final ThreadLocal<StringMap> localMap;
-    protected final int initialCapacity;
+    private final int initialCapacity;
 
     public CopyOnWriteSortedArrayThreadContextMap() {
         this(
-                PropertiesUtil.getProperties().getBooleanProperty(THREAD_CONTEXT_MAP_INHERITABLE),
+                PropertiesUtil.getProperties().getBooleanProperty(INHERITABLE_MAP),
                 PropertiesUtil.getProperties()
-                        .getIntegerProperty(THREAD_CONTEXT_INITIAL_CAPACITY, THREAD_CONTEXT_DEFAULT_INITIAL_CAPACITY));
+                        .getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY));
     }
 
     CopyOnWriteSortedArrayThreadContextMap(final boolean inheritableMap, final int initialCapacity) {
@@ -61,9 +73,11 @@ class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
         this.initialCapacity = initialCapacity;
     }
 
+    // LOG4J2-479: by default, use a plain ThreadLocal, only use InheritableThreadLocal if configured.
+    // (This method is package protected for JUnit tests.)
     private ThreadLocal<StringMap> createThreadLocalMap(final boolean inheritableMap) {
         if (inheritableMap) {
-            return new InheritableThreadLocal<>() {
+            return new InheritableThreadLocal<StringMap>() {
                 @Override
                 protected StringMap childValue(final StringMap parentValue) {
                     if (parentValue == null) {
@@ -147,13 +161,13 @@ class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
 
     @Override
     public String get(final String key) {
-        return getValue(key);
+        return (String) getValue(key);
     }
 
     @Override
     public <V> V getValue(final String key) {
         final StringMap map = localMap.get();
-        return map == null ? null : map.getValue(key);
+        return map == null ? null : map.<V>getValue(key);
     }
 
     @Override
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
new file mode 100644
index 0000000000..3c45eee98f
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
@@ -0,0 +1,244 @@
+/*
+ * 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.Collections;
+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;
+
+/**
+ * The actual ThreadContext Map. A new ThreadContext Map is created each time it is updated and the Map stored is always
+ * immutable. This means the Map can be passed to other threads without concern that it will be updated. Since it is
+ * expected that the Map will be passed to many more log events than the number of keys it contains the performance
+ * should be much better than if the Map was copied for each event.
+ */
+public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyStringMap {
+    private static final long serialVersionUID = 8218007901108944053L;
+
+    /**
+     * 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<Map<String, String>> localMap;
+
+    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<Map<String, String>> createThreadLocalMap(final boolean isMapEnabled) {
+        if (inheritableMap) {
+            return new InheritableThreadLocal<Map<String, String>>() {
+                @Override
+                protected Map<String, String> childValue(final Map<String, String> parentValue) {
+                    return parentValue != null && isMapEnabled //
+                            ? Collections.unmodifiableMap(new HashMap<>(parentValue)) //
+                            : null;
+                }
+            };
+        }
+        // if not inheritable, return plain ThreadLocal with null as initial value
+        return new ThreadLocal<>();
+    }
+
+    static void init() {
+        inheritableMap = PropertiesUtil.getProperties().getBooleanProperty(INHERITABLE_MAP);
+    }
+
+    public DefaultThreadContextMap() {
+        this(true);
+    }
+
+    public DefaultThreadContextMap(final boolean useMap) {
+        this.useMap = useMap;
+        this.localMap = createThreadLocalMap(useMap);
+    }
+
+    @Override
+    public void put(final String key, final String value) {
+        if (!useMap) {
+            return;
+        }
+        Map<String, String> map = localMap.get();
+        map = map == null ? new HashMap<>(1) : new HashMap<>(map);
+        map.put(key, value);
+        localMap.set(Collections.unmodifiableMap(map));
+    }
+
+    public void putAll(final Map<String, String> m) {
+        if (!useMap) {
+            return;
+        }
+        Map<String, String> map = localMap.get();
+        map = map == null ? new HashMap<>(m.size()) : new HashMap<>(map);
+        for (final Map.Entry<String, String> e : m.entrySet()) {
+            map.put(e.getKey(), e.getValue());
+        }
+        localMap.set(Collections.unmodifiableMap(map));
+    }
+
+    @Override
+    public String get(final String key) {
+        final Map<String, String> map = localMap.get();
+        return map == null ? null : map.get(key);
+    }
+
+    @Override
+    public void remove(final String key) {
+        final Map<String, String> map = localMap.get();
+        if (map != null) {
+            final Map<String, String> copy = new HashMap<>(map);
+            copy.remove(key);
+            localMap.set(Collections.unmodifiableMap(copy));
+        }
+    }
+
+    public void removeAll(final Iterable<String> keys) {
+        final Map<String, String> map = localMap.get();
+        if (map != null) {
+            final Map<String, String> copy = new HashMap<>(map);
+            for (final String key : keys) {
+                copy.remove(key);
+            }
+            localMap.set(Collections.unmodifiableMap(copy));
+        }
+    }
+
+    @Override
+    public void clear() {
+        localMap.remove();
+    }
+
+    @Override
+    public Map<String, String> toMap() {
+        return getCopy();
+    }
+
+    @Override
+    public boolean containsKey(final String key) {
+        final Map<String, String> map = localMap.get();
+        return map != null && map.containsKey(key);
+    }
+
+    @Override
+    public <V> void forEach(final BiConsumer<String, ? super V> action) {
+        final Map<String, String> map = localMap.get();
+        if (map == null) {
+            return;
+        }
+        for (final Map.Entry<String, String> entry : map.entrySet()) {
+            // BiConsumer should be able to handle values of any type V. In our case the values are of type String.
+            @SuppressWarnings("unchecked")
+            final V value = (V) entry.getValue();
+            action.accept(entry.getKey(), value);
+        }
+    }
+
+    @Override
+    public <V, S> void forEach(final TriConsumer<String, ? super V, S> action, final S state) {
+        final Map<String, String> map = localMap.get();
+        if (map == null) {
+            return;
+        }
+        for (final Map.Entry<String, String> entry : map.entrySet()) {
+            // TriConsumer should be able to handle values of any type V. In our case the values are of type String.
+            @SuppressWarnings("unchecked")
+            final V value = (V) entry.getValue();
+            action.accept(entry.getKey(), value, state);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <V> V getValue(final String key) {
+        final Map<String, String> map = localMap.get();
+        return (V) (map == null ? null : map.get(key));
+    }
+
+    @Override
+    public Map<String, String> getCopy() {
+        final Map<String, String> map = localMap.get();
+        return map == null ? new HashMap<>() : new HashMap<>(map);
+    }
+
+    @Override
+    public Map<String, String> getImmutableMapOrNull() {
+        return localMap.get();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        final Map<String, String> map = localMap.get();
+        return map == null || map.isEmpty();
+    }
+
+    @Override
+    public int size() {
+        final Map<String, String> map = localMap.get();
+        return map == null ? 0 : map.size();
+    }
+
+    @Override
+    public String toString() {
+        final Map<String, String> map = localMap.get();
+        return map == null ? "{}" : map.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        final Map<String, String> map = this.localMap.get();
+        result = prime * result + ((map == null) ? 0 : map.hashCode());
+        result = prime * result + Boolean.valueOf(this.useMap).hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (obj instanceof DefaultThreadContextMap) {
+            final DefaultThreadContextMap other = (DefaultThreadContextMap) obj;
+            if (this.useMap != other.useMap) {
+                return false;
+            }
+        }
+        if (!(obj instanceof ThreadContextMap)) {
+            return false;
+        }
+        final ThreadContextMap other = (ThreadContextMap) obj;
+        final Map<String, String> map = this.localMap.get();
+        final Map<String, String> otherMap = other.getImmutableMapOrNull();
+        return Objects.equals(map, otherMap);
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
index 5fe721ee39..d9d823d353 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
@@ -16,10 +16,6 @@
  */
 package org.apache.logging.log4j.spi;
 
-import static org.apache.logging.log4j.spi.LoggingSystem.THREAD_CONTEXT_DEFAULT_INITIAL_CAPACITY;
-import static org.apache.logging.log4j.spi.LoggingSystemProperty.THREAD_CONTEXT_INITIAL_CAPACITY;
-import static org.apache.logging.log4j.spi.LoggingSystemProperty.THREAD_CONTEXT_MAP_INHERITABLE;
-
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -38,16 +34,32 @@ import org.apache.logging.log4j.util.StringMap;
  * </p>
  * @since 2.7
  */
-class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ThreadContextMap {
+class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap {
+
+    /**
+     * 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";
+
+    /**
+     * The default initial capacity.
+     */
+    protected static final int DEFAULT_INITIAL_CAPACITY = 16;
+
+    /**
+     * System property name that can be used to control the data structure's initial capacity.
+     */
+    protected static final String PROPERTY_NAME_INITIAL_CAPACITY = "log4j2.ThreadContext.initial.capacity";
 
     protected final ThreadLocal<StringMap> localMap;
-    protected final int initialCapacity;
+    private final int initialCapacity;
 
     public GarbageFreeSortedArrayThreadContextMap() {
         this(
-                PropertiesUtil.getProperties().getBooleanProperty(THREAD_CONTEXT_MAP_INHERITABLE),
+                PropertiesUtil.getProperties().getBooleanProperty(INHERITABLE_MAP),
                 PropertiesUtil.getProperties()
-                        .getIntegerProperty(THREAD_CONTEXT_INITIAL_CAPACITY, THREAD_CONTEXT_DEFAULT_INITIAL_CAPACITY));
+                        .getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY));
     }
 
     GarbageFreeSortedArrayThreadContextMap(final boolean inheritableMap, final int initialCapacity) {
@@ -59,7 +71,7 @@ class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
     // (This method is package protected for JUnit tests.)
     private ThreadLocal<StringMap> createThreadLocalMap(final boolean inheritableMap) {
         if (inheritableMap) {
-            return new InheritableThreadLocal<>() {
+            return new InheritableThreadLocal<StringMap>() {
                 @Override
                 protected StringMap childValue(final StringMap parentValue) {
                     return parentValue != null ? createStringMap(parentValue) : null;
@@ -78,18 +90,6 @@ class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
      * @return an implementation of the {@code StringMap} used to back this thread context map
      */
     protected StringMap createStringMap() {
-        return createStringMap(initialCapacity);
-    }
-
-    /**
-     * Returns an implementation of the {@code StringMap} used to back this thread context map.
-     * <p>
-     * Subclasses may override.
-     * </p>
-     * @return an implementation of the {@code StringMap} used to back this thread context map
-     * @param initialCapacity initial capacity of the StringMap
-     */
-    protected StringMap createStringMap(final int initialCapacity) {
         return new SortedArrayStringMap(initialCapacity);
     }
 
@@ -149,7 +149,7 @@ class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
 
     @Override
     public String get(final String key) {
-        return getValue(key);
+        return (String) getValue(key);
     }
 
     @Override
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ObjectThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LocationAwareLogger.java
similarity index 64%
copy from log4j-api/src/main/java/org/apache/logging/log4j/spi/ObjectThreadContextMap.java
copy to log4j-api/src/main/java/org/apache/logging/log4j/spi/LocationAwareLogger.java
index beda028b4c..dd6950eed1 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ObjectThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LocationAwareLogger.java
@@ -16,13 +16,19 @@
  */
 package org.apache.logging.log4j.spi;
 
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.message.Message;
+
 /**
- * Legacy interface for backward compatibility with extensions to ThreadContextMap.
- * These methods have since been moved to default methods on ThreadContextMap.
- *
- * @see ThreadContextMap
- * @since 2.8
- * @deprecated use {@link ThreadContextMap} directly
+ * Logger that accepts the location of the caller.
  */
-@Deprecated(forRemoval = true, since = "3.0")
-public interface ObjectThreadContextMap extends CleanableThreadContextMap {}
+public interface LocationAwareLogger {
+    void logMessage(
+            final Level level,
+            final Marker marker,
+            final String fqcn,
+            final StackTraceElement location,
+            final Message message,
+            final Throwable throwable);
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystem.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystem.java
index 02ad1242e7..e543936a60 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystem.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystem.java
@@ -70,7 +70,7 @@ public class LoggingSystem {
     private static final String PROVIDER_RESOURCE = "META-INF/log4j-provider.properties";
 
     private static final String API_VERSION = "Log4jAPIVersion";
-    private static final String[] COMPATIBLE_API_VERSIONS = {"3.0.0"};
+    private static final String[] COMPATIBLE_API_VERSIONS = {"2.6.0", "3.0.0"};
 
     public static final int THREAD_CONTEXT_DEFAULT_INITIAL_CAPACITY = 16;
 
@@ -202,7 +202,7 @@ public class LoggingSystem {
     }
 
     private static List<Provider> loadLegacyProviders() {
-        return LoaderUtil.findUrlResources(PROVIDER_RESOURCE).stream()
+        return LoaderUtil.findUrlResources(PROVIDER_RESOURCE, false).stream()
                 .map(urlResource -> loadLegacyProvider(urlResource.getUrl(), urlResource.getClassLoader()))
                 .filter(Objects::nonNull)
                 .collect(Collectors.toList());
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/MessageFactory2Adapter.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/MessageFactory2Adapter.java
new file mode 100644
index 0000000000..1e0f4162c0
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/MessageFactory2Adapter.java
@@ -0,0 +1,157 @@
+/*
+ * 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.Objects;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.message.MessageFactory2;
+import org.apache.logging.log4j.message.SimpleMessage;
+
+/**
+ * Adapts a legacy MessageFactory to the new MessageFactory2 interface.
+ *
+ * @since 2.6
+ */
+public class MessageFactory2Adapter implements MessageFactory2 {
+    private final MessageFactory wrapped;
+
+    public MessageFactory2Adapter(final MessageFactory wrapped) {
+        this.wrapped = Objects.requireNonNull(wrapped);
+    }
+
+    public MessageFactory getOriginal() {
+        return wrapped;
+    }
+
+    @Override
+    public Message newMessage(final CharSequence charSequence) {
+        return new SimpleMessage(charSequence);
+    }
+
+    @Override
+    public Message newMessage(final String message, final Object p0) {
+        return wrapped.newMessage(message, p0);
+    }
+
+    @Override
+    public Message newMessage(final String message, final Object p0, final Object p1) {
+        return wrapped.newMessage(message, p0, p1);
+    }
+
+    @Override
+    public Message newMessage(final String message, final Object p0, final Object p1, final Object p2) {
+        return wrapped.newMessage(message, p0, p1, p2);
+    }
+
+    @Override
+    public Message newMessage(
+            final String message, final Object p0, final Object p1, final Object p2, final Object p3) {
+        return wrapped.newMessage(message, p0, p1, p2, p3);
+    }
+
+    @Override
+    public Message newMessage(
+            final String message, final Object p0, final Object p1, final Object p2, final Object p3, final Object p4) {
+        return wrapped.newMessage(message, p0, p1, p2, p3, p4);
+    }
+
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5) {
+        return wrapped.newMessage(message, p0, p1, p2, p3, p4, p5);
+    }
+
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6) {
+        return wrapped.newMessage(message, p0, p1, p2, p3, p4, p5, p6);
+    }
+
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7) {
+        return wrapped.newMessage(message, p0, p1, p2, p3, p4, p5, p6, p7);
+    }
+
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7,
+            final Object p8) {
+        return wrapped.newMessage(message, p0, p1, p2, p3, p4, p5, p6, p7, p8);
+    }
+
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7,
+            final Object p8,
+            final Object p9) {
+        return wrapped.newMessage(message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9);
+    }
+
+    @Override
+    public Message newMessage(final Object message) {
+        return wrapped.newMessage(message);
+    }
+
+    @Override
+    public Message newMessage(final String message) {
+        return wrapped.newMessage(message);
+    }
+
+    @Override
+    public Message newMessage(final String message, final Object... params) {
+        return wrapped.newMessage(message, params);
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
index 3ff4090eae..155baad10a 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
@@ -20,9 +20,8 @@ import java.util.HashMap;
 import java.util.Map;
 
 /**
- * {@code ThreadContextMap} implementation used when either of system properties
- * {@link LoggingSystemProperty#THREAD_CONTEXT_MAP_ENABLED} or
- * {@link LoggingSystemProperty#THREAD_CONTEXT_ENABLE} is {@code false}. This implementation does nothing.
+ * {@code ThreadContextMap} implementation used when either of system properties {@code disableThreadContextMap} or .
+ * {@code disableThreadContext} is {@code true}. This implementation does nothing.
  *
  * @since 2.7
  */
@@ -60,21 +59,4 @@ public class NoOpThreadContextMap implements ThreadContextMap {
 
     @Override
     public void remove(final String key) {}
-
-    @Override
-    public void putAll(final Map<String, String> map) {}
-
-    @Override
-    public void removeAll(final Iterable<String> keys) {}
-
-    @Override
-    public <V> V getValue(final String key) {
-        return null;
-    }
-
-    @Override
-    public <V> void putValue(final String key, final V value) {}
-
-    @Override
-    public <V> void putAllValues(final Map<String, V> values) {}
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ObjectThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ObjectThreadContextMap.java
index beda028b4c..5e312453fb 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ObjectThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ObjectThreadContextMap.java
@@ -16,13 +16,38 @@
  */
 package org.apache.logging.log4j.spi;
 
+import java.util.Map;
+
 /**
- * Legacy interface for backward compatibility with extensions to ThreadContextMap.
- * These methods have since been moved to default methods on ThreadContextMap.
+ * Extension service provider interface to allow putting Object values in the
+ * {@link org.apache.logging.log4j.ThreadContext}.
  *
  * @see ThreadContextMap
  * @since 2.8
- * @deprecated use {@link ThreadContextMap} directly
  */
-@Deprecated(forRemoval = true, since = "3.0")
-public interface ObjectThreadContextMap extends CleanableThreadContextMap {}
+public interface ObjectThreadContextMap extends CleanableThreadContextMap {
+
+    /**
+     * Returns the Object value for the specified key, or {@code null} if the specified key does not exist in this
+     * collection.
+     *
+     * @param key the key whose value to return
+     * @return the value for the specified key or {@code null}
+     */
+    <V> V getValue(String key);
+
+    /**
+     * Puts the specified key-value pair into the collection.
+     *
+     * @param key the key to add or remove. Keys may be {@code null}.
+     * @param value the value to add. Values may be {@code null}.
+     */
+    <V> void putValue(String key, V value);
+
+    /**
+     * Puts all given key-value pairs into the collection.
+     *
+     * @param values the map of key-value pairs to add
+     */
+    <V> void putAllValues(Map<String, V> values);
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ReadOnlyThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ReadOnlyThreadContextMap.java
index aafbacfeb8..de9ae028aa 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ReadOnlyThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ReadOnlyThreadContextMap.java
@@ -25,9 +25,8 @@ import org.apache.logging.log4j.util.StringMap;
  * <p>
  * {@code ThreadContextMap} implementations that also implement this interface can be accessed
  * by applications via the {@link ThreadContext#getThreadContextMap()} method.
- * <p>
- *     <strong>Warning:</strong> part of the API, not the SPI, since it is used by {@link ThreadContext#getThreadContextMap()}.
  * </p>
+ *
  * @see ThreadContext#getThreadContextMap()
  * @since 2.8
  */
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java
index e958827d6d..f0af90ddbb 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java
@@ -18,9 +18,6 @@ package org.apache.logging.log4j.spi;
 
 import java.util.Map;
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.util.Cast;
-import org.apache.logging.log4j.util.SortedArrayStringMap;
-import org.apache.logging.log4j.util.StringMap;
 
 /**
  * Service provider interface to implement custom MDC behavior for {@link org.apache.logging.log4j.ThreadContext}.
@@ -28,9 +25,6 @@ import org.apache.logging.log4j.util.StringMap;
  * Since 2.8, {@code ThreadContextMap} implementations that implement the {@link ReadOnlyThreadContextMap} interface
  * are accessible to applications via the {@link ThreadContext#getThreadContextMap()} method.
  * </p>
- * <p>
- * Since 3.0.0, {@code ThreadContextMap} combines all its extension interfaces into default methods on this interface.
- * </p>
  */
 public interface ThreadContextMap {
 
@@ -91,85 +85,4 @@ public interface ThreadContextMap {
      * @param key The key to remove.
      */
     void remove(final String key);
-
-    /**
-     * Puts all given context map entries into the current thread's
-     * context map.
-     *
-     * <p>If the current thread does not have a context map it is
-     * created as a side effect.</p>
-     * @param map The map.
-     * @since 3.0.0
-     */
-    default void putAll(Map<String, String> map) {
-        map.forEach(this::put);
-    }
-
-    /**
-     * Returns the context data for reading. Note that regardless of whether the returned context data has been
-     * {@linkplain StringMap#freeze() frozen} (made read-only) or not, callers should not attempt to modify
-     * the returned data structure.
-     *
-     * @return the {@code StringMap}
-     * @since 3.0.0
-     */
-    default StringMap getReadOnlyContextData() {
-        if (this instanceof ReadOnlyThreadContextMap) {
-            return ((ReadOnlyThreadContextMap) this).getReadOnlyContextData();
-        }
-        final Map<String, String> copy = getCopy();
-        final StringMap map = new SortedArrayStringMap(copy.size());
-        copy.forEach(map::putValue);
-        map.freeze();
-        return map;
-    }
-
-    /**
-     * Removes all given context map keys from the current thread's context map.
-     *
-     * <p>If the current thread does not have a context map it is
-     * created as a side effect.</p>
-     *
-     * @param keys The keys.
-     * @since 3.0.0
-     */
-    default void removeAll(Iterable<String> keys) {
-        keys.forEach(this::remove);
-    }
-
-    /**
-     * Returns the Object value for the specified key, or {@code null} if the specified key does not exist in this
-     * collection.
-     *
-     * @param key the key whose value to return
-     * @param <V> The type of the returned value.
-     * @return the value for the specified key or {@code null}
-     * @since 3.0.0
-     */
-    default <V> V getValue(String key) {
-        return Cast.cast(get(key));
-    }
-
-    /**
-     * Puts the specified key-value pair into the collection.
-     *
-     * @param key the key to add or remove. Keys may be {@code null}.
-     * @param <V> The type of the stored and returned value.
-     * @param value the value to add. Values may be {@code null}.
-     * @since 3.0.0
-     */
-    default <V> void putValue(String key, V value) {
-        put(key, value != null ? value.toString() : null);
-    }
-
-    /**
-     * Puts all given key-value pairs into the collection.
-     *
-     * @param values the map of key-value pairs to add
-     * @param <V> The type of the value being added.
-     * @since 3.0.0
-     */
-    default <V> void putAllValues(Map<String, V> values) {
-        values.forEach(this::putValue);
-    }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap2.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap2.java
index 35841feb4a..ccd7f86b67 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap2.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap2.java
@@ -16,13 +16,37 @@
  */
 package org.apache.logging.log4j.spi;
 
+import java.util.Map;
+import org.apache.logging.log4j.util.StringMap;
+
 /**
- * Legacy interface for backward compatibility with extensions to ThreadContextMap.
- * These methods have since been moved to default methods on ThreadContextMap.
+ * Extension service provider interface to implement additional custom MDC behavior for
+ * {@link org.apache.logging.log4j.ThreadContext}.
+ *
+ * Consider implementing {@link CleanableThreadContextMap} instead.
  *
  * @see ThreadContextMap
  * @since 2.7
- * @deprecated use {@link ThreadContextMap} directly
  */
-@Deprecated(forRemoval = true, since = "3.0")
-public interface ThreadContextMap2 extends ThreadContextMap {}
+public interface ThreadContextMap2 extends ThreadContextMap {
+
+    /**
+     * Puts all given context map entries into the current thread's
+     * context map.
+     *
+     * <p>If the current thread does not have a context map it is
+     * created as a side effect.</p>
+     * @param map The map.
+     * @since 2.7
+     */
+    void putAll(final Map<String, String> map);
+
+    /**
+     * Returns the context data for reading. Note that regardless of whether the returned context data has been
+     * {@linkplain StringMap#freeze() frozen} (made read-only) or not, callers should not attempt to modify
+     * the returned data structure.
+     *
+     * @return the {@code StringMap}
+     */
+    StringMap getReadOnlyContextData();
+}
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
new file mode 100644
index 0000000000..970e757c70
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMapFactory.java
@@ -0,0 +1,146 @@
+/*
+ * 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 org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Constants;
+import org.apache.logging.log4j.util.LoaderUtil;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.ProviderUtil;
+
+/**
+ * Creates the ThreadContextMap instance used by the ThreadContext.
+ * <p>
+ * If {@link Constants#ENABLE_THREADLOCALS Log4j can use ThreadLocals}, a garbage-free StringMap-based context map can
+ * be installed by setting system property {@code log4j2.garbagefree.threadContextMap} to {@code true}.
+ * </p><p>
+ * Furthermore, any custom {@code ThreadContextMap} can be installed by setting system property
+ * {@code log4j2.threadContextMap} to the fully qualified class name of the class implementing the
+ * {@code ThreadContextMap} interface. (Also implement the {@code ReadOnlyThreadContextMap} interface if your custom
+ * {@code ThreadContextMap} implementation should be accessible to applications via the
+ * {@link ThreadContext#getThreadContextMap()} method.)
+ * </p><p>
+ * Instead of system properties, the above can also be specified in a properties file named
+ * {@code log4j2.component.properties} in the classpath.
+ * </p>
+ *
+ * @see ThreadContextMap
+ * @see ReadOnlyThreadContextMap
+ * @see org.apache.logging.log4j.ThreadContext
+ * @since 2.7
+ */
+public final class ThreadContextMapFactory {
+    private static final Logger LOGGER = StatusLogger.getLogger();
+    private static final String THREAD_CONTEXT_KEY = "log4j2.threadContextMap";
+    private static final String GC_FREE_THREAD_CONTEXT_KEY = "log4j2.garbagefree.threadContextMap";
+
+    private static boolean GcFreeThreadContextKey;
+    private static String ThreadContextMapName;
+    private static final String GARBAGE_FREE_CONTEXT_MAP =
+            "org.apache.logging.log4j.spi" + ".GarbageFreeSortedArrayThreadContextMap";
+    private static final String COPY_ON_WRITE_CONTEXT_MAP =
+            "org.apache.logging.log4j.spi" + ".CopyOnWriteSortedArrayThreadContextMap";
+
+    static {
+        initPrivate();
+    }
+
+    /**
+     * Initializes static variables based on system properties. Normally called when this class is initialized by the VM
+     * and when Log4j is reconfigured.
+     */
+    public static void init() {
+        DefaultThreadContextMap.init();
+        initPrivate();
+    }
+
+    /**
+     * Initializes static variables based on system properties. Normally called when this class is initialized by the VM
+     * and when Log4j is reconfigured.
+     */
+    private static void initPrivate() {
+        final PropertiesUtil properties = PropertiesUtil.getProperties();
+        ThreadContextMapName = properties.getStringProperty(THREAD_CONTEXT_KEY);
+        GcFreeThreadContextKey = properties.getBooleanProperty(GC_FREE_THREAD_CONTEXT_KEY);
+    }
+
+    private ThreadContextMapFactory() {}
+
+    public static ThreadContextMap createThreadContextMap() {
+        final ClassLoader cl = ProviderUtil.findClassLoader();
+        ThreadContextMap result = null;
+        if (ThreadContextMapName != null) {
+            /*
+             * Two implementation are package-private classes, so we instantiate them directly.
+             * Other implementation must be publicly accessible (through `LoaderUtil`).
+             */
+            switch (ThreadContextMapName) {
+                case GARBAGE_FREE_CONTEXT_MAP:
+                    result = new GarbageFreeSortedArrayThreadContextMap();
+                    break;
+                case COPY_ON_WRITE_CONTEXT_MAP:
+                    result = new CopyOnWriteSortedArrayThreadContextMap();
+                    break;
+                default:
+                    try {
+                        result = LoaderUtil.newCheckedInstanceOf(ThreadContextMapName, ThreadContextMap.class);
+                    } catch (final ClassNotFoundException cnfe) {
+                        LOGGER.error("Unable to locate configured ThreadContextMap {}", ThreadContextMapName);
+                    } catch (final Exception ex) {
+                        LOGGER.error("Unable to create configured ThreadContextMap {}", ThreadContextMapName, ex);
+                    }
+            }
+        }
+        if (result == null && ProviderUtil.hasProviders() && LogManager.getFactory() != null) { // LOG4J2-1658
+            final String factoryClassName = LogManager.getFactory().getClass().getName();
+            for (final Provider provider : ProviderUtil.getProviders()) {
+                if (factoryClassName.equals(provider.getClassName())) {
+                    final Class<? extends ThreadContextMap> clazz = provider.loadThreadContextMap();
+                    if (clazz != null) {
+                        try {
+                            result = LoaderUtil.newInstanceOf(clazz);
+                            break;
+                        } catch (final Exception e) {
+                            LOGGER.error(
+                                    "Unable to locate or load configured ThreadContextMap {}",
+                                    provider.getThreadContextMap(),
+                                    e);
+                            result = createDefaultThreadContextMap();
+                        }
+                    }
+                }
+            }
+        }
+        if (result == null) {
+            result = createDefaultThreadContextMap();
+        }
+        return result;
+    }
+
+    private static ThreadContextMap createDefaultThreadContextMap() {
+        if (Constants.ENABLE_THREADLOCALS) {
+            if (GcFreeThreadContextKey) {
+                return new GarbageFreeSortedArrayThreadContextMap();
+            }
+            return new CopyOnWriteSortedArrayThreadContextMap();
+        }
+        return new DefaultThreadContextMap(true);
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Constants.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/Constants.java
index 78fd2eccc1..a354c48b37 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Constants.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Constants.java
@@ -23,7 +23,6 @@ import org.apache.logging.log4j.spi.LoggingSystemProperty;
  *
  * @since 2.6.2
  */
-@InternalApi
 public final class Constants {
 
     private static final LazyBoolean isWebApp = new LazyBoolean(() -> PropertiesUtil.getProperties()
@@ -37,6 +36,11 @@ public final class Constants {
      * "log4j2.is.webapp", or (if this system property is not set) whether the  {@code javax.servlet.Servlet} class
      * is present in the classpath.
      */
+    public static final boolean IS_WEB_APP = PropertiesUtil.getProperties()
+            .getBooleanProperty(
+                    "log4j2.is.webapp",
+                    isClassAvailable("javax.servlet.Servlet") || isClassAvailable("jakarta.servlet.Servlet"));
+
     public static boolean isWebApp() {
         return isWebApp.getAsBoolean();
     }
@@ -49,6 +53,19 @@ public final class Constants {
         isWebApp.reset();
     }
 
+    /**
+     * Kill switch for object pooling in ThreadLocals that enables much of the LOG4J2-1270 no-GC behaviour.
+     * <p>
+     * {@code True} for non-{@link #IS_WEB_APP web apps}, disable by setting system property
+     * "log4j2.enable.threadlocals" to "false".
+     * </p>
+     */
+    public static final boolean ENABLE_THREADLOCALS =
+            !IS_WEB_APP && PropertiesUtil.getProperties().getBooleanProperty("log4j2.enable.threadlocals", true);
+
+    /**
+     * Java major version.
+     */
     public static final int JAVA_MAJOR_VERSION = getMajorVersion();
 
     /**
@@ -56,17 +73,41 @@ public final class Constants {
      * After a large message has been delivered to the appenders, the StringBuilder is trimmed to this size.
      * <p>
      * The default value is 518, which allows the StringBuilder to resize three times from its initial size.
-     * Users can override with system property {@link LoggingSystemProperty#GC_REUSABLE_MESSAGE_MAX_SIZE}.
+     * Users can override with system property "log4j.maxReusableMsgSize".
      * </p>
      * @since 2.9
      */
-    public static final int MAX_REUSABLE_MESSAGE_SIZE =
-            size(LoggingSystemProperty.GC_REUSABLE_MESSAGE_MAX_SIZE, (128 * 2 + 2) * 2 + 2);
+    public static final int MAX_REUSABLE_MESSAGE_SIZE = size("log4j.maxReusableMsgSize", (128 * 2 + 2) * 2 + 2);
+
+    /**
+     * Name of the system property that will turn on TRACE level internal log4j2 status logging.
+     * <p>
+     * If system property {@value} is either defined empty or its value equals to {@code true} (ignoring case), all internal log4j2 logging will be
+     * printed to the console. The presence of this system property overrides any value set in the configuration's
+     * {@code <Configuration status="<level>" ...>} status attribute, as well as any value set for
+     * system property {@code org.apache.logging.log4j.simplelog.StatusLogger.level}.
+     * </p>
+     */
+    public static final String LOG4J2_DEBUG = "log4j2.debug";
 
-    private static int size(final PropertyKey property, final int defaultValue) {
+    private static int size(final String property, final int defaultValue) {
         return PropertiesUtil.getProperties().getIntegerProperty(property, defaultValue);
     }
 
+    /**
+     * Determines if a named Class can be loaded or not.
+     *
+     * @param className The class name.
+     * @return {@code true} if the class could be found or {@code false} otherwise.
+     */
+    private static boolean isClassAvailable(final String className) {
+        try {
+            return LoaderUtil.loadClass(className) != null;
+        } catch (final Throwable e) {
+            return false;
+        }
+    }
+
     /**
      * The empty array.
      */
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/LoaderUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/LoaderUtil.java
index fd834b17a4..027bcee7a0 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/LoaderUtil.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/LoaderUtil.java
@@ -20,12 +20,13 @@ import java.io.IOException;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.net.URL;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.util.Collection;
 import java.util.Enumeration;
 import java.util.LinkedHashSet;
 import java.util.Objects;
 import java.util.function.Supplier;
-import org.apache.logging.log4j.spi.LoggingSystemProperty;
 
 /**
  * <em>Consider this class private.</em> Utility class for ClassLoaders.
@@ -40,17 +41,43 @@ public final class LoaderUtil {
 
     private static final ClassLoader[] EMPTY_CLASS_LOADER_ARRAY = {};
 
+    /**
+     * System property to set to ignore the thread context ClassLoader.
+     *
+     * @since 2.1
+     */
+    public static final String IGNORE_TCCL_PROPERTY = "log4j.ignoreTCL";
+
     // this variable must be lazily loaded; otherwise, we get a nice circular class loading problem where LoaderUtil
     // wants to use PropertiesUtil, but then PropertiesUtil wants to use LoaderUtil.
     private static Boolean ignoreTCCL;
     static Boolean forceTcclOnly;
 
-    static {
-        if (System.getSecurityManager() != null) {
-            LowLevelLogUtil.log(
-                    "A custom SecurityManager was detected; Log4j no longer supports security permissions.");
+    static final RuntimePermission GET_CLASS_LOADER = new RuntimePermission("getClassLoader");
+    static final LazyBoolean GET_CLASS_LOADER_DISABLED = new LazyBoolean(() -> {
+        if (System.getSecurityManager() == null) {
+            return false;
         }
-    }
+        try {
+            AccessController.checkPermission(GET_CLASS_LOADER);
+            // seems like we'll be ok
+            return false;
+        } catch (final SecurityException ignored) {
+            try {
+                // let's see if we can obtain that permission
+                AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
+                    AccessController.checkPermission(GET_CLASS_LOADER);
+                    return null;
+                });
+                return false;
+            } catch (final SecurityException ignore) {
+                // no chance
+                return true;
+            }
+        }
+    });
+
+    private static final PrivilegedAction<ClassLoader> TCCL_GETTER = new ThreadContextClassLoaderGetter();
 
     private LoaderUtil() {}
 
@@ -65,13 +92,18 @@ public final class LoaderUtil {
 
     // TODO: this method could use some explanation
     public static ClassLoader getClassLoader(final Class<?> class1, final Class<?> class2) {
-        final ClassLoader loader1 = class1 == null ? null : class1.getClassLoader();
-        final ClassLoader loader2 = class2 == null ? null : class2.getClassLoader();
-        final ClassLoader referenceLoader = Thread.currentThread().getContextClassLoader();
-        if (isChild(referenceLoader, loader1)) {
-            return isChild(referenceLoader, loader2) ? referenceLoader : loader2;
-        }
-        return isChild(loader1, loader2) ? loader1 : loader2;
+        PrivilegedAction<ClassLoader> action = () -> {
+            final ClassLoader loader1 = class1 == null ? null : class1.getClassLoader();
+            final ClassLoader loader2 = class2 == null ? null : class2.getClassLoader();
+            final ClassLoader referenceLoader = GET_CLASS_LOADER_DISABLED.getAsBoolean()
+                    ? getThisClassLoader()
+                    : Thread.currentThread().getContextClassLoader();
+            if (isChild(referenceLoader, loader1)) {
+                return isChild(referenceLoader, loader2) ? referenceLoader : loader2;
+            }
+            return isChild(loader1, loader2) ? loader1 : loader2;
+        };
+        return runPrivileged(action);
     }
 
     /**
@@ -95,22 +127,45 @@ public final class LoaderUtil {
     }
 
     /**
-     * Looks up the ClassLoader for this current thread falling back to either the
+     * Looks up the ClassLoader for this current thread. If this class does not have the runtime permission
+     * {@code getClassLoader}, then the only ClassLoader this attempts to look up is the loader behind this
+     * class. When a SecurityManager is installed, this attempts to make a privileged call to get the current
+     * {@linkplain Thread#getContextClassLoader() thread context ClassLoader}, falling back to either the
      * ClassLoader of this class or the {@linkplain ClassLoader#getSystemClassLoader() system ClassLoader}.
+     * When no SecurityManager is present, the same lookups are performed without use of {@link AccessController}.
      * If none of these strategies can obtain a ClassLoader, then this returns {@code null}.
      *
      * @return the current thread's ClassLoader, a fallback loader, or null if no fallback can be determined
      */
     public static ClassLoader getThreadContextClassLoader() {
-        final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
-        if (contextClassLoader != null) {
-            return contextClassLoader;
+        try {
+            return GET_CLASS_LOADER_DISABLED.getAsBoolean() ? getThisClassLoader() : runPrivileged(TCCL_GETTER);
+        } catch (final SecurityException ignored) {
+            return null;
         }
-        final ClassLoader thisClassLoader = getThisClassLoader();
-        if (thisClassLoader != null) {
-            return thisClassLoader;
+    }
+
+    private static ClassLoader getThisClassLoader() {
+        return LoaderUtil.class.getClassLoader();
+    }
+
+    private static <T> T runPrivileged(final PrivilegedAction<T> action) {
+        return System.getSecurityManager() != null ? AccessController.doPrivileged(action) : action.run();
+    }
+
+    private static class ThreadContextClassLoaderGetter implements PrivilegedAction<ClassLoader> {
+        @Override
+        public ClassLoader run() {
+            final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+            if (contextClassLoader != null) {
+                return contextClassLoader;
+            }
+            final ClassLoader thisClassLoader = getThisClassLoader();
+            if (thisClassLoader != null || GET_CLASS_LOADER_DISABLED.getAsBoolean()) {
+                return thisClassLoader;
+            }
+            return ClassLoader.getSystemClassLoader();
         }
-        return ClassLoader.getSystemClassLoader();
     }
 
     public static ClassLoader[] getClassLoaders() {
@@ -140,10 +195,6 @@ public final class LoaderUtil {
         return classLoaders.toArray(EMPTY_CLASS_LOADER_ARRAY);
     }
 
-    private static ClassLoader getThisClassLoader() {
-        return LoaderUtil.class.getClassLoader();
-    }
-
     private static void accumulateLayerClassLoaders(
             final ModuleLayer layer, final Collection<ClassLoader> classLoaders) {
         for (final Module module : layer.modules()) {
@@ -157,8 +208,8 @@ public final class LoaderUtil {
     }
 
     /**
-     * Adds the provided loader to the loaders collection, and traverses up the tree until either a null
-     * value or a classloader which has already been added is encountered.
+     * Adds the provided loader to the loaders collection, and traverses up the tree until either a null value or a
+     * classloader which has already been added is encountered.
      */
     private static void accumulateClassLoaders(final ClassLoader loader, final Collection<ClassLoader> loaders) {
         // Some implementations may use null to represent the bootstrap class loader.
@@ -180,16 +231,15 @@ public final class LoaderUtil {
             return true;
         } catch (final ClassNotFoundException | LinkageError e) {
             return false;
-        } catch (final Throwable e) {
-            LowLevelLogUtil.logException("Unknown error checking for existence of class: " + className, e);
+        } catch (final Throwable error) {
+            // LOGGER.error("Unknown error while checking existence of class `{}`", className, error);
             return false;
         }
     }
 
     /**
-     * Loads and initializes a class given its fully qualified class name. This method respects the
-     * {@link LoggingSystemProperty#LOADER_IGNORE_THREAD_CONTEXT_LOADER} Log4j property. If this property is specified
-     * and set to anything besides {@code false}, then this class's ClassLoader will be used.
+     * Loads a class by name. This method respects the {@link #IGNORE_TCCL_PROPERTY} Log4j property. If this property is
+     * specified and set to anything besides {@code false}, then the default ClassLoader will be used.
      *
      * @param className fully qualified class name to load
      * @return the loaded class
@@ -307,6 +357,58 @@ public final class LoaderUtil {
         return newInstanceOf(clazz);
     }
 
+    /**
+     * Loads and instantiates a class given by a property name.
+     *
+     * @param propertyName The property name to look up a class name for.
+     * @param clazz        The class to cast it to.
+     * @param <T>          The type to cast it to.
+     * @return new instance of the class given in the property or {@code null} if the property was unset.
+     * @throws ClassNotFoundException      if the class isn't available to the usual ClassLoaders
+     * @throws ExceptionInInitializerError if an exception was thrown while initializing the class
+     * @throws LinkageError                if the linkage of the class fails for any other reason
+     * @throws ClassCastException          if the class is not compatible with the generic type parameter provided
+     * @throws NoSuchMethodException       if no zero-arg constructor exists
+     * @throws SecurityException           if this class is not allowed to access declared members of the provided class
+     * @throws IllegalAccessException      if the class can't be instantiated through a public constructor
+     * @throws InstantiationException      if the provided class is abstract or an interface
+     * @throws InvocationTargetException   if an exception is thrown by the constructor
+     * @since 2.5
+     */
+    public static <T> T newCheckedInstanceOfProperty(final String propertyName, final Class<T> clazz)
+            throws ReflectiveOperationException {
+        return newCheckedInstanceOfProperty(propertyName, clazz, () -> null);
+    }
+
+    /**
+     * Loads and instantiates a class given by a property name.
+     *
+     * @param propertyName    The property name to look up a class name for.
+     * @param clazz           The class to cast it to.
+     * @param defaultSupplier Supplier of a default value if the property is not present.
+     * @param <T>             The type to cast it to.
+     * @return new instance of the class given in the property or {@code null} if the property was unset.
+     * @throws ClassNotFoundException      if the class isn't available to the usual ClassLoaders
+     * @throws ExceptionInInitializerError if an exception was thrown while initializing the class
+     * @throws LinkageError                if the linkage of the class fails for any other reason
+     * @throws ClassCastException          if the class is not compatible with the generic type parameter provided
+     * @throws NoSuchMethodException       if no zero-arg constructor exists
+     * @throws SecurityException           if this class is not allowed to access declared members of the provided class
+     * @throws IllegalAccessException      if the class can't be instantiated through a public constructor
+     * @throws InstantiationException      if the provided class is abstract or an interface
+     * @throws InvocationTargetException   if an exception is thrown by the constructor
+     * @since 2.22
+     */
+    public static <T> T newCheckedInstanceOfProperty(
+            final String propertyName, final Class<T> clazz, final Supplier<T> defaultSupplier)
+            throws ReflectiveOperationException {
+        final String className = PropertiesUtil.getProperties().getStringProperty(propertyName);
+        if (className == null) {
+            return defaultSupplier.get();
+        }
+        return newCheckedInstanceOf(className, clazz);
+    }
+
     /**
      * Loads and instantiates a class given by a property name.
      *
@@ -410,8 +512,7 @@ public final class LoaderUtil {
     private static boolean isIgnoreTccl() {
         // we need to lazily initialize this, but concurrent access is not an issue
         if (ignoreTCCL == null) {
-            final String ignoreTccl = PropertiesUtil.getProperties()
-                    .getStringProperty(LoggingSystemProperty.LOADER_IGNORE_THREAD_CONTEXT_LOADER, null);
+            final String ignoreTccl = PropertiesUtil.getProperties().getStringProperty(IGNORE_TCCL_PROPERTY, null);
             ignoreTCCL = ignoreTccl != null && !"false".equalsIgnoreCase(ignoreTccl.trim());
         }
         return ignoreTCCL;
@@ -420,7 +521,7 @@ public final class LoaderUtil {
     private static boolean isForceTccl() {
         if (forceTcclOnly == null) {
             // PropertiesUtil.getProperties() uses that code path so don't use that!
-            forceTcclOnly = Boolean.getBoolean(LoggingSystemProperty.LOADER_FORCE_THREAD_CONTEXT_LOADER.getSystemKey());
+            forceTcclOnly = Boolean.getBoolean("log4j2.Loader.forceTCLOnly");
         }
         return forceTcclOnly;
     }
@@ -433,7 +534,11 @@ public final class LoaderUtil {
      * @since 2.1
      */
     public static Collection<URL> findResources(final String resource) {
-        final Collection<UrlResource> urlResources = findUrlResources(resource);
+        return findResources(resource, true);
+    }
+
+    static Collection<URL> findResources(final String resource, final boolean useTccl) {
+        final Collection<UrlResource> urlResources = findUrlResources(resource, useTccl);
         final Collection<URL> resources = new LinkedHashSet<>(urlResources.size());
         for (final UrlResource urlResource : urlResources) {
             resources.add(urlResource.getUrl());
@@ -441,25 +546,24 @@ public final class LoaderUtil {
         return resources;
     }
 
-    /**
-     * This method will only find resources that follow the JPMS rules for encapsulation. Resources
-     * on the class path should be found as normal along with resources with no package name in all
-     * modules. Resources within packages in modules must declare those resources open to org.apache.logging.log4j.
-     *
-     * @param resource The resource to locate.
-     * @return The located resources.
-     */
-    public static Collection<UrlResource> findUrlResources(final String resource) {
+    public static Collection<UrlResource> findUrlResources(final String resource, final boolean useTccl) {
+        // @formatter:off
+        final ClassLoader[] candidates = {
+            useTccl ? getThreadContextClassLoader() : null,
+            LoaderUtil.class.getClassLoader(),
+            GET_CLASS_LOADER_DISABLED.getAsBoolean() ? null : ClassLoader.getSystemClassLoader()
+        };
+        // @formatter:on
         final Collection<UrlResource> resources = new LinkedHashSet<>();
-        for (final ClassLoader cl : getClassLoaders()) {
+        for (final ClassLoader cl : candidates) {
             if (cl != null) {
                 try {
                     final Enumeration<URL> resourceEnum = cl.getResources(resource);
                     while (resourceEnum.hasMoreElements()) {
                         resources.add(new UrlResource(cl, resourceEnum.nextElement()));
                     }
-                } catch (final IOException e) {
-                    LowLevelLogUtil.logException(e);
+                } catch (final IOException error) {
+                    // LOGGER.error("failed to collect resources of name `{}`", resource, error);
                 }
             }
         }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/MessageSupplier.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/MessageSupplier.java
index 2904668e15..8b13571350 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/MessageSupplier.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/MessageSupplier.java
@@ -16,7 +16,6 @@
  */
 package org.apache.logging.log4j.util;
 
-import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.message.Message;
 
 /**
@@ -27,10 +26,19 @@ import org.apache.logging.log4j.message.Message;
  *
  * <p>Implementors are free to cache values or return a new or distinct value each time the supplier is invoked.
  *
- * <p>
- *     <strong>Warning:</strong> part of the public API, since it is used by {@link org.apache.logging.log4j.Logger#debug(Marker, MessageSupplier)}.
+ * <p><strong>DEPRECATED:</strong> this class should not normally be used outside a Java 8+ lambda syntax. Instead,
+ * {@link Supplier Supplier<Message>} should be used as an anonymous class. Both this and {@link Supplier} will be
+ * removed in 3.0.
  * </p>
+ *
  * @since 2.4
  */
-@Deprecated(forRemoval = true, since = "3.0")
-public interface MessageSupplier extends java.util.function.Supplier<Message> {}
+public interface MessageSupplier {
+
+    /**
+     * Gets a Message.
+     *
+     * @return a Message
+     */
+    Message get();
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java
index dcb1a4fbad..f2239b0108 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/PropertiesUtil.java
@@ -51,7 +51,7 @@ import org.apache.logging.log4j.internal.CopyOnWriteNavigableSet;
  * <p>
  * Provides utility methods for managing {@link Properties} instances as well as access to the global configuration
  * system. Properties by default are loaded from the system properties, system environment, and a classpath resource
- * file named {@value #LOG4J_SYSTEM_PROPERTIES_FILE_NAME}. Additional properties can be loaded by implementing a custom
+ * file named {@value #LOG4J_PROPERTIES_FILE_NAME}. Additional properties can be loaded by implementing a custom
  * {@link PropertySource} service and specifying it via a {@link ServiceLoader} file called
  * {@code META-INF/services/org.apache.logging.log4j.util.PropertySource} with a list of fully qualified class names
  * implementing that interface.
@@ -63,6 +63,7 @@ import org.apache.logging.log4j.internal.CopyOnWriteNavigableSet;
 @ServiceConsumer(value = PropertySource.class, cardinality = Cardinality.MULTIPLE)
 public class PropertiesUtil implements PropertyEnvironment {
 
+    private static final String LOG4J_PROPERTIES_FILE_NAME = "log4j2.component.properties";
     private static final String LOG4J_NAMESPACE = "component";
     private static final String LOG4J_CONTEXT_PREFIX = "log4j2.context.";
     private static final String DELIMITER = ".";
@@ -146,6 +147,31 @@ public class PropertiesUtil implements PropertyEnvironment {
         this.environment = new Environment(contextName, sources);
     }
 
+    /**
+     * Loads and closes the given property input stream. If an error occurs, log to the status logger.
+     *
+     * @param in     a property input stream.
+     * @param source a source object describing the source, like a resource string or a URL.
+     * @return a new Properties object
+     */
+    static Properties loadClose(final InputStream in, final Object source) {
+        final Properties props = new Properties();
+        if (null != in) {
+            try {
+                props.load(in);
+            } catch (final IOException error) {
+                // LOGGER.error("Unable to read source `{}`", source, error);
+            } finally {
+                try {
+                    in.close();
+                } catch (final IOException error) {
+                    // LOGGER.error("Unable to close source `{}`", source, error);
+                }
+            }
+        }
+        return props;
+    }
+
     public static boolean hasThreadProperties() {
         return environments.get() != null;
     }
@@ -731,6 +757,21 @@ public class PropertiesUtil implements PropertyEnvironment {
             if (literal.containsKey(key)) {
                 return literal.get(key);
             }
+            // This part is temporarily copied from 2.x to prepare
+            // the removal of Log4j API 3.x
+            final List<CharSequence> tokens = PropertySource.Util.tokenize(key);
+            final boolean hasTokens = !tokens.isEmpty();
+            for (final PropertySource source : sources) {
+                if (hasTokens) {
+                    final String normalKey = Objects.toString(source.getNormalForm(tokens), null);
+                    if (normalKey != null && source.containsProperty(normalKey)) {
+                        return source.getProperty(normalKey);
+                    }
+                }
+                if (source.containsProperty(key)) {
+                    return source.getProperty(key);
+                }
+            }
             String result = null;
             final String contextKey = getContextKey(key);
             if (contextName != null && !contextName.equals(PropertySource.SYSTEM_CONTEXT)) {
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/ProviderActivator.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/ProviderActivator.java
new file mode 100644
index 0000000000..bbb6966485
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/ProviderActivator.java
@@ -0,0 +1,53 @@
+/*
+ * 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.util;
+
+import java.util.Hashtable;
+import org.apache.logging.log4j.spi.Provider;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * Utility class to register Log4j2 providers in an OSGI environment.
+ */
+public abstract class ProviderActivator implements BundleActivator {
+
+    public static final String API_VERSION = "APIVersion";
+
+    private final Provider provider;
+    private ServiceRegistration<Provider> providerRegistration = null;
+
+    protected ProviderActivator(final Provider provider) {
+        this.provider = provider;
+    }
+
+    @Override
+    @SuppressWarnings("JdkObsolete")
+    public void start(final BundleContext context) throws Exception {
+        final Hashtable<String, String> props = new Hashtable<>();
+        props.put(API_VERSION, provider.getVersions());
+        providerRegistration = context.registerService(Provider.class, provider, props);
+    }
+
+    @Override
+    public void stop(final BundleContext context) throws Exception {
+        if (providerRegistration != null) {
+            providerRegistration.unregister();
+        }
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/ProviderUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/ProviderUtil.java
new file mode 100644
index 0000000000..cde96d6356
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/ProviderUtil.java
@@ -0,0 +1,178 @@
+/*
+ * 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.util;
+
+import aQute.bnd.annotation.Cardinality;
+import aQute.bnd.annotation.Resolution;
+import aQute.bnd.annotation.baseline.BaselineIgnore;
+import aQute.bnd.annotation.spi.ServiceConsumer;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.ServiceLoader;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.spi.Provider;
+import org.apache.logging.log4j.status.StatusLogger;
+
+/**
+ * <em>Consider this class private.</em> Utility class for Log4j {@link Provider}s. When integrating with an application
+ * container framework, any Log4j Providers not accessible through standard classpath scanning should
+ * {@link #loadProvider(java.net.URL, ClassLoader)} a classpath accordingly.
+ */
+@InternalApi
+@BaselineIgnore("2.22.0")
+@ServiceConsumer(value = Provider.class, resolution = Resolution.OPTIONAL, cardinality = Cardinality.MULTIPLE)
+public final class ProviderUtil {
+
+    /**
+     * Resource name for a Log4j 2 provider properties file.
+     */
+    static final String PROVIDER_RESOURCE = "META-INF/log4j-provider.properties";
+
+    /**
+     * Loaded providers.
+     */
+    static final Collection<Provider> PROVIDERS = new HashSet<>();
+
+    /**
+     * Guards the ProviderUtil singleton instance from lazy initialization. This is primarily used for OSGi support.
+     *
+     * @since 2.1
+     */
+    static final Lock STARTUP_LOCK = new ReentrantLock();
+
+    private static final String API_VERSION = "Log4jAPIVersion";
+    private static final String[] COMPATIBLE_API_VERSIONS = {"2.6.0"};
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    // STARTUP_LOCK guards INSTANCE for lazy initialization; this allows the OSGi Activator to pause the startup and
+    // wait for a Provider to be installed. See LOG4J2-373
+    private static volatile ProviderUtil instance;
+
+    private ProviderUtil() {
+        ServiceLoaderUtil.safeStream(ServiceLoader.load(Provider.class, ProviderUtil.class.getClassLoader()))
+                .filter(provider -> validVersion(provider.getVersions()))
+                .forEach(PROVIDERS::add);
+
+        for (final LoaderUtil.UrlResource resource : LoaderUtil.findUrlResources(PROVIDER_RESOURCE, false)) {
+            loadProvider(resource.getUrl(), resource.getClassLoader());
+        }
+    }
+
+    static void addProvider(final Provider provider) {
+        PROVIDERS.add(provider);
+        LOGGER.debug("Loaded Provider {}", provider);
+    }
+
+    /**
+     * Loads an individual Provider implementation. This method is really only useful for the OSGi bundle activator and
+     * this class itself.
+     *
+     * @param url the URL to the provider properties file
+     * @param cl the ClassLoader to load the provider classes with
+     */
+    @SuppressFBWarnings(
+            value = "URLCONNECTION_SSRF_FD",
+            justification = "Uses a fixed URL that ends in 'META-INF/log4j-provider.properties'.")
+    static void loadProvider(final URL url, final ClassLoader cl) {
+        try {
+            final Properties props = PropertiesUtil.loadClose(url.openStream(), url);
+            if (validVersion(props.getProperty(API_VERSION))) {
+                final Provider provider = new Provider(props, url, cl);
+                PROVIDERS.add(provider);
+                LOGGER.debug("Loaded Provider {}", provider);
+            }
+        } catch (final IOException e) {
+            LOGGER.error("Unable to open {}", url, e);
+        }
+    }
+
+    /**
+     *
+     * @param classLoader null can be used to mark the bootstrap class loader.
+     */
+    static void loadProviders(final ClassLoader classLoader) {
+        ServiceLoaderUtil.safeStream(ServiceLoader.load(Provider.class, classLoader))
+                .filter(provider -> validVersion(provider.getVersions()))
+                .forEach(PROVIDERS::add);
+    }
+
+    /**
+     * @deprecated Use {@link #loadProvider(java.net.URL, ClassLoader)} instead. Will be removed in 3.0.
+     */
+    @Deprecated
+    static void loadProviders(final Enumeration<URL> urls, final ClassLoader cl) {
+        if (urls != null) {
+            while (urls.hasMoreElements()) {
+                loadProvider(urls.nextElement(), cl);
+            }
+        }
+    }
+
+    public static Iterable<Provider> getProviders() {
+        lazyInit();
+        return PROVIDERS;
+    }
+
+    public static boolean hasProviders() {
+        lazyInit();
+        return !PROVIDERS.isEmpty();
+    }
+
+    /**
+     * Lazily initializes the ProviderUtil singleton.
+     *
+     * @since 2.1
+     */
+    static void lazyInit() {
+        // noinspection DoubleCheckedLocking
+        if (instance == null) {
+            try {
+                STARTUP_LOCK.lockInterruptibly();
+                try {
+                    if (instance == null) {
+                        instance = new ProviderUtil();
+                    }
+                } finally {
+                    STARTUP_LOCK.unlock();
+                }
+            } catch (final InterruptedException e) {
+                LOGGER.fatal("Interrupted before Log4j Providers could be loaded.", e);
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    public static ClassLoader findClassLoader() {
+        return LoaderUtil.getThreadContextClassLoader();
+    }
+
+    private static boolean validVersion(final String version) {
+        for (final String v : COMPATIBLE_API_VERSIONS) {
+            if (version.startsWith(v)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/Supplier.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/Supplier.java
index c207f0076e..aaa4d9b89a 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Supplier.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Supplier.java
@@ -25,11 +25,20 @@ package org.apache.logging.log4j.util;
  *
  * <p>Implementors are free to cache values or return a new or distinct value each time the supplier is invoked.
  *
- * <p><strong>DEPRECATED:</strong> this class is deprecated and will be replaced with the Java 8 interface.</p>
+ * <p><strong>DEPRECATED:</strong> this class will be removed in 3.0 to be replaced with the Java 8 interface.</p>
  *
  * @param <T> the type of values returned by this supplier
  *
  * @since 2.4
  */
-@Deprecated(forRemoval = true, since = "2.22")
-public interface Supplier<T> extends java.util.function.Supplier<T> {}
+@InternalApi
+@Deprecated
+public interface Supplier<T> {
+
+    /**
+     * Gets a value.
+     *
+     * @return a value
+     */
+    T get();
+}
diff --git a/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/AsyncThreadContextTest.java b/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/AsyncThreadContextTest.java
index c8d57007bc..3c55fda33e 100644
--- a/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/AsyncThreadContextTest.java
+++ b/log4j-async-logger/src/test/java/org/apache/logging/log4j/async/logger/AsyncThreadContextTest.java
@@ -32,12 +32,10 @@ import org.apache.logging.log4j.core.Logger;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.ThreadContextTestAccess;
 import org.apache.logging.log4j.core.impl.Log4jContextFactory;
-import org.apache.logging.log4j.core.impl.Log4jPropertyKey;
 import org.apache.logging.log4j.core.selector.ClassLoaderContextSelector;
 import org.apache.logging.log4j.core.selector.ContextSelector;
 import org.apache.logging.log4j.core.test.CoreLoggerContexts;
 import org.apache.logging.log4j.plugins.di.DI;
-import org.apache.logging.log4j.spi.LoggingSystemProperty;
 import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
 import org.apache.logging.log4j.test.TestProperties;
 import org.apache.logging.log4j.test.junit.InitializesThreadContext;
@@ -50,12 +48,9 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
 
 @Tag("sleepy")
-@SetTestProperty(
-        key = Log4jPropertyKey.Constant.ASYNC_LOGGER_RING_BUFFER_SIZE,
-        value = "128") // minimum ringbuffer size
-@SetTestProperty(
-        key = Log4jPropertyKey.Constant.ASYNC_CONFIG_RING_BUFFER_SIZE,
-        value = "128") // minimum ringbuffer size
+@SetTestProperty(key = "log4j2.enable.threadlocals", value = "false")
+@SetTestProperty(key = "AsyncLogger.ringBufferSize", value = "128") // minimum ringbuffer size
+@SetTestProperty(key = "AsyncLoggerConfig.ringBufferSize", value = "128") // minimum ringbuffer size
 @InitializesThreadContext
 public class AsyncThreadContextTest {
 
@@ -92,10 +87,13 @@ public class AsyncThreadContextTest {
         COPY_ON_WRITE;
 
         void init() {
-            System.clearProperty(LoggingSystemProperty.Constant.THREAD_CONTEXT_MAP_CLASS);
             final String PACKAGE = "org.apache.logging.log4j.spi.";
-            System.setProperty(
-                    LoggingSystemProperty.Constant.THREAD_CONTEXT_MAP_CLASS, PACKAGE + implClassSimpleName());
+            final String implClass = implClassSimpleName();
+            if (implClass != null) {
+                System.setProperty("log4j2.threadContextMap", PACKAGE + implClass);
+            } else {
+                System.clearProperty("log4j2.threadContextMap");
+            }
             PropertiesUtil.getProperties().reload();
             ThreadContextTestAccess.init();
         }
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
index 5bb6691169..39258f5003 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.logging.log4j.core.impl;
 
+import static java.util.Arrays.asList;
 import static java.util.concurrent.Executors.newSingleThreadExecutor;
 import static org.apache.logging.log4j.ThreadContext.getThreadContextMap;
 import static org.apache.logging.log4j.core.impl.ContextDataInjectorFactory.createInjector;
@@ -27,23 +28,19 @@ import static org.hamcrest.Matchers.hasEntry;
 import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import java.lang.reflect.Constructor;
+import java.util.Collection;
 import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.ContextDataInjector;
-import org.apache.logging.log4j.spi.LoggingSystem;
 import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
-import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.apache.logging.log4j.test.ThreadContextUtilityClass;
+import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.SortedArrayStringMap;
 import org.apache.logging.log4j.util.StringMap;
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
-import org.junit.platform.commons.function.Try;
-import org.junit.platform.commons.support.ReflectionSupport;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
@@ -51,36 +48,38 @@ import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
 public class ThreadContextDataInjectorTest {
-
-    public static final String THREAD_LOCAL_MAP_CLASS_NAME =
-            "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap";
-    public static final String GARBAGE_FREE_MAP_CLASS_NAME =
-            "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap";
-
-    @Parameters(name = "{1}")
-    public static Object[][] threadContextMapConstructorsAndReadOnlyNames() {
-        return new Object[][] {
+    @Parameters(name = "{0}")
+    public static Collection<String[]> threadContextMapClassNames() {
+        return asList(new String[][] {
             {
-                (Function<Boolean, ThreadContextMap>) ThreadContextDataInjectorTest::createThreadLocalMap,
-                THREAD_LOCAL_MAP_CLASS_NAME
+                "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap",
+                "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap"
             },
             {
-                (Function<Boolean, ThreadContextMap>) ThreadContextDataInjectorTest::createGarbageFreeMap,
-                GARBAGE_FREE_MAP_CLASS_NAME
-            }
-        };
+                "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap",
+                "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap"
+            },
+            {"org.apache.logging.log4j.spi.DefaultThreadContextMap", null}
+        });
     }
 
-    @Parameter(0)
-    public Function<Boolean, ThreadContextMap> threadContextMapConstructor;
+    @Parameter
+    public String threadContextMapClassName;
 
-    @Parameter(1)
-    public String readOnlyThreadContextMapClassName;
+    @Parameter(value = 1)
+    public String readOnlythreadContextMapClassName;
+
+    @Before
+    public void before() {
+        System.setProperty("log4j2.threadContextMap", threadContextMapClassName);
+    }
 
     @After
     public void after() {
         ThreadContext.remove("foo");
         ThreadContext.remove("baz");
+        System.clearProperty("log4j2.threadContextMap");
+        System.clearProperty("log4j2.isThreadContextMapInheritable");
     }
 
     private void testContextDataInjector() {
@@ -90,7 +89,7 @@ public class ThreadContextDataInjectorTest {
                 (readOnlythreadContextMap == null)
                         ? null
                         : readOnlythreadContextMap.getClass().getName(),
-                is(equalTo(readOnlyThreadContextMapClassName)));
+                is(equalTo(readOnlythreadContextMapClassName)));
 
         final ContextDataInjector contextDataInjector = createInjector();
         final StringMap stringMap = contextDataInjector.injectContextData(null, new SortedArrayStringMap());
@@ -122,9 +121,9 @@ public class ThreadContextDataInjectorTest {
     }
 
     private void prepareThreadContext(final boolean isThreadContextMapInheritable) {
-        LoggingSystem.getInstance()
-                .setThreadContextMapFactory(() -> threadContextMapConstructor.apply(isThreadContextMapInheritable));
-        ThreadContext.init();
+        System.setProperty("log4j2.isThreadContextMapInheritable", Boolean.toString(isThreadContextMapInheritable));
+        PropertiesUtil.getProperties().reload();
+        ThreadContextUtilityClass.reset();
         ThreadContext.remove("baz");
         ThreadContext.put("foo", "bar");
     }
@@ -140,31 +139,8 @@ public class ThreadContextDataInjectorTest {
         prepareThreadContext(true);
         try {
             newSingleThreadExecutor().submit(this::testContextDataInjector).get();
-        } catch (ExecutionException ee) {
+        } catch (final ExecutionException ee) {
             throw ee.getCause();
         }
     }
-
-    private static ThreadContextMap createThreadLocalMap(final boolean inheritable) {
-        final Try<Class<?>> loadedClass = ReflectionSupport.tryToLoadClass(THREAD_LOCAL_MAP_CLASS_NAME);
-        final Class<? extends ThreadContextMap> mapClass =
-                assertDoesNotThrow(loadedClass::get).asSubclass(ThreadContextMap.class);
-        return newInstanceWithCapacityArg(mapClass, inheritable);
-    }
-
-    private static ThreadContextMap createGarbageFreeMap(final boolean inheritable) {
-        final Try<Class<?>> loadedClass = ReflectionSupport.tryToLoadClass(GARBAGE_FREE_MAP_CLASS_NAME);
-        final Class<? extends ThreadContextMap> mapClass =
-                assertDoesNotThrow(loadedClass::get).asSubclass(ThreadContextMap.class);
-        return newInstanceWithCapacityArg(mapClass, inheritable);
-    }
-
-    private static ThreadContextMap newInstanceWithCapacityArg(
-            final Class<? extends ThreadContextMap> mapClass, final boolean inheritable) {
-        final Constructor<? extends ThreadContextMap> constructor =
-                assertDoesNotThrow(() -> mapClass.getDeclaredConstructor(Boolean.TYPE, Integer.TYPE));
-        assertTrue(constructor.trySetAccessible(), () -> "Unable to access constructor for " + mapClass);
-        return assertDoesNotThrow(
-                () -> constructor.newInstance(inheritable, LoggingSystem.THREAD_CONTEXT_DEFAULT_INITIAL_CAPACITY));
-    }
 }
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteOpenHashMapThreadContextMap.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteOpenHashMapThreadContextMap.java
index 4b5827be74..0787a8325a 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteOpenHashMapThreadContextMap.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteOpenHashMapThreadContextMap.java
@@ -17,6 +17,7 @@
 package org.apache.logging.log4j.spi;
 
 import org.apache.logging.log4j.perf.nogc.OpenHashStringMap;
+import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.StringMap;
 
@@ -32,6 +33,9 @@ public class CopyOnWriteOpenHashMapThreadContextMap extends CopyOnWriteSortedArr
     /** Constant used in benchmark code */
     public static final Class<? extends ThreadContextMap> SUPER = CopyOnWriteSortedArrayThreadContextMap.class;
 
+    private static final int initialCapacity =
+            PropertiesUtil.getProperties().getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY);
+
     @Override
     protected StringMap createStringMap() {
         return new OpenHashStringMap<>(initialCapacity);
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/GarbageFreeOpenHashMapThreadContextMap.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/GarbageFreeOpenHashMapThreadContextMap.java
index a40dec0d62..aae7457348 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/GarbageFreeOpenHashMapThreadContextMap.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/spi/GarbageFreeOpenHashMapThreadContextMap.java
@@ -16,9 +16,6 @@
  */
 package org.apache.logging.log4j.spi;
 
-import static org.apache.logging.log4j.spi.LoggingSystem.THREAD_CONTEXT_DEFAULT_INITIAL_CAPACITY;
-import static org.apache.logging.log4j.spi.LoggingSystemProperty.THREAD_CONTEXT_INITIAL_CAPACITY;
-
 import org.apache.logging.log4j.perf.nogc.OpenHashStringMap;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
@@ -39,10 +36,12 @@ public class GarbageFreeOpenHashMapThreadContextMap extends GarbageFreeSortedArr
     /** Constant used in benchmark code */
     public static final Class<? extends ThreadContextMap> SUPER = GarbageFreeSortedArrayThreadContextMap.class;
 
+    private static final int initialCapacity =
+            PropertiesUtil.getProperties().getIntegerProperty(PROPERTY_NAME_INITIAL_CAPACITY, DEFAULT_INITIAL_CAPACITY);
+
     @Override
-    protected StringMap createStringMap(final int initialCapacity) {
-        return new OpenHashStringMap<>(PropertiesUtil.getProperties()
-                .getIntegerProperty(THREAD_CONTEXT_INITIAL_CAPACITY, THREAD_CONTEXT_DEFAULT_INITIAL_CAPACITY));
+    protected StringMap createStringMap() {
+        return new OpenHashStringMap<>(initialCapacity);
     }
 
     @Override
diff --git a/log4j-to-slf4j/pom.xml b/log4j-to-slf4j/pom.xml
index f92ff4d06d..ef7e783c93 100644
--- a/log4j-to-slf4j/pom.xml
+++ b/log4j-to-slf4j/pom.xml
@@ -44,6 +44,11 @@
     </bnd-extra-module-options>
   </properties>
   <dependencies>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.framework</artifactId>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
diff --git a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/Activator.java
similarity index 66%
copy from log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
copy to log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/Activator.java
index c340d30c40..5080869f5d 100644
--- a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
+++ b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/Activator.java
@@ -16,16 +16,16 @@
  */
 package org.apache.logging.slf4j;
 
-import aQute.bnd.annotation.Resolution;
-import aQute.bnd.annotation.spi.ServiceProvider;
-import org.apache.logging.log4j.spi.Provider;
+import org.apache.logging.log4j.util.ProviderActivator;
+import org.osgi.annotation.bundle.Header;
 
-/**
- * Bind the Log4j API to SLF4J.
- */
-@ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL)
-public class SLF4JProvider extends Provider {
-    public SLF4JProvider() {
-        super(15, "3.0.0", SLF4JLoggerContextFactory.class, MDCContextMap.class);
+@Header(name = org.osgi.framework.Constants.BUNDLE_ACTIVATOR, value = "${@class}")
+@Header(
+        name = org.osgi.framework.Constants.BUNDLE_ACTIVATIONPOLICY,
+        value = org.osgi.framework.Constants.ACTIVATION_LAZY)
+public class Activator extends ProviderActivator {
+
+    public Activator() {
+        super(new SLF4JProvider());
     }
 }
diff --git a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/MDCContextMap.java b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/MDCContextMap.java
index f2e3463e7e..4f2ef190bb 100644
--- a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/MDCContextMap.java
+++ b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/MDCContextMap.java
@@ -19,7 +19,7 @@ package org.apache.logging.slf4j;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
-import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.apache.logging.log4j.spi.CleanableThreadContextMap;
 import org.apache.logging.log4j.util.SortedArrayStringMap;
 import org.apache.logging.log4j.util.StringMap;
 import org.slf4j.MDC;
@@ -27,7 +27,7 @@ import org.slf4j.MDC;
 /**
  * Bind the ThreadContextMap to the SLF4J MDC.
  */
-public class MDCContextMap implements ThreadContextMap {
+public class MDCContextMap implements CleanableThreadContextMap {
 
     private static final StringMap EMPTY_CONTEXT_DATA = new SortedArrayStringMap(1);
 
diff --git a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
index 919cf2dee5..26e94c67b3 100644
--- a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
+++ b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
@@ -23,14 +23,14 @@ import org.apache.logging.log4j.message.LoggerNameAwareMessage;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.spi.AbstractLogger;
-import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.util.Constants;
 import org.slf4j.LoggerFactory;
 import org.slf4j.MarkerFactory;
 import org.slf4j.spi.LocationAwareLogger;
 
 public class SLF4JLogger extends AbstractLogger {
 
+    private static final long serialVersionUID = 1L;
     /**
      * Logback supports turbo filters, that can override the logger's level.
      * Therefore we can never return a no-op builder.
@@ -38,11 +38,9 @@ public class SLF4JLogger extends AbstractLogger {
     private static final boolean LAZY_LEVEL_CHECK = "ch.qos.logback.classic.LoggerContext"
             .equals(LoggerFactory.getILoggerFactory().getClass().getName());
 
-    private static final Recycler<SLF4JLogBuilder> LOG_BUILDER_RECYCLER =
-            LoggingSystem.getRecyclerFactory().create(SLF4JLogBuilder::new);
+    private static final ThreadLocal<SLF4JLogBuilder> logBuilder = ThreadLocal.withInitial(SLF4JLogBuilder::new);
 
     private final org.slf4j.Logger logger;
-
     private final LocationAwareLogger locationAwareLogger;
 
     public SLF4JLogger(final String name, final MessageFactory messageFactory, final org.slf4j.Logger logger) {
@@ -366,8 +364,10 @@ public class SLF4JLogger extends AbstractLogger {
 
     @Override
     protected LogBuilder getLogBuilder(final Level level) {
-        final SLF4JLogBuilder builder = LOG_BUILDER_RECYCLER.acquire();
-        return builder.reset(this, level);
+        final SLF4JLogBuilder builder = logBuilder.get();
+        return Constants.ENABLE_THREADLOCALS && !builder.isInUse()
+                ? builder.reset(this, level)
+                : new SLF4JLogBuilder(this, level);
     }
 
     @Override
diff --git a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
index c340d30c40..b8514c4f63 100644
--- a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
+++ b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JProvider.java
@@ -26,6 +26,6 @@ import org.apache.logging.log4j.spi.Provider;
 @ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL)
 public class SLF4JProvider extends Provider {
     public SLF4JProvider() {
-        super(15, "3.0.0", SLF4JLoggerContextFactory.class, MDCContextMap.class);
+        super(15, "2.6.0", SLF4JLoggerContextFactory.class, MDCContextMap.class);
     }
 }
diff --git a/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/LoggerTest.java b/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/LoggerTest.java
index ad14d17aed..5f89d5a526 100644
--- a/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/LoggerTest.java
+++ b/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/LoggerTest.java
@@ -25,7 +25,6 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -41,9 +40,10 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.message.MessageFactory;
-import org.apache.logging.log4j.message.ReusableMessageFactory;
+import org.apache.logging.log4j.message.ParameterizedMessageFactory;
 import org.apache.logging.log4j.message.StringFormatterMessageFactory;
-import org.apache.logging.log4j.spi.LoggingSystem;
+import org.apache.logging.log4j.spi.AbstractLogger;
+import org.apache.logging.log4j.spi.MessageFactory2Adapter;
 import org.apache.logging.log4j.test.junit.UsingStatusListener;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -53,6 +53,7 @@ import org.slf4j.MDC;
 @LoggerContextSource
 public class LoggerTest {
 
+    private static final Object OBJ = new Object();
     // Log4j objects
     private Logger logger;
     // Logback objects
@@ -79,9 +80,23 @@ public class LoggerTest {
         assertThat(list.strList, hasSize(2));
     }
 
+    @Test
+    public void basicFlowDepreacted() {
+        logger.entry();
+        logger.exit();
+        assertThat(list.strList, hasSize(2));
+    }
+
+    @Test
+    public void simpleFlowDeprecated() {
+        logger.entry(OBJ);
+        logger.exit(0);
+        assertThat(list.strList, hasSize(2));
+    }
+
     @Test
     public void simpleFlow() {
-        logger.traceEntry("foo");
+        logger.entry(OBJ);
         logger.traceExit(0);
         assertThat(list.strList, hasSize(2));
     }
@@ -113,7 +128,7 @@ public class LoggerTest {
         final Logger testLogger = testMessageFactoryMismatch(
                 "getLogger_String_MessageFactoryMismatch",
                 StringFormatterMessageFactory.INSTANCE,
-                new ReusableMessageFactory());
+                ParameterizedMessageFactory.INSTANCE);
         testLogger.debug("%,d", Integer.MAX_VALUE);
         assertThat(list.strList, hasSize(1));
         assertThat(list.strList, hasItem(String.format("%,d", Integer.MAX_VALUE)));
@@ -138,12 +153,17 @@ public class LoggerTest {
         return testLogger;
     }
 
-    private static void checkMessageFactory(final MessageFactory messageFactory, final Logger testLogger) {
-        if (messageFactory == null) {
-            assertSame(LoggingSystem.getMessageFactory(), testLogger.getMessageFactory());
+    private static void checkMessageFactory(final MessageFactory messageFactory1, final Logger testLogger1) {
+        if (messageFactory1 == null) {
+            assertEquals(
+                    AbstractLogger.DEFAULT_MESSAGE_FACTORY_CLASS,
+                    testLogger1.getMessageFactory().getClass());
         } else {
-            final MessageFactory actual = testLogger.getMessageFactory();
-            assertEquals(messageFactory, actual);
+            MessageFactory actual = testLogger1.getMessageFactory();
+            if (actual instanceof MessageFactory2Adapter) {
+                actual = ((MessageFactory2Adapter) actual).getOriginal();
+            }
+            assertEquals(messageFactory1, actual);
         }
     }