You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2023/12/18 10:49:45 UTC

(logging-log4j2) branch api-queue created (now ab1c1fe7f6)

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

vy pushed a change to branch api-queue
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


      at ab1c1fe7f6 Move JCTools dependencies from `log4j-core` to `log4j-jctools`

This branch includes the following new commits:

     new 8954e0770a Remove the need for an SPSC queue
     new 4fd515f783 Move JCTools-based queues in the API to a new module
     new b37dcc14c3 Create `log4j-jctools` module with a JCTools-based recycler
     new ab1c1fe7f6 Move JCTools dependencies from `log4j-core` to `log4j-jctools`

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(logging-log4j2) 02/04: Move JCTools-based queues in the API to a new module

Posted by vy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch api-queue
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 4fd515f7833d4c3e37a728106d34c8e34b5c0fa7
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Tue Dec 12 12:45:09 2023 +0100

    Move JCTools-based queues in the API to a new module
---
 {log4j-api => log4j-api-queue-jctools}/pom.xml     | 41 +++++++------
 .../log4j/api/queue/JCToolsQueueFactory.java       | 37 +++++++++++
 log4j-api/pom.xml                                  | 11 +---
 .../logging/log4j/internal/QueueFactories.java     | 71 +++++++++-------------
 .../logging/log4j/internal/RecyclerFactories.java  |  4 +-
 .../apache/logging/log4j/status/StatusLogger.java  |  2 +-
 pom.xml                                            |  7 +++
 7 files changed, 99 insertions(+), 74 deletions(-)

diff --git a/log4j-api/pom.xml b/log4j-api-queue-jctools/pom.xml
similarity index 64%
copy from log4j-api/pom.xml
copy to log4j-api-queue-jctools/pom.xml
index 2ea9e66998..18dd10d906 100644
--- a/log4j-api/pom.xml
+++ b/log4j-api-queue-jctools/pom.xml
@@ -15,44 +15,45 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
   <modelVersion>4.0.0</modelVersion>
+
   <parent>
     <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j</artifactId>
     <version>${revision}</version>
     <relativePath>../log4j-parent</relativePath>
   </parent>
-  <artifactId>log4j-api</artifactId>
-  <packaging>jar</packaging>
-  <name>Apache Log4j API</name>
-  <description>The Apache Log4j API</description>
+
+  <artifactId>log4j-api-queue-jctools</artifactId>
+  <name>Apache Log4j API queue provider (JCTools)</name>
+  <description>Provides JCTools-based queue implementations for the Apache Log4j API.</description>
+
   <properties>
-    <maven.javadoc.skip>false</maven.javadoc.skip>
+
+    <log4jParentDir>${basedir}/..</log4jParentDir>
 
     <!--
       ~ OSGi and JPMS options
       -->
-    <bnd-module-name>org.apache.logging.log4j</bnd-module-name>
-    <bnd-extra-package-options>
-      <!-- No JSR 305 at runtime -->
-      !javax.annotation.*,
-      <!-- Optional dependencies -->
-      org.jctools.*;resolution:=optional
-    </bnd-extra-package-options>
-    <bnd-extra-module-options>
-      <!-- No JSR 305 at runtime -->
-      jsr305;ignore=true,
-      <!-- Used in StringBuilders through reflection -->
-      java.sql;static=true
-    </bnd-extra-module-options>
+    <bnd-module-name>org.apache.logging.log4j.api.queue.jctools</bnd-module-name>
+    <Fragment-Host>org.apache.logging.log4j.core</Fragment-Host>
 
   </properties>
+
   <dependencies>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-api</artifactId>
+    </dependency>
+
     <dependency>
       <groupId>org.jctools</groupId>
       <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
     </dependency>
+
   </dependencies>
+
 </project>
diff --git a/log4j-api-queue-jctools/src/main/java/org/apache/logging/log4j/api/queue/JCToolsQueueFactory.java b/log4j-api-queue-jctools/src/main/java/org/apache/logging/log4j/api/queue/JCToolsQueueFactory.java
new file mode 100644
index 0000000000..57a39e31f8
--- /dev/null
+++ b/log4j-api-queue-jctools/src/main/java/org/apache/logging/log4j/api/queue/JCToolsQueueFactory.java
@@ -0,0 +1,37 @@
+/*
+ * 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.api.queue;
+
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.Queue;
+import org.apache.logging.log4j.spi.QueueFactory;
+import org.jctools.queues.MpmcArrayQueue;
+
+/**
+ * A multi-producer-multi-consumer, thread-safe {@link QueueFactory} implementation based on <a href="https://jctools.github.io/JCTools/">JCTools</a>.
+ */
+@ServiceProvider(QueueFactory.class)
+public final class JCToolsQueueFactory implements QueueFactory {
+
+    @Override
+    public <E> Queue<E> create(final int capacity) {
+        if (capacity < 1) {
+            throw new IllegalArgumentException("invalid capacity: " + capacity);
+        }
+        return new MpmcArrayQueue<>(capacity);
+    }
+}
diff --git a/log4j-api/pom.xml b/log4j-api/pom.xml
index 2ea9e66998..bb22c40e74 100644
--- a/log4j-api/pom.xml
+++ b/log4j-api/pom.xml
@@ -36,9 +36,7 @@
     <bnd-module-name>org.apache.logging.log4j</bnd-module-name>
     <bnd-extra-package-options>
       <!-- No JSR 305 at runtime -->
-      !javax.annotation.*,
-      <!-- Optional dependencies -->
-      org.jctools.*;resolution:=optional
+      !javax.annotation.*
     </bnd-extra-package-options>
     <bnd-extra-module-options>
       <!-- No JSR 305 at runtime -->
@@ -48,11 +46,4 @@
     </bnd-extra-module-options>
 
   </properties>
-  <dependencies>
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
-  </dependencies>
 </project>
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
index 3d0313057b..b0275c4506 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
@@ -16,67 +16,56 @@
  */
 package org.apache.logging.log4j.internal;
 
+import static org.apache.logging.log4j.util.LowLevelLogUtil.log;
+
+import aQute.bnd.annotation.spi.ServiceConsumer;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
+import java.util.List;
 import java.util.Queue;
+import java.util.ServiceLoader;
 import java.util.concurrent.ArrayBlockingQueue;
-import java.util.function.Supplier;
 import org.apache.logging.log4j.spi.QueueFactory;
 import org.apache.logging.log4j.util.Cast;
 import org.apache.logging.log4j.util.InternalApi;
 import org.apache.logging.log4j.util.LoaderUtil;
-import org.jctools.queues.MpmcArrayQueue;
+import org.apache.logging.log4j.util.ServiceLoaderUtil;
 
 /**
- * Provides {@link QueueFactory} instances for different use cases.
- * <p>
- * Implementations provided by <a href="https://jctools.github.io/JCTools/">JCTools</a> will be preferred, if available at runtime.
- * Otherwise, {@link ArrayBlockingQueue} will be used.
- * </p>
+ * Provides the default {@link QueueFactory} instance.
  *
  * @since 3.0.0
  */
 @InternalApi
-public enum QueueFactories implements QueueFactory {
+@ServiceConsumer(QueueFactory.class)
+public final class QueueFactories {
 
     /**
-     * Provides a bounded queue for multi-producer/multi-consumer usage.
+     * The default {@link QueueFactory} instance.
      */
-    MPMC(() -> MpmcArrayQueue::new);
-
-    private final QueueFactory queueFactory;
-
-    QueueFactories(final Supplier<QueueFactory> queueFactoryProvider) {
-        this.queueFactory = getOrReplaceQueueFactory(queueFactoryProvider);
-    }
-
-    private static QueueFactory getOrReplaceQueueFactory(final Supplier<QueueFactory> queueFactoryProvider) {
-        try {
-            final QueueFactory queueFactory = queueFactoryProvider.get();
-            // Test with a large enough capacity to avoid any `IllegalArgumentExceptions` from trivial queues
-            queueFactory.create(16);
-            return queueFactory;
-        } catch (final LinkageError ignored) {
-            return ArrayBlockingQueueFactory.INSTANCE;
+    public static final QueueFactory INSTANCE = findInstance();
+
+    private static QueueFactory findInstance() {
+        final ServiceLoader<QueueFactory> serviceLoader =
+                ServiceLoader.load(QueueFactory.class, QueueFactory.class.getClassLoader());
+        final List<QueueFactory> factories =
+                ServiceLoaderUtil.safeStream(serviceLoader).toList();
+        if (factories.isEmpty()) {
+            return ArrayBlockingQueue::new;
+        } else {
+            final int factoryCount = factories.size();
+            if (factoryCount > 1) {
+                log("Log4j was expecting a single `QueueFactory` provider, found:");
+                for (int factoryIndex = 0; factoryIndex < factoryCount; factoryIndex++) {
+                    log((factoryIndex + 1) + ". `" + factories.get(factoryIndex) + "`");
+                }
+                log("Log4j will use the first `QueueFactory` provider as the default.");
+            }
+            return factories.get(0);
         }
     }
 
-    @Override
-    public <E> Queue<E> create(final int capacity) {
-        return queueFactory.create(capacity);
-    }
-
-    private static final class ArrayBlockingQueueFactory implements QueueFactory {
-
-        private static final ArrayBlockingQueueFactory INSTANCE = new ArrayBlockingQueueFactory();
-
-        private ArrayBlockingQueueFactory() {}
-
-        @Override
-        public <E> Queue<E> create(final int capacity) {
-            return new ArrayBlockingQueue<>(capacity);
-        }
-    }
+    private QueueFactories() {}
 
     /**
      * Creates a {@link QueueFactory} using the provided supplier.
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java
index aea850f199..c7ec83a87a 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java
@@ -42,7 +42,7 @@ public final class RecyclerFactories {
      */
     public static final RecyclerFactory INSTANCE = isThreadLocalsEnabled()
             ? new ThreadLocalRecyclerFactory(CAPACITY)
-            : new QueueingRecyclerFactory(QueueFactories.MPMC, CAPACITY);
+            : new QueueingRecyclerFactory(QueueFactories.INSTANCE, CAPACITY);
 
     private RecyclerFactories() {}
 
@@ -146,7 +146,7 @@ public final class RecyclerFactories {
 
         // Execute the read spec
         final QueueFactory queueFactory =
-                supplierPath != null ? QueueFactories.ofSupplier(supplierPath) : QueueFactories.MPMC;
+                supplierPath != null ? QueueFactories.ofSupplier(supplierPath) : QueueFactories.INSTANCE;
         return new QueueingRecyclerFactory(queueFactory, capacity);
     }
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
index 7f0247ccef..d5134593ef 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
@@ -97,7 +97,7 @@ public final class StatusLogger extends AbstractLogger {
         this.logger = logger;
         this.configuration = configuration;
         this.listenersLevel = configuration.getDefaultLevel().intLevel();
-        messages = QueueFactories.MPMC.create(configuration.getMaxEntries());
+        messages = QueueFactories.INSTANCE.create(configuration.getMaxEntries());
     }
 
     /**
diff --git a/pom.xml b/pom.xml
index 3dcdfa03ce..8862927529 100644
--- a/pom.xml
+++ b/pom.xml
@@ -234,6 +234,7 @@
          Note that modules here must have a corresponding entry in `dependencyManagement > dependencies` block below! -->
     <module>log4j-1.2-api</module>
     <module>log4j-api</module>
+    <module>log4j-api-queue-jctools</module>
     <module>log4j-api-test</module>
     <module>log4j-appserver</module>
     <module>log4j-core</module>
@@ -356,6 +357,12 @@
         <version>${project.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-api-queue-jctools</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-api-test</artifactId>


(logging-log4j2) 04/04: Move JCTools dependencies from `log4j-core` to `log4j-jctools`

Posted by vy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch api-queue
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit ab1c1fe7f67e14879e6ac571bd7fbb65ee2788fa
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Mon Dec 18 11:45:18 2023 +0100

    Move JCTools dependencies from `log4j-core` to `log4j-jctools`
---
 .../recycler/RecyclerFactoryRegistryTest.java      |  2 +-
 ...sTestUtil.java => RecyclerFactoryTestUtil.java} |  4 +-
 .../ThreadLocalRecyclerFactoryProviderTest.java    |  2 +-
 log4j-core-its/pom.xml                             |  6 --
 log4j-core-test/pom.xml                            |  6 --
 .../log4j/core/appender/AsyncAppenderTest.java     |  8 ---
 .../log4j/core/async/perftest/RunJCTools.java      | 29 ---------
 log4j-core/pom.xml                                 |  7 --
 log4j-gctests/pom.xml                              |  6 --
 log4j-jctools/pom.xml                              |  6 ++
 .../jctools}/JCToolsBlockingQueueFactory.java      |  9 +--
 ...er.java => JCToolsRecyclerFactoryProvider.java} | 16 ++---
 .../jctools/JCToolsBlockingQueueFactoryTest.java   | 76 ++++++++++++++++++++++
 ...ava => JCToolsRecyclerFactoryProviderTest.java} |  4 +-
 .../resources/JCToolsBlockingQueueFactoryTest.xml  |  2 +-
 ...ging.log4j.spi.recycler.RecyclerFactoryProvider |  2 +-
 log4j-layout-template-json-test/pom.xml            |  6 --
 ...rFactoryCustomizedJsonTemplateLayoutLogging.xml | 30 ---------
 log4j-layout-template-json/pom.xml                 |  5 --
 pom.xml                                            | 12 ++--
 src/site/asciidoc/manual/appenders.adoc            |  1 +
 src/site/asciidoc/runtime-dependencies.adoc        |  4 --
 22 files changed, 109 insertions(+), 134 deletions(-)

diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java
index 8793c1244e..f6684f63cc 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java
@@ -16,7 +16,7 @@
  */
 package org.apache.logging.log4j.internal.recycler;
 
-import static org.apache.logging.log4j.internal.recycler.RecyclerFactoriesTestUtil.createForEnvironment;
+import static org.apache.logging.log4j.internal.recycler.RecyclerFactoryTestUtil.createForEnvironment;
 import static org.apache.logging.log4j.spi.recycler.Recycler.DEFAULT_CAPACITY;
 import static org.assertj.core.api.Assertions.assertThat;
 
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoriesTestUtil.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryTestUtil.java
similarity index 95%
rename from log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoriesTestUtil.java
rename to log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryTestUtil.java
index 44422a3319..a6c8f99a2c 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoriesTestUtil.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryTestUtil.java
@@ -23,9 +23,9 @@ import org.apache.logging.log4j.spi.recycler.RecyclerFactoryRegistry;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.PropertyEnvironment;
 
-final class RecyclerFactoriesTestUtil {
+final class RecyclerFactoryTestUtil {
 
-    private RecyclerFactoriesTestUtil() {}
+    private RecyclerFactoryTestUtil() {}
 
     @Nullable
     static RecyclerFactory createForEnvironment(
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java
index 7ccde99823..147a84e52c 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java
@@ -16,7 +16,7 @@
  */
 package org.apache.logging.log4j.internal.recycler;
 
-import static org.apache.logging.log4j.internal.recycler.RecyclerFactoriesTestUtil.createForEnvironment;
+import static org.apache.logging.log4j.internal.recycler.RecyclerFactoryTestUtil.createForEnvironment;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
diff --git a/log4j-core-its/pom.xml b/log4j-core-its/pom.xml
index c1dee9b916..2b38ca60b5 100644
--- a/log4j-core-its/pom.xml
+++ b/log4j-core-its/pom.xml
@@ -72,12 +72,6 @@
       <artifactId>jackson-dataformat-yaml</artifactId>
       <optional>true</optional>
     </dependency>
-    <!-- Alternative implementation of BlockingQueue using JCTools for AsyncAppender -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml
index ec7e071ee0..bd3a339e2a 100644
--- a/log4j-core-test/pom.xml
+++ b/log4j-core-test/pom.xml
@@ -155,12 +155,6 @@
       <artifactId>jansi</artifactId>
       <optional>true</optional>
     </dependency>
-    <!-- Alternative implementation of BlockingQueue using JCTools for AsyncAppender -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/AsyncAppenderTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/AsyncAppenderTest.java
index b1aacc3e41..9631d9a2e0 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/AsyncAppenderTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/AsyncAppenderTest.java
@@ -118,14 +118,6 @@ public class AsyncAppenderTest {
         assertEquals("STDOUT", appender.getErrorRef());
     }
 
-    @Test
-    @Tag("jctools")
-    @LoggerContextSource("BlockingQueueFactory-JCToolsBlockingQueue.xml")
-    public void testJcToolsBlockingQueue(final LoggerContext context) throws InterruptedException {
-        rewriteTest(context);
-        exceptionTest(context);
-    }
-
     @Test
     @LoggerContextSource("BlockingQueueFactory-LinkedTransferQueue.xml")
     public void testLinkedTransferQueue(final LoggerContext context) throws InterruptedException {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/perftest/RunJCTools.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/perftest/RunJCTools.java
deleted file mode 100644
index 58622a938a..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/perftest/RunJCTools.java
+++ /dev/null
@@ -1,29 +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.core.async.perftest;
-
-import java.util.concurrent.BlockingQueue;
-import org.apache.logging.log4j.core.async.JCToolsBlockingQueueFactory;
-import org.apache.logging.log4j.core.async.JCToolsBlockingQueueFactory.WaitStrategy;
-
-public class RunJCTools extends AbstractRunQueue {
-
-    @Override
-    BlockingQueue<String> createQueue(final int capacity) {
-        return JCToolsBlockingQueueFactory.createFactory(WaitStrategy.SPIN).create(capacity);
-    }
-}
diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml
index fcdfffd644..bc33f87910 100644
--- a/log4j-core/pom.xml
+++ b/log4j-core/pom.xml
@@ -40,7 +40,6 @@
       com.lmax.disruptor.*;resolution:=optional,
       org.apache.commons.compress.*;resolution:=optional,
       org.fusesource.jansi;resolution:=optional,
-      org.jctools.*;resolution:=optional,
       <!-- Optional Java modules -->
       <!-- java.management -->
       java.lang.management;resolution:=optional,
@@ -134,12 +133,6 @@
       <artifactId>jansi</artifactId>
       <optional>true</optional>
     </dependency>
-    <!-- Alternative implementation of BlockingQueue using JCTools for AsyncAppender -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
   </dependencies>
   <build>
     <plugins>
diff --git a/log4j-gctests/pom.xml b/log4j-gctests/pom.xml
index a461b5fff7..988808c891 100644
--- a/log4j-gctests/pom.xml
+++ b/log4j-gctests/pom.xml
@@ -99,12 +99,6 @@
       <artifactId>jansi</artifactId>
       <optional>true</optional>
     </dependency>
-    <!-- Alternative implementation of BlockingQueue using JCTools for AsyncAppender -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/log4j-jctools/pom.xml b/log4j-jctools/pom.xml
index a66acc8017..e5e8c01b76 100644
--- a/log4j-jctools/pom.xml
+++ b/log4j-jctools/pom.xml
@@ -53,6 +53,12 @@
       <artifactId>jctools-core</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/JCToolsBlockingQueueFactory.java b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactory.java
similarity index 95%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/async/JCToolsBlockingQueueFactory.java
rename to log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactory.java
index 188d49bb5b..d35801cdd6 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/JCToolsBlockingQueueFactory.java
+++ b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactory.java
@@ -14,12 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.core.async;
+package org.apache.logging.log4j.jctools;
 
 import java.util.Collection;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.LockSupport;
+import org.apache.logging.log4j.core.async.BlockingQueueFactory;
 import org.apache.logging.log4j.plugins.Configurable;
 import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginAttribute;
@@ -27,13 +28,13 @@ import org.apache.logging.log4j.plugins.PluginFactory;
 import org.jctools.queues.MpscArrayQueue;
 
 /**
- * Factory for creating instances of BlockingQueues backed by JCTools {@link MpscArrayQueue}.
+ * A {@link BlockingQueueFactory} based on <a href="https://jctools.github.io/JCTools/">JCTools</a>.
  *
- * @since 2.7
+ * @since 3.0.0
  */
 @Configurable(elementType = BlockingQueueFactory.ELEMENT_TYPE, printObject = true)
 @Plugin("JCToolsBlockingQueue")
-public class JCToolsBlockingQueueFactory implements BlockingQueueFactory {
+public final class JCToolsBlockingQueueFactory implements BlockingQueueFactory {
 
     private final WaitStrategy waitStrategy;
 
diff --git a/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProvider.java b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProvider.java
similarity index 85%
rename from log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProvider.java
rename to log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProvider.java
index 3a8fe11305..321b7e9983 100644
--- a/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProvider.java
+++ b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProvider.java
@@ -32,10 +32,12 @@ import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.jctools.queues.MpmcArrayQueue;
 
 /**
- * A multi-producer-multi-consumer, thread-safe {@link Recycler} factory provider implementation based on <a href="https://jctools.github.io/JCTools/">JCTools</a>.
+ * A {@link Recycler} factory provider implementation based on <a href="https://jctools.github.io/JCTools/">JCTools</a>.
+ *
+ * @since 3.0.0
  */
 @ServiceProvider(RecyclerFactoryProvider.class)
-public final class JCToolsMpmcRecyclerFactoryProvider implements RecyclerFactoryProvider {
+public final class JCToolsRecyclerFactoryProvider implements RecyclerFactoryProvider {
 
     @Override
     public int getOrder() {
@@ -57,10 +59,8 @@ public final class JCToolsMpmcRecyclerFactoryProvider implements RecyclerFactory
         return new JCToolsMpmcRecyclerFactory(capacity);
     }
 
-    // Visible for testing
-    static final class JCToolsMpmcRecyclerFactory implements RecyclerFactory {
+    private static final class JCToolsMpmcRecyclerFactory implements RecyclerFactory {
 
-        // Visible for testing
         private final int capacity;
 
         private JCToolsMpmcRecyclerFactory(final int capacity) {
@@ -75,13 +75,11 @@ public final class JCToolsMpmcRecyclerFactoryProvider implements RecyclerFactory
             return new JCToolsMpmcRecycler<>(supplier, cleaner, queue);
         }
 
-        // Visible for testing
-        static final class JCToolsMpmcRecycler<V> extends AbstractRecycler<V> {
+        private static final class JCToolsMpmcRecycler<V> extends AbstractRecycler<V> {
 
             private final Consumer<V> cleaner;
 
-            // Visible for testing
-            final Queue<V> queue;
+            private final Queue<V> queue;
 
             private JCToolsMpmcRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final Queue<V> queue) {
                 super(supplier);
diff --git a/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactoryTest.java b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactoryTest.java
new file mode 100644
index 0000000000..03e96f05ba
--- /dev/null
+++ b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactoryTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.jctools;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.logging.log4j.LoggingException;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AsyncAppender;
+import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
+import org.apache.logging.log4j.spi.ExtendedLogger;
+import org.junit.jupiter.api.Test;
+
+class JCToolsBlockingQueueFactoryTest {
+
+    private static void exceptionTest(final LoggerContext context) throws InterruptedException {
+        final ExtendedLogger logger = context.getLogger(AsyncAppender.class);
+        final Exception parent = new IllegalStateException("Test");
+        final Throwable child = new LoggingException("This is a test", parent);
+        logger.error("This is a test", child);
+        final ListAppender appender = context.getConfiguration().getAppender("List");
+        final List<String> messages;
+        try {
+            messages = appender.getMessages(1, 2, TimeUnit.SECONDS);
+        } finally {
+            appender.clear();
+        }
+        assertNotNull(messages);
+        assertEquals(1, messages.size());
+        assertTrue(messages.get(0).contains(parent.getClass().getName()));
+    }
+
+    private static void rewriteTest(final LoggerContext context) throws InterruptedException {
+        final ExtendedLogger logger = context.getLogger(AsyncAppender.class);
+        logger.error("This is a test");
+        logger.warn("Hello world!");
+        final ListAppender appender = context.getConfiguration().getAppender("List");
+        final List<String> messages;
+        try {
+            messages = appender.getMessages(2, 2, TimeUnit.SECONDS);
+        } finally {
+            appender.clear();
+        }
+        assertNotNull(messages);
+        assertEquals(2, messages.size());
+        final String messagePrefix = JCToolsBlockingQueueFactoryTest.class.getName() + " rewriteTest ";
+        assertEquals(messagePrefix + "This is a test", messages.get(0));
+        assertEquals(messagePrefix + "Hello world!", messages.get(1));
+    }
+
+    @Test
+    @LoggerContextSource("JCToolsBlockingQueueFactoryTest.xml")
+    public void testJcToolsBlockingQueue(final LoggerContext context) throws InterruptedException {
+        rewriteTest(context);
+        exceptionTest(context);
+    }
+}
diff --git a/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProviderTest.java b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProviderTest.java
similarity index 91%
rename from log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProviderTest.java
rename to log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProviderTest.java
index b936c14309..51e9d9e272 100644
--- a/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProviderTest.java
+++ b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProviderTest.java
@@ -24,7 +24,7 @@ import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
 import org.apache.logging.log4j.spi.recycler.RecyclerFactoryRegistry;
 import org.junit.jupiter.api.Test;
 
-class JCToolsMpmcRecyclerFactoryProviderTest {
+class JCToolsRecyclerFactoryProviderTest {
 
     @Test
     void verify_is_the_first() {
@@ -32,6 +32,6 @@ class JCToolsMpmcRecyclerFactoryProviderTest {
                 .sorted(Comparator.comparing(RecyclerFactoryProvider::getOrder))
                 .<Class<?>>map(RecyclerFactoryProvider::getClass)
                 .toList();
-        assertThat(providerClasses).startsWith(JCToolsMpmcRecyclerFactoryProvider.class);
+        assertThat(providerClasses).startsWith(JCToolsRecyclerFactoryProvider.class);
     }
 }
diff --git a/log4j-core-test/src/test/resources/BlockingQueueFactory-JCToolsBlockingQueue.xml b/log4j-jctools/src/test/resources/JCToolsBlockingQueueFactoryTest.xml
similarity index 95%
rename from log4j-core-test/src/test/resources/BlockingQueueFactory-JCToolsBlockingQueue.xml
rename to log4j-jctools/src/test/resources/JCToolsBlockingQueueFactoryTest.xml
index c53500891b..1b6ff95acb 100644
--- a/log4j-core-test/src/test/resources/BlockingQueueFactory-JCToolsBlockingQueue.xml
+++ b/log4j-jctools/src/test/resources/JCToolsBlockingQueueFactoryTest.xml
@@ -15,7 +15,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<Configuration status="OFF" name="JCToolsBlockingQueueFactory">
+<Configuration status="OFF" name="JCToolsBlockingQueueFactoryTest">
 
   <Appenders>
     <Console name="STDOUT">
diff --git a/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider b/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider
index 36741a8bb5..25c8b92149 100644
--- a/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider
+++ b/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider
@@ -3,4 +3,4 @@
 # As a result, tests of the same artifact (running against `target/` contents, not the JAR) don't have them.
 # To mitigate this, we manually create this file here only for tests.
 # `logging-parent` version `10.5.0` will switch from `bnd:jar` to `bnd:bnd-process`, then this hack won't be needed.
-org.apache.logging.log4j.jctools.JCToolsMpmcRecyclerFactoryProvider
+org.apache.logging.log4j.jctools.JCToolsRecyclerFactoryProvider
diff --git a/log4j-layout-template-json-test/pom.xml b/log4j-layout-template-json-test/pom.xml
index 7a24826ad0..f0945107da 100644
--- a/log4j-layout-template-json-test/pom.xml
+++ b/log4j-layout-template-json-test/pom.xml
@@ -78,12 +78,6 @@
       <artifactId>java-allocation-instrumenter</artifactId>
       <scope>test</scope>
     </dependency>
-    <!-- needed for `RecyclerFactoriesTest` -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/log4j-layout-template-json-test/src/test/resources/recyclerFactoryCustomizedJsonTemplateLayoutLogging.xml b/log4j-layout-template-json-test/src/test/resources/recyclerFactoryCustomizedJsonTemplateLayoutLogging.xml
deleted file mode 100644
index 63936384cc..0000000000
--- a/log4j-layout-template-json-test/src/test/resources/recyclerFactoryCustomizedJsonTemplateLayoutLogging.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  ~ 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.
-  -->
-<Configuration status="OFF">
-  <Appenders>
-    <List name="List" raw="true">
-      <JsonTemplateLayout
-          recyclerFactory="queue:supplier=org.jctools.queues.MpmcArrayQueue.new,capacity=512"/>
-    </List>
-  </Appenders>
-  <Loggers>
-    <Root level="TRACE">
-      <AppenderRef ref="List"/>
-    </Root>
-  </Loggers>
-</Configuration>
diff --git a/log4j-layout-template-json/pom.xml b/log4j-layout-template-json/pom.xml
index 651512771e..6279900cd6 100644
--- a/log4j-layout-template-json/pom.xml
+++ b/log4j-layout-template-json/pom.xml
@@ -52,11 +52,6 @@
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-plugins</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
   </dependencies>
 
   <build>
diff --git a/pom.xml b/pom.xml
index 2344d4c60f..92041a027c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -357,12 +357,6 @@
         <version>${project.version}</version>
       </dependency>
 
-      <dependency>
-        <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-api-queue-jctools</artifactId>
-        <version>${project.version}</version>
-      </dependency>
-
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-api-test</artifactId>
@@ -417,6 +411,12 @@
         <version>${project.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-jctools</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-jdbc</artifactId>
diff --git a/src/site/asciidoc/manual/appenders.adoc b/src/site/asciidoc/manual/appenders.adoc
index c2127cf344..5256b8adab 100644
--- a/src/site/asciidoc/manual/appenders.adoc
+++ b/src/site/asciidoc/manual/appenders.adoc
@@ -192,6 +192,7 @@ attribute, `spinPolicy`, which corresponds to the `SpinPolicy` enum.
 |JCToolsBlockingQueue |This uses
 https://jctools.github.io/JCTools/[JCTools], specifically the MPSC
 bounded lock-free queue.
+This implementation is provided by the `log4j-jctools` artifact.
 
 |LinkedTransferQueue |This uses the new in Java 7 implementation
 https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/LinkedTransferQueue.html[`LinkedTransferQueue`].
diff --git a/src/site/asciidoc/runtime-dependencies.adoc b/src/site/asciidoc/runtime-dependencies.adoc
index e46761edc7..e98983a2a6 100644
--- a/src/site/asciidoc/runtime-dependencies.adoc
+++ b/src/site/asciidoc/runtime-dependencies.adoc
@@ -182,8 +182,6 @@ The module info definition may be modified in the future to export these only to
 !
 !requires static com.lmax.disruptor
 !
-!requires static org.jctools.core
-!
 !requires static org.osgi.framework
 !
 !requires static com.conversantmedia.disruptor
@@ -312,8 +310,6 @@ The module info definition may be modified in the future to export these only to
 !
 !requires org.apache.logging.log4j.core
 !
-!requires org.jctools.core
-!
 !===
 
 |log4j-plugin-processor


(logging-log4j2) 03/04: Create `log4j-jctools` module with a JCTools-based recycler

Posted by vy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch api-queue
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit b37dcc14c3d6b674508c1f856f686818031f03d5
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Fri Dec 15 19:15:38 2023 +0100

    Create `log4j-jctools` module with a JCTools-based recycler
---
 .../log4j/internal/RecyclerFactoriesTest.java      | 102 ------
 .../log4j/internal/StringParameterParserTest.java  | 373 ---------------------
 .../internal/{ => recycler}/ArrayQueueTest.java    |   2 +-
 .../recycler/RecyclerFactoriesTestUtil.java        |  46 +++
 .../recycler/RecyclerFactoryRegistryTest.java      |  85 +++++
 .../ThreadLocalRecyclerFactoryProviderTest.java}   |  46 ++-
 .../logging/log4j/internal/DefaultLogBuilder.java  |   4 +-
 .../logging/log4j/internal/QueueFactories.java     | 143 --------
 .../log4j/internal/QueueingRecyclerFactory.java    |  92 -----
 .../logging/log4j/internal/RecyclerFactories.java  | 173 ----------
 .../log4j/internal/StringParameterParser.java      | 304 -----------------
 .../log4j/internal/ThreadLocalRecyclerFactory.java | 101 ------
 .../log4j/internal/{ => recycler}/ArrayQueue.java  |   2 +-
 .../recycler/DummyRecyclerFactoryProvider.java     |  81 +++++
 .../recycler/QueueingRecyclerFactoryProvider.java  | 106 ++++++
 .../ThreadLocalRecyclerFactoryProvider.java        | 125 +++++++
 .../logging/log4j/message/MessageFactory.java      |   2 +-
 .../log4j/message/ParameterizedMessage.java        |   2 +-
 .../log4j/message/ReusableMessageFactory.java      |   4 +-
 .../message/ReusableParameterizedMessage.java      |   4 +-
 .../apache/logging/log4j/spi/AbstractLogger.java   |   2 +
 .../logging/log4j/spi/DummyRecyclerFactory.java    |  59 ----
 .../apache/logging/log4j/spi/LoggingSystem.java    |   6 +-
 .../logging/log4j/spi/LoggingSystemProperty.java   |  13 +-
 .../logging/log4j/spi/PropertyComponent.java       |   2 +
 .../org/apache/logging/log4j/spi/QueueFactory.java |  32 --
 .../apache/logging/log4j/spi/RecyclerFactory.java  |  58 ----
 .../log4j/spi/{ => recycler}/AbstractRecycler.java |  10 +-
 .../logging/log4j/spi/{ => recycler}/Recycler.java |  10 +-
 .../log4j/spi/{ => recycler}/RecyclerAware.java    |   3 +-
 .../log4j/spi/recycler/RecyclerFactory.java        |  58 ++++
 .../spi/recycler/RecyclerFactoryProvider.java      |  59 ++++
 .../spi/recycler/RecyclerFactoryRegistry.java      | 119 +++++++
 .../logging/log4j/spi/recycler/package-info.java   |  26 ++
 .../apache/logging/log4j/status/StatusLogger.java  |   5 +-
 .../logging/log4j/util/ServiceLoaderUtil.java      |  11 +
 .../logging/log4j/core/config/Configuration.java   |   2 +-
 .../log4j/core/filter/StructuredDataFilter.java    |   4 +-
 .../logging/log4j/core/impl/DefaultBundle.java     |   1 +
 .../logging/log4j/core/impl/MutableLogEvent.java   |   2 +-
 .../log4j/core/impl/ReusableLogEventFactory.java   |   4 +-
 .../log4j/core/layout/AbstractStringLayout.java    |   4 +-
 .../logging/log4j/core/layout/PatternLayout.java   |   2 +-
 .../log4j/core/layout/StringBuilderEncoder.java    |   2 +-
 .../apache/logging/log4j/core/util/JsonUtils.java  |   2 +-
 {log4j-api-queue-jctools => log4j-jctools}/pom.xml |  21 +-
 .../JCToolsMpmcRecyclerFactoryProvider.java        | 106 ++++++
 .../JCToolsMpmcRecyclerFactoryProviderTest.java    |  32 +-
 ...ging.log4j.spi.recycler.RecyclerFactoryProvider |   6 +
 .../layout/template/json/JsonTemplateLayout.java   |   2 +-
 .../template/json/resolver/CounterResolver.java    |   2 +-
 .../json/resolver/MessageParameterResolver.java    |   2 +-
 .../json/resolver/ReadOnlyStringMapResolver.java   |   4 +-
 .../json/resolver/StackTraceStringResolver.java    |   4 +-
 .../plugins/convert/TypeConverterFactory.java      |   3 -
 .../java/org/apache/logging/slf4j/SLF4JLogger.java |   2 +-
 pom.xml                                            |   2 +-
 57 files changed, 961 insertions(+), 1518 deletions(-)

diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/RecyclerFactoriesTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/RecyclerFactoriesTest.java
deleted file mode 100644
index 9673abe14f..0000000000
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/RecyclerFactoriesTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import java.util.ArrayDeque;
-import java.util.concurrent.ArrayBlockingQueue;
-import org.apache.logging.log4j.spi.DummyRecyclerFactory;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
-import org.assertj.core.api.Assertions;
-import org.assertj.core.api.InstanceOfAssertFactories;
-import org.junit.jupiter.api.Test;
-
-public class RecyclerFactoriesTest {
-
-    @Test
-    void DummyRecyclerFactory_should_work() {
-        final RecyclerFactory actualDummyRecyclerFactory = RecyclerFactories.ofSpec("dummy");
-        Assertions.assertThat(actualDummyRecyclerFactory).isSameAs(DummyRecyclerFactory.getInstance());
-    }
-
-    @Test
-    void ThreadLocalRecyclerFactory_should_work() {
-        final RecyclerFactory actualThreadLocalRecyclerFactory = RecyclerFactories.ofSpec("threadLocal");
-        Assertions.assertThat(actualThreadLocalRecyclerFactory)
-                .asInstanceOf(InstanceOfAssertFactories.type(ThreadLocalRecyclerFactory.class))
-                .extracting(ThreadLocalRecyclerFactory::getCapacity)
-                .isEqualTo(RecyclerFactories.CAPACITY);
-    }
-
-    @Test
-    void ThreadLocalRecyclerFactory_should_work_with_capacity() {
-        final int capacity = 13;
-        final RecyclerFactory actualThreadLocalRecyclerFactory =
-                RecyclerFactories.ofSpec("threadLocal:capacity=" + capacity);
-        Assertions.assertThat(actualThreadLocalRecyclerFactory)
-                .asInstanceOf(InstanceOfAssertFactories.type(ThreadLocalRecyclerFactory.class))
-                .extracting(ThreadLocalRecyclerFactory::getCapacity)
-                .isEqualTo(capacity);
-    }
-
-    @Test
-    void QueueingRecyclerFactory_should_work() {
-        final RecyclerFactory actualQueueingRecyclerFactory = RecyclerFactories.ofSpec("queue");
-        Assertions.assertThat(actualQueueingRecyclerFactory)
-                .asInstanceOf(InstanceOfAssertFactories.type(QueueingRecyclerFactory.class))
-                .extracting(QueueingRecyclerFactory::getCapacity)
-                .isEqualTo(RecyclerFactories.CAPACITY);
-    }
-
-    @Test
-    void QueueingRecyclerFactory_should_work_with_supplier() {
-        final RecyclerFactory recyclerFactory = RecyclerFactories.ofSpec("queue:supplier=java.util.ArrayDeque.new");
-        Assertions.assertThat(recyclerFactory).isInstanceOf(QueueingRecyclerFactory.class);
-        final QueueingRecyclerFactory queueingRecyclerFactory = (QueueingRecyclerFactory) recyclerFactory;
-        final Recycler<Object> recycler = queueingRecyclerFactory.create(Object::new);
-        Assertions.assertThat(recycler).isInstanceOf(QueueingRecyclerFactory.QueueingRecycler.class);
-        final QueueingRecyclerFactory.QueueingRecycler<Object> queueingRecycler =
-                (QueueingRecyclerFactory.QueueingRecycler<Object>) recycler;
-        Assertions.assertThat(queueingRecycler.getQueue()).isInstanceOf(ArrayDeque.class);
-    }
-
-    @Test
-    void QueueingRecyclerFactory_should_work_with_capacity() {
-        final int capacity = 100;
-        final RecyclerFactory actualQueueingRecyclerFactory = RecyclerFactories.ofSpec("queue:capacity=" + capacity);
-        Assertions.assertThat(actualQueueingRecyclerFactory)
-                .asInstanceOf(InstanceOfAssertFactories.type(QueueingRecyclerFactory.class))
-                .extracting(QueueingRecyclerFactory::getCapacity)
-                .isEqualTo(capacity);
-    }
-
-    @Test
-    void QueueingRecyclerFactory_should_work_with_supplier_and_capacity() {
-        final int capacity = 100;
-        final RecyclerFactory recyclerFactory = RecyclerFactories.ofSpec(
-                "queue:supplier=java.util.concurrent.ArrayBlockingQueue.new,capacity=" + capacity);
-        Assertions.assertThat(recyclerFactory).isInstanceOf(QueueingRecyclerFactory.class);
-        final QueueingRecyclerFactory queueingRecyclerFactory = (QueueingRecyclerFactory) recyclerFactory;
-        final Recycler<Object> recycler = queueingRecyclerFactory.create(Object::new);
-        Assertions.assertThat(recycler).isInstanceOf(QueueingRecyclerFactory.QueueingRecycler.class);
-        final QueueingRecyclerFactory.QueueingRecycler<Object> queueingRecycler =
-                (QueueingRecyclerFactory.QueueingRecycler<Object>) recycler;
-        Assertions.assertThat(queueingRecycler.getQueue()).isInstanceOf(ArrayBlockingQueue.class);
-        final ArrayBlockingQueue<Object> queue = (ArrayBlockingQueue<Object>) queueingRecycler.getQueue();
-        Assertions.assertThat(queue.remainingCapacity()).isEqualTo(capacity);
-    }
-}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/StringParameterParserTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/StringParameterParserTest.java
deleted file mode 100644
index 512cc708c6..0000000000
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/StringParameterParserTest.java
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import org.apache.logging.log4j.internal.StringParameterParser.DoubleQuotedStringValue;
-import org.apache.logging.log4j.internal.StringParameterParser.NullValue;
-import org.apache.logging.log4j.internal.StringParameterParser.StringValue;
-import org.apache.logging.log4j.internal.StringParameterParser.Value;
-import org.apache.logging.log4j.internal.StringParameterParser.Values;
-import org.assertj.core.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-@SuppressWarnings("DoubleBraceInitialization")
-class StringParameterParserTest {
-
-    @Test
-    void test_empty_string() {
-        testSuccess("", Collections.emptyMap());
-    }
-
-    @Test
-    void test_blank_string() {
-        testSuccess("\t", Collections.emptyMap());
-    }
-
-    @Test
-    void test_simple_pair() {
-        testSuccess("a=b", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_simple_pair_with_whitespace_1() {
-        testSuccess(" a=b", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_simple_pair_with_whitespace_2() {
-        testSuccess(" a =b", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_simple_pair_with_whitespace_3() {
-        testSuccess(" a = b", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_simple_pair_with_whitespace_4() {
-        testSuccess(" a = b ", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_null_value_1() {
-        testSuccess("a", Collections.singletonMap("a", Values.nullValue()));
-    }
-
-    @Test
-    void test_null_value_2() {
-        testSuccess("a,b=c,d=", new LinkedHashMap<>() {
-            {
-                put("a", Values.nullValue());
-                put("b", Values.stringValue("c"));
-                put("d", Values.nullValue());
-            }
-        });
-    }
-
-    @Test
-    void test_null_value_3() {
-        testSuccess("a,b=c,d", new LinkedHashMap<>() {
-            {
-                put("a", Values.nullValue());
-                put("b", Values.stringValue("c"));
-                put("d", Values.nullValue());
-            }
-        });
-    }
-
-    @Test
-    void test_null_value_4() {
-        testSuccess("a,b=\"c,=\\\"\",d=,e=f", new LinkedHashMap<>() {
-            {
-                put("a", Values.nullValue());
-                put("b", Values.doubleQuotedStringValue("c,=\""));
-                put("d", Values.nullValue());
-                put("e", Values.stringValue("f"));
-            }
-        });
-    }
-
-    @Test
-    void test_two_pairs() {
-        testSuccess("a=b,c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.stringValue("b"));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_01() {
-        testSuccess("a=\"b\"", Collections.singletonMap("a", Values.doubleQuotedStringValue("b")));
-    }
-
-    @Test
-    void test_quoted_string_02() {
-        testSuccess("a=\"b\",c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("b"));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_03() {
-        testSuccess("a=b,c=\"d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.stringValue("b"));
-                put("c", Values.doubleQuotedStringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_04() {
-        testSuccess("a=\"b\",c=\"d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("b"));
-                put("c", Values.doubleQuotedStringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_05() {
-        testSuccess("a=\"\\\"b\"", Collections.singletonMap("a", Values.doubleQuotedStringValue("\"b")));
-    }
-
-    @Test
-    void test_quoted_string_06() {
-        testSuccess("a=\"\\\"b\\\"\"", Collections.singletonMap("a", Values.doubleQuotedStringValue("\"b\"")));
-    }
-
-    @Test
-    void test_quoted_string_07() {
-        testSuccess("a=\"\\\"b\",c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b"));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_08() {
-        testSuccess("a=\"\\\"b\\\"\",c=d", new LinkedHashMap<String, Value>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\""));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_09() {
-        testSuccess("a=\"\\\"b,\",c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b,"));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_10() {
-        testSuccess("a=\"\\\"b\\\",\",c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\","));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_11() {
-        testSuccess("a=\"\\\"b\",c=\"d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b"));
-                put("c", Values.doubleQuotedStringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_12() {
-        testSuccess("a=\"\\\"b\\\"\",c=\"d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\""));
-                put("c", Values.doubleQuotedStringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_13() {
-        testSuccess("a=\"\\\"b,\",c=\"\\\"d\"", new LinkedHashMap<String, Value>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b,"));
-                put("c", Values.doubleQuotedStringValue("\"d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_14() {
-        testSuccess("a=\"\\\"b\\\",\",c=\"\\\"d\\\"\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\","));
-                put("c", Values.doubleQuotedStringValue("\"d\""));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_15() {
-        testSuccess("a=\"\\\"b\",c=\",d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b"));
-                put("c", Values.doubleQuotedStringValue(",d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_16() {
-        testSuccess("a=\"\\\"b\\\"\",c=\",d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\""));
-                put("c", Values.doubleQuotedStringValue(",d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_17() {
-        testSuccess("a=\"\\\"b,\",c=\"\\\"d,\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b,"));
-                put("c", Values.doubleQuotedStringValue("\"d,"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_18() {
-        testSuccess("a=\"\\\"b\\\",\",c=\"\\\"d\\\",\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\","));
-                put("c", Values.doubleQuotedStringValue("\"d\","));
-            }
-        });
-    }
-
-    private static void testSuccess(final String input, final Map<String, Value> expectedMap) {
-        final Map<String, Value> actualMap = StringParameterParser.parse(input);
-        Assertions.assertThat(actualMap).as("input: %s", input).isEqualTo(expectedMap);
-    }
-
-    @Test
-    void test_missing_key() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = ",a=b";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("failed to locate key at index 0");
-    }
-
-    @Test
-    void test_conflicting_key() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,a";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("conflicting key at index 2");
-    }
-
-    @Test
-    void test_prematurely_ending_quoted_string_01() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b=\"";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("failed to locate the end of double-quoted content starting at index 4");
-    }
-
-    @Test
-    void test_prematurely_ending_quoted_string_02() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b=\"c";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("failed to locate the end of double-quoted content starting at index 4");
-    }
-
-    @Test
-    void test_prematurely_ending_quoted_string_03() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b=\",c";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("failed to locate the end of double-quoted content starting at index 4");
-    }
-
-    @Test
-    void test_prematurely_ending_quoted_string_04() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b=\",c\" x";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("was expecting comma at index 9");
-    }
-
-    @Test
-    void test_NullValue_toString() {
-        final Map<String, Value> map = StringParameterParser.parse("a");
-        final NullValue value = (NullValue) map.get("a");
-        Assertions.assertThat(value.toString()).isEqualTo("null");
-    }
-
-    @Test
-    void test_StringValue_toString() {
-        final Map<String, Value> map = StringParameterParser.parse("a=b");
-        final StringValue value = (StringValue) map.get("a");
-        Assertions.assertThat(value.toString()).isEqualTo("b");
-    }
-
-    @Test
-    void test_DoubleQuotedStringValue_toString() {
-        final Map<String, Value> map = StringParameterParser.parse("a=\"\\\"b\"");
-        final DoubleQuotedStringValue value = (DoubleQuotedStringValue) map.get("a");
-        Assertions.assertThat(value.toString()).isEqualTo("\"b");
-    }
-
-    @Test
-    void test_allowedKeys() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b";
-                    final Set<String> allowedKeys = new LinkedHashSet<>(Collections.singletonList("a"));
-                    StringParameterParser.parse(input, allowedKeys);
-                })
-                .hasMessageStartingWith("unknown key \"b\" is found in input: a,b");
-    }
-}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ArrayQueueTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ArrayQueueTest.java
similarity index 98%
rename from log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ArrayQueueTest.java
rename to log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ArrayQueueTest.java
index e063e7f81c..2ded8ff7a8 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ArrayQueueTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ArrayQueueTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.internal;
+package org.apache.logging.log4j.internal.recycler;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoriesTestUtil.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoriesTestUtil.java
new file mode 100644
index 0000000000..44422a3319
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoriesTestUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Properties;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryRegistry;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+final class RecyclerFactoriesTestUtil {
+
+    private RecyclerFactoriesTestUtil() {}
+
+    @Nullable
+    static RecyclerFactory createForEnvironment(
+            @Nullable Boolean threadLocalsEnabled, @Nullable final String factory, @Nullable final Integer capacity) {
+        final Properties properties = new Properties();
+        if (threadLocalsEnabled != null) {
+            properties.setProperty("log4j2.*.ThreadLocals.enable", "" + threadLocalsEnabled);
+        }
+        if (factory != null) {
+            properties.setProperty("log4j2.*.Recycler.factory", factory);
+        }
+        if (capacity != null) {
+            properties.setProperty("log4j2.*.Recycler.capacity", "" + capacity);
+        }
+        final PropertyEnvironment env = new PropertiesUtil(properties);
+        return RecyclerFactoryRegistry.findRecyclerFactory(env);
+    }
+}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java
new file mode 100644
index 0000000000..8793c1244e
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import static org.apache.logging.log4j.internal.recycler.RecyclerFactoriesTestUtil.createForEnvironment;
+import static org.apache.logging.log4j.spi.recycler.Recycler.DEFAULT_CAPACITY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.logging.log4j.internal.recycler.DummyRecyclerFactoryProvider.DummyRecyclerFactory;
+import org.apache.logging.log4j.internal.recycler.QueueingRecyclerFactoryProvider.QueueingRecyclerFactory;
+import org.apache.logging.log4j.internal.recycler.ThreadLocalRecyclerFactoryProvider.ThreadLocalRecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+
+public class RecyclerFactoryRegistryTest {
+
+    @Test
+    void DummyRecyclerFactory_should_work() {
+        final RecyclerFactory factory = createForEnvironment(null, "dummy", null);
+        assertThat(factory).isInstanceOf(DummyRecyclerFactory.class);
+    }
+
+    @Test
+    void ThreadLocalRecyclerFactory_should_work() {
+        final RecyclerFactory factory = createForEnvironment(null, "threadLocal", null);
+        assertThat(factory)
+                .asInstanceOf(InstanceOfAssertFactories.type(ThreadLocalRecyclerFactory.class))
+                .extracting(factory_ -> factory_.capacity)
+                .isEqualTo(DEFAULT_CAPACITY);
+    }
+
+    @Test
+    void ThreadLocalRecyclerFactory_should_work_with_capacity() {
+        final int capacity = 13;
+        final RecyclerFactory factory = createForEnvironment(null, "threadLocal", capacity);
+        assertThat(factory)
+                .asInstanceOf(InstanceOfAssertFactories.type(ThreadLocalRecyclerFactory.class))
+                .extracting(factory_ -> factory_.capacity)
+                .isEqualTo(capacity);
+    }
+
+    @Test
+    void QueueingRecyclerFactory_should_work() {
+        final RecyclerFactory factory = createForEnvironment(null, "queue", null);
+        assertThat(factory)
+                .asInstanceOf(InstanceOfAssertFactories.type(QueueingRecyclerFactory.class))
+                .extracting(factory_ -> factory_.capacity)
+                .isEqualTo(DEFAULT_CAPACITY);
+    }
+
+    @Test
+    void QueueingRecyclerFactory_should_work_with_capacity() {
+        final int capacity = 100;
+        final RecyclerFactory factory = createForEnvironment(null, "queue", capacity);
+        assertThat(factory)
+                .asInstanceOf(InstanceOfAssertFactories.type(QueueingRecyclerFactory.class))
+                .extracting(factory_ -> factory_.capacity)
+                .isEqualTo(capacity);
+    }
+
+    @Test
+    void verify_order() {
+        final RecyclerFactoryProvider dummyProvider = new DummyRecyclerFactoryProvider();
+        final RecyclerFactoryProvider threadLocalProvider = new ThreadLocalRecyclerFactoryProvider();
+        final RecyclerFactoryProvider queueProvider = new QueueingRecyclerFactoryProvider();
+        assertThat(dummyProvider.getOrder()).isGreaterThan(queueProvider.getOrder());
+        assertThat(queueProvider.getOrder()).isGreaterThan(threadLocalProvider.getOrder());
+    }
+}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactoryTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java
similarity index 60%
rename from log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactoryTest.java
rename to log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java
index c822e2218f..7ccde99823 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactoryTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java
@@ -14,34 +14,48 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.internal;
+package org.apache.logging.log4j.internal.recycler;
 
+import static org.apache.logging.log4j.internal.recycler.RecyclerFactoriesTestUtil.createForEnvironment;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.util.List;
 import java.util.Queue;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.internal.recycler.ThreadLocalRecyclerFactoryProvider.ThreadLocalRecyclerFactory;
+import org.apache.logging.log4j.internal.recycler.ThreadLocalRecyclerFactoryProvider.ThreadLocalRecyclerFactory.ThreadLocalRecycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junitpioneer.jupiter.params.IntRangeSource;
 
-class ThreadLocalRecyclerFactoryTest {
+class ThreadLocalRecyclerFactoryProviderTest {
 
-    private static final int CAPACITY = 8;
+    private static final int CAPACITY = 13;
 
     private static class RecyclableObject {}
 
-    private Recycler<RecyclableObject> recycler;
+    private ThreadLocalRecycler<RecyclableObject> recycler;
 
     private Queue<RecyclableObject> recyclerQueue;
 
     @BeforeEach
     void setUp() {
-        recycler = new ThreadLocalRecyclerFactory(CAPACITY).create(RecyclableObject::new);
-        recyclerQueue = ((ThreadLocalRecyclerFactory.ThreadLocalRecycler<RecyclableObject>) recycler).getQueue();
+        final RecyclerFactory recyclerFactory = createForEnvironment(null, "threadLocal", CAPACITY);
+        assertThat(recyclerFactory).isInstanceOf(ThreadLocalRecyclerFactory.class);
+        assert recyclerFactory != null;
+        recycler = (ThreadLocalRecycler<RecyclableObject>) recyclerFactory.create(RecyclableObject::new);
+        recyclerQueue = recycler.queueRef.get();
+    }
+
+    @Test
+    void should_not_be_configured_when_TLs_are_disabled() {
+        assertThatThrownBy(() -> createForEnvironment(false, "threadLocal", null))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageStartingWith("failed to configure recycler");
     }
 
     @ParameterizedTest
@@ -79,18 +93,22 @@ class ThreadLocalRecyclerFactoryTest {
 
         assertThat(recyclerQueue).isEmpty();
 
-        // simulate a massively callstack with tons of logging
-        final List<RecyclableObject> acquiredObjects =
-                IntStream.range(0, 1024).mapToObj(i -> recycler.acquire()).collect(Collectors.toList());
+        // Simulate a callstack with excessive logging
+        final int acquisitionCount = Math.addExact(CAPACITY, 1024);
+        final List<RecyclableObject> acquiredObjects = IntStream.range(0, acquisitionCount)
+                .mapToObj(i -> recycler.acquire())
+                .toList();
 
-        // still nothing returned to pool
+        // Verify collected instances are all new
+        assertThat(acquiredObjects).doesNotHaveDuplicates();
+
+        // Verify the pool is still empty
         assertThat(recyclerQueue).isEmpty();
 
-        // don't want any duplicate instances
-        assertThat(acquiredObjects).containsOnlyOnceElementsOf(acquiredObjects);
+        // Release all acquired instances
         acquiredObjects.forEach(recycler::release);
 
-        // upon return, we should only have `CAPACITY` retained for future use
+        // Verify the queue size is capped
         assertThat(recyclerQueue).hasSize(CAPACITY);
     }
 }
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 e53fd1f589..cffd2ce56f 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
@@ -24,8 +24,8 @@ 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;
-import org.apache.logging.log4j.spi.RecyclerAware;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerAware;
 import org.apache.logging.log4j.util.*;
 
 /**
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
deleted file mode 100644
index b0275c4506..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import static org.apache.logging.log4j.util.LowLevelLogUtil.log;
-
-import aQute.bnd.annotation.spi.ServiceConsumer;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.util.List;
-import java.util.Queue;
-import java.util.ServiceLoader;
-import java.util.concurrent.ArrayBlockingQueue;
-import org.apache.logging.log4j.spi.QueueFactory;
-import org.apache.logging.log4j.util.Cast;
-import org.apache.logging.log4j.util.InternalApi;
-import org.apache.logging.log4j.util.LoaderUtil;
-import org.apache.logging.log4j.util.ServiceLoaderUtil;
-
-/**
- * Provides the default {@link QueueFactory} instance.
- *
- * @since 3.0.0
- */
-@InternalApi
-@ServiceConsumer(QueueFactory.class)
-public final class QueueFactories {
-
-    /**
-     * The default {@link QueueFactory} instance.
-     */
-    public static final QueueFactory INSTANCE = findInstance();
-
-    private static QueueFactory findInstance() {
-        final ServiceLoader<QueueFactory> serviceLoader =
-                ServiceLoader.load(QueueFactory.class, QueueFactory.class.getClassLoader());
-        final List<QueueFactory> factories =
-                ServiceLoaderUtil.safeStream(serviceLoader).toList();
-        if (factories.isEmpty()) {
-            return ArrayBlockingQueue::new;
-        } else {
-            final int factoryCount = factories.size();
-            if (factoryCount > 1) {
-                log("Log4j was expecting a single `QueueFactory` provider, found:");
-                for (int factoryIndex = 0; factoryIndex < factoryCount; factoryIndex++) {
-                    log((factoryIndex + 1) + ". `" + factories.get(factoryIndex) + "`");
-                }
-                log("Log4j will use the first `QueueFactory` provider as the default.");
-            }
-            return factories.get(0);
-        }
-    }
-
-    private QueueFactories() {}
-
-    /**
-     * Creates a {@link QueueFactory} using the provided supplier.
-     * <p>
-     * A supplier path must be formatted as follows:
-     * <ul>
-     * <li>{@code <fully-qualified-class-name>.new} – the class constructor accepting a single {@code int} argument (denoting the capacity) will be used (e.g., {@code org.jctools.queues.MpmcArrayQueue.new})</li>
-     * <li>{@code <fully-qualified-class-name>.<static-factory-method>} – the static factory method accepting a single {@code int} argument (denoting the capacity) will be used (e.g., {@code com.acme.Queues.createBoundedQueue})</li>
-     * </ul>
-     * </p>
-     *
-     * @param supplierPath a queue supplier path (e.g., {@code org.jctools.queues.MpmcArrayQueue.new}, {@code com.acme.Queues.createBoundedQueue})
-     * @return a new {@link QueueFactory} instance
-     */
-    public static QueueFactory ofSupplier(final String supplierPath) {
-        final int supplierPathSplitterIndex = supplierPath.lastIndexOf('.');
-        if (supplierPathSplitterIndex < 0) {
-            final String message = String.format("invalid queue factory supplier path: `%s`", supplierPath);
-            throw new IllegalArgumentException(message);
-        }
-        final String supplierClassName = supplierPath.substring(0, supplierPathSplitterIndex);
-        final String supplierMethodName = supplierPath.substring(supplierPathSplitterIndex + 1);
-        try {
-            final Class<?> supplierClass = LoaderUtil.loadClass(supplierClassName);
-            if ("new".equals(supplierMethodName)) {
-                final Constructor<?> supplierCtor = supplierClass.getDeclaredConstructor(int.class);
-                return new ConstructorProvidedQueueFactory(supplierCtor);
-            } else {
-                final Method supplierMethod = supplierClass.getMethod(supplierMethodName, int.class);
-                return new StaticMethodProvidedQueueFactory(supplierMethod);
-            }
-        } catch (final ReflectiveOperationException | LinkageError | SecurityException error) {
-            final String message =
-                    String.format("failed to create the queue factory using the supplier path `%s`", supplierPath);
-            throw new RuntimeException(message, error);
-        }
-    }
-
-    private static final class ConstructorProvidedQueueFactory implements QueueFactory {
-
-        private final Constructor<?> constructor;
-
-        private ConstructorProvidedQueueFactory(final Constructor<?> constructor) {
-            this.constructor = constructor;
-        }
-
-        @Override
-        public <E> Queue<E> create(final int capacity) {
-            final Constructor<Queue<E>> typedConstructor = Cast.cast(constructor);
-            try {
-                return typedConstructor.newInstance(capacity);
-            } catch (final ReflectiveOperationException error) {
-                throw new RuntimeException("queue construction failure", error);
-            }
-        }
-    }
-
-    private static final class StaticMethodProvidedQueueFactory implements QueueFactory {
-
-        private final Method method;
-
-        private StaticMethodProvidedQueueFactory(final Method method) {
-            this.method = method;
-        }
-
-        @Override
-        public <E> Queue<E> create(final int capacity) {
-            try {
-                return Cast.cast(method.invoke(null, capacity));
-            } catch (final ReflectiveOperationException error) {
-                throw new RuntimeException("queue construction failure", error);
-            }
-        }
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueingRecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueingRecyclerFactory.java
deleted file mode 100644
index b5b6c70f72..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueingRecyclerFactory.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import static java.util.Objects.requireNonNull;
-
-import java.util.Queue;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import org.apache.logging.log4j.spi.AbstractRecycler;
-import org.apache.logging.log4j.spi.QueueFactory;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
-
-/**
- * A {@link RecyclerFactory} pooling objects in a queue created using the provided {@link QueueFactory}.
- */
-final class QueueingRecyclerFactory implements RecyclerFactory {
-
-    private final QueueFactory queueFactory;
-
-    private final int capacity;
-
-    QueueingRecyclerFactory(final QueueFactory queueFactory, final int capacity) {
-        if (capacity < 1) {
-            throw new IllegalArgumentException("was expecting `capacity > 0`, found: " + capacity);
-        }
-        this.queueFactory = requireNonNull(queueFactory, "queueFactory");
-        this.capacity = capacity;
-    }
-
-    /**
-     * @return the maximum number of objects retained per thread in recyclers created
-     */
-    int getCapacity() {
-        return capacity;
-    }
-
-    @Override
-    public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
-        requireNonNull(supplier, "supplier");
-        requireNonNull(cleaner, "cleaner");
-        final Queue<V> queue = queueFactory.create(capacity);
-        return new QueueingRecycler<>(supplier, cleaner, queue);
-    }
-
-    // Visible for tests
-    static class QueueingRecycler<V> extends AbstractRecycler<V> {
-
-        private final Consumer<V> cleaner;
-
-        private final Queue<V> queue;
-
-        private QueueingRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final Queue<V> queue) {
-            super(supplier);
-            this.cleaner = cleaner;
-            this.queue = queue;
-        }
-
-        // Visible for tests
-        Queue<V> getQueue() {
-            return queue;
-        }
-
-        @Override
-        public V acquire() {
-            final V value = queue.poll();
-            return value != null ? value : createInstance();
-        }
-
-        @Override
-        public void release(final V value) {
-            requireNonNull(value, "value");
-            cleaner.accept(value);
-            queue.offer(value);
-        }
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java
deleted file mode 100644
index c7ec83a87a..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import static java.util.Objects.requireNonNull;
-import static org.apache.logging.log4j.util.Constants.isThreadLocalsEnabled;
-
-import java.util.Map;
-import java.util.Set;
-import org.apache.logging.log4j.spi.DummyRecyclerFactory;
-import org.apache.logging.log4j.spi.QueueFactory;
-import org.apache.logging.log4j.spi.RecyclerFactory;
-import org.apache.logging.log4j.util.InternalApi;
-
-/**
- * Stores the default {@link RecyclerFactory} instance.
- */
-@InternalApi
-public final class RecyclerFactories {
-
-    /**
-     * The default recycler capacity.
-     */
-    public static final int CAPACITY = Math.max(2 * Runtime.getRuntime().availableProcessors() + 1, 8);
-
-    /**
-     * The default recycler instance.
-     */
-    public static final RecyclerFactory INSTANCE = isThreadLocalsEnabled()
-            ? new ThreadLocalRecyclerFactory(CAPACITY)
-            : new QueueingRecyclerFactory(QueueFactories.INSTANCE, CAPACITY);
-
-    private RecyclerFactories() {}
-
-    /**
-     * Creates a {@link RecyclerFactory} instance using the provided specification.
-     * <p>
-     * The recycler factory specification string must be formatted as follows:
-     * </p>
-     * <pre>{@code
-     * recyclerFactorySpec            = dummySpec
-     *                                | threadLocalRecyclerFactorySpec
-     *                                | queueingRecyclerFactorySpec
-     *
-     * dummySpec                      = "dummy"
-     *
-     * threadLocalRecyclerFactorySpec = "threadLocal" , [ ":" , capacityArg ]
-     * capacityArg                    = "capacity=" , integer
-     *
-     * queueingRecyclerFactorySpec    = "queue" , [ ":" , queueingRecyclerFactoryArgs ]
-     * queueingRecyclerFactoryArgs    = queueingRecyclerFactoryArg , [ "," , queueingRecyclerFactoryArg ]*
-     * queueingRecyclerFactoryArg     = capacityArg
-     *                                | queueSupplierArg
-     * queueSupplierArg               = ( classPath , ".new" )
-     *                                | ( classPath , "." , methodName )
-     * }</pre>
-     * <p>
-     * If not specified, {@code capacity} will be set to {@code max(8, 2*C+1)}, where {@code C} denotes the value returned by {@link Runtime#availableProcessors()}.
-     * </p>
-     * <p>
-     * You can find some examples below.
-     * </p>
-     * <ul>
-     * <li><code>{@code dummy}</code></li>
-     * <li><code>{@code threadLocal}</code></li>
-     * <li><code>{@code threadLocal:capacity=13}</code></li>
-     * <li><code>{@code queue}</code></li>
-     * <li><code>{@code queue:supplier=java.util.ArrayDeque.new}</code></li>
-     * <li><code>{@code queue:capacity=100}</code></li>
-     * <li><code>{@code queue:supplier=com.acme.AwesomeQueue.create,capacity=42}</code></li>
-     * </ul>
-     * @param recyclerFactorySpec the recycler factory specification string
-     * @return a recycler factory instance
-     */
-    public static RecyclerFactory ofSpec(final String recyclerFactorySpec) {
-
-        // Check arguments
-        requireNonNull(recyclerFactorySpec, "recyclerFactorySpec");
-
-        // Is a dummy factory requested?
-        if (recyclerFactorySpec.equals("dummy")) {
-            return DummyRecyclerFactory.getInstance();
-        }
-
-        // Is a TLA factory requested?
-        else if (recyclerFactorySpec.startsWith("threadLocal")) {
-            return readThreadLocalRecyclerFactory(recyclerFactorySpec);
-        }
-
-        // Is a queueing factory requested?
-        else if (recyclerFactorySpec.startsWith("queue")) {
-            return readQueueingRecyclerFactory(recyclerFactorySpec);
-        }
-
-        // Bogus input, bail out.
-        else {
-            throw new IllegalArgumentException("invalid recycler factory: " + recyclerFactorySpec);
-        }
-    }
-
-    private static RecyclerFactory readThreadLocalRecyclerFactory(final String recyclerFactorySpec) {
-
-        // Parse the spec
-        final String queueFactorySpec = recyclerFactorySpec.substring(
-                "threadLocal".length() + (recyclerFactorySpec.startsWith("threadLocal:") ? 1 : 0));
-        final Map<String, StringParameterParser.Value> parsedValues =
-                StringParameterParser.parse(queueFactorySpec, Set.of("capacity"));
-
-        // Read the capacity
-        final int capacity = readQueueCapacity(queueFactorySpec, parsedValues);
-
-        // Execute the read spec
-        return new ThreadLocalRecyclerFactory(capacity);
-    }
-
-    private static RecyclerFactory readQueueingRecyclerFactory(final String recyclerFactorySpec) {
-
-        // Parse the spec
-        final String queueFactorySpec =
-                recyclerFactorySpec.substring("queue".length() + (recyclerFactorySpec.startsWith("queue:") ? 1 : 0));
-        final Map<String, StringParameterParser.Value> parsedValues =
-                StringParameterParser.parse(queueFactorySpec, Set.of("supplier", "capacity"));
-
-        // Read the capacity
-        final int capacity = readQueueCapacity(queueFactorySpec, parsedValues);
-
-        // Read the supplier path
-        final StringParameterParser.Value supplierValue = parsedValues.get("supplier");
-        final String supplierPath = supplierValue == null || supplierValue instanceof StringParameterParser.NullValue
-                ? null
-                : supplierValue.toString();
-
-        // Execute the read spec
-        final QueueFactory queueFactory =
-                supplierPath != null ? QueueFactories.ofSupplier(supplierPath) : QueueFactories.INSTANCE;
-        return new QueueingRecyclerFactory(queueFactory, capacity);
-    }
-
-    private static int readQueueCapacity(
-            final String factorySpec, final Map<String, StringParameterParser.Value> parsedValues) {
-        final StringParameterParser.Value capacityValue = parsedValues.get("capacity");
-        if (capacityValue == null || capacityValue instanceof StringParameterParser.NullValue) {
-            return CAPACITY;
-        } else {
-            final int capacity;
-            try {
-                capacity = Integer.parseInt(capacityValue.toString());
-            } catch (final NumberFormatException error) {
-                throw new IllegalArgumentException(
-                        "failed reading `capacity` in recycler factory: " + factorySpec, error);
-            }
-            if (capacity < 1) {
-                throw new IllegalArgumentException(
-                        "was expecting `capacity > 0` in the recycler factory: " + factorySpec);
-            }
-            return capacity;
-        }
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/StringParameterParser.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/StringParameterParser.java
deleted file mode 100644
index 9d1127ae28..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/StringParameterParser.java
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import java.util.*;
-import java.util.concurrent.Callable;
-import org.apache.logging.log4j.util.InternalApi;
-import org.apache.logging.log4j.util.Strings;
-
-/**
- * Utility class for parsing string-formatted parameters, e.g., {@code queue:supplier=com.acme.FastestQueue.new,capacity=42}.
- * <p>
- * See the associated test class for possible combinations and double-, single-quote handling.
- * </p>
- */
-@InternalApi
-public final class StringParameterParser {
-
-    private StringParameterParser() {}
-
-    public static final class Values {
-
-        private Values() {}
-
-        static NullValue nullValue() {
-            return NullValue.INSTANCE;
-        }
-
-        static StringValue stringValue(final String string) {
-            return new StringValue(string);
-        }
-
-        static DoubleQuotedStringValue doubleQuotedStringValue(final String doubleQuotedString) {
-            return new DoubleQuotedStringValue(doubleQuotedString);
-        }
-    }
-
-    public interface Value {}
-
-    public static final class NullValue implements Value {
-
-        private static final NullValue INSTANCE = new NullValue();
-
-        private NullValue() {}
-
-        @Override
-        public String toString() {
-            return "null";
-        }
-    }
-
-    public static final class StringValue implements Value {
-
-        private final String string;
-
-        private StringValue(final String string) {
-            this.string = string;
-        }
-
-        public String getString() {
-            return string;
-        }
-
-        @Override
-        public boolean equals(final Object object) {
-            if (this == object) {
-                return true;
-            }
-            if (object == null || getClass() != object.getClass()) {
-                return false;
-            }
-            final StringValue that = (StringValue) object;
-            return string.equals(that.string);
-        }
-
-        @Override
-        public int hashCode() {
-            return 31 + Objects.hashCode(string);
-        }
-
-        @Override
-        public String toString() {
-            return string;
-        }
-    }
-
-    public static final class DoubleQuotedStringValue implements Value {
-
-        private final String doubleQuotedString;
-
-        private DoubleQuotedStringValue(final String doubleQuotedString) {
-            this.doubleQuotedString = doubleQuotedString;
-        }
-
-        public String getDoubleQuotedString() {
-            return doubleQuotedString;
-        }
-
-        @Override
-        public boolean equals(final Object object) {
-            if (this == object) {
-                return true;
-            }
-            if (object == null || getClass() != object.getClass()) {
-                return false;
-            }
-            final DoubleQuotedStringValue that = (DoubleQuotedStringValue) object;
-            return doubleQuotedString.equals(that.doubleQuotedString);
-        }
-
-        @Override
-        public int hashCode() {
-            return 31 + Objects.hashCode(doubleQuotedString);
-        }
-
-        @Override
-        public String toString() {
-            return doubleQuotedString.replaceAll("\\\\\"", "\"");
-        }
-    }
-
-    private enum State {
-        READING_KEY,
-        READING_VALUE
-    }
-
-    private static final class Parser implements Callable<Map<String, Value>> {
-
-        private final String input;
-
-        private final Map<String, Value> map;
-
-        private State state;
-
-        private int i;
-
-        private String key;
-
-        private Parser(final String input) {
-            this.input = Objects.requireNonNull(input, "input");
-            this.map = new LinkedHashMap<>();
-            this.state = State.READING_KEY;
-            this.i = 0;
-            this.key = null;
-        }
-
-        @Override
-        public Map<String, Value> call() {
-            while (true) {
-                skipWhitespace();
-                if (i >= input.length()) {
-                    break;
-                }
-                switch (state) {
-                    case READING_KEY:
-                        readKey();
-                        break;
-                    case READING_VALUE:
-                        readValue();
-                        break;
-                    default:
-                        throw new IllegalStateException("unknown state: " + state);
-                }
-            }
-            if (state == State.READING_VALUE) {
-                map.put(key, Values.nullValue());
-            }
-            return map;
-        }
-
-        private void readKey() {
-            final int eq = input.indexOf('=', i);
-            final int co = input.indexOf(',', i);
-            final int j;
-            final int nextI;
-            if (eq < 0 && co < 0) {
-                // Neither '=', nor ',' was found.
-                j = nextI = input.length();
-            } else if (eq < 0) {
-                // Found ','.
-                j = nextI = co;
-            } else if (co < 0) {
-                // Found '='.
-                j = eq;
-                nextI = eq + 1;
-            } else if (eq < co) {
-                // Found '=...,'.
-                j = eq;
-                nextI = eq + 1;
-            } else {
-                // Found ',...='.
-                j = co;
-                nextI = co;
-            }
-            key = input.substring(i, j).trim();
-            if (Strings.isEmpty(key)) {
-                final String message = String.format("failed to locate key at index %d: %s", i, input);
-                throw new IllegalArgumentException(message);
-            }
-            if (map.containsKey(key)) {
-                final String message = String.format("conflicting key at index %d: %s", i, input);
-                throw new IllegalArgumentException(message);
-            }
-            state = State.READING_VALUE;
-            i = nextI;
-        }
-
-        private void readValue() {
-            final boolean doubleQuoted = input.charAt(i) == '"';
-            if (doubleQuoted) {
-                readDoubleQuotedStringValue();
-            } else {
-                readStringValue();
-            }
-            key = null;
-            state = State.READING_KEY;
-        }
-
-        private void readDoubleQuotedStringValue() {
-            int j = i + 1;
-            while (j < input.length()) {
-                if (input.charAt(j) == '"' && input.charAt(j - 1) != '\\') {
-                    break;
-                } else {
-                    j++;
-                }
-            }
-            if (j >= input.length()) {
-                final String message = String.format(
-                        "failed to locate the end of double-quoted content starting at index %d: %s", i, input);
-                throw new IllegalArgumentException(message);
-            }
-            final String content = input.substring(i + 1, j).replaceAll("\\\\\"", "\"");
-            final Value value = Values.doubleQuotedStringValue(content);
-            map.put(key, value);
-            i = j + 1;
-            skipWhitespace();
-            if (i < input.length()) {
-                if (input.charAt(i) != ',') {
-                    final String message = String.format("was expecting comma at index %d: %s", i, input);
-                    throw new IllegalArgumentException(message);
-                }
-                i++;
-            }
-        }
-
-        private void skipWhitespace() {
-            while (i < input.length()) {
-                final char c = input.charAt(i);
-                if (!Character.isWhitespace(c)) {
-                    break;
-                } else {
-                    i++;
-                }
-            }
-        }
-
-        private void readStringValue() {
-            int j = input.indexOf(',', i /* + 1*/);
-            if (j < 0) {
-                j = input.length();
-            }
-            final String content = input.substring(i, j);
-            final String trimmedContent = content.trim();
-            final Value value = trimmedContent.isEmpty() ? Values.nullValue() : Values.stringValue(trimmedContent);
-            map.put(key, value);
-            i += content.length() + 1;
-        }
-    }
-
-    public static Map<String, Value> parse(final String input) {
-        return parse(input, null);
-    }
-
-    public static Map<String, Value> parse(final String input, final Set<String> allowedKeys) {
-        if (Strings.isBlank(input)) {
-            return Collections.emptyMap();
-        }
-        final Map<String, Value> map = new Parser(input).call();
-        final Set<String> actualKeys = map.keySet();
-        for (final String actualKey : actualKeys) {
-            final boolean allowed = allowedKeys == null || allowedKeys.contains(actualKey);
-            if (!allowed) {
-                final String message = String.format("unknown key \"%s\" is found in input: %s", actualKey, input);
-                throw new IllegalArgumentException(message);
-            }
-        }
-        return map;
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java
deleted file mode 100644
index a05e9f3166..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import static java.util.Objects.requireNonNull;
-
-import java.util.Queue;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import org.apache.logging.log4j.spi.AbstractRecycler;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
-
-/**
- * A {@link RecyclerFactory} pooling objects in a queue stored in a {@link ThreadLocal}.
- * <p>
- * This strategy may not be appropriate in workloads where units of work are independent of operating system threads such as reactive streams, coroutines, or virtual threads.
- * For such use cases, see {@link QueueingRecyclerFactory}.
- * </p>
- *
- * @since 3.0.0
- */
-final class ThreadLocalRecyclerFactory implements RecyclerFactory {
-
-    /**
-     * Maximum number of objects retained per thread.
-     * <p>
-     * This allows to acquire objects in recursive method calls and maintain minimal overhead in the scenarios where the active instance count goes far beyond this for a brief moment.
-     * </p>
-     */
-    private final int capacity;
-
-    ThreadLocalRecyclerFactory(int capacity) {
-        if (capacity < 1) {
-            throw new IllegalArgumentException("was expecting a `capacity` greater than 1, found: " + capacity);
-        }
-        this.capacity = capacity;
-    }
-
-    /**
-     * @return maximum number of objects retained per thread in recyclers created
-     */
-    int getCapacity() {
-        return capacity;
-    }
-
-    @Override
-    public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
-        requireNonNull(supplier, "supplier");
-        requireNonNull(cleaner, "cleaner");
-        return new ThreadLocalRecycler<>(supplier, cleaner, capacity);
-    }
-
-    // Visible for testing
-    static class ThreadLocalRecycler<V> extends AbstractRecycler<V> {
-
-        private final Consumer<V> cleaner;
-
-        private final ThreadLocal<Queue<V>> queueRef;
-
-        private ThreadLocalRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final int capacity) {
-            super(supplier);
-            this.queueRef = ThreadLocal.withInitial(() -> new ArrayQueue<>(capacity));
-            this.cleaner = cleaner;
-        }
-
-        @Override
-        public V acquire() {
-            final Queue<V> queue = queueRef.get();
-            final V value = queue.poll();
-            return value != null ? value : createInstance();
-        }
-
-        @Override
-        public void release(final V value) {
-            requireNonNull(value, "value");
-            cleaner.accept(value);
-            final Queue<V> queue = queueRef.get();
-            queue.offer(value);
-        }
-
-        // Visible for testing
-        Queue<V> getQueue() {
-            return queueRef.get();
-        }
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ArrayQueue.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ArrayQueue.java
similarity index 98%
rename from log4j-api/src/main/java/org/apache/logging/log4j/internal/ArrayQueue.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ArrayQueue.java
index 238efa8796..90c573b364 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ArrayQueue.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ArrayQueue.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.internal;
+package org.apache.logging.log4j.internal.recycler;
 
 import java.util.AbstractQueue;
 import java.util.Iterator;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/DummyRecyclerFactoryProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/DummyRecyclerFactoryProvider.java
new file mode 100644
index 0000000000..f0a531b657
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/DummyRecyclerFactoryProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import static java.util.Objects.requireNonNull;
+
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.spi.recycler.AbstractRecycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+/**
+ * A {@link Recycler} factory provider such that the recycler does not recycle anything; all instances are freshly created.
+ *
+ * @since 3.0.0
+ */
+@ServiceProvider(RecyclerFactoryProvider.class)
+public final class DummyRecyclerFactoryProvider implements RecyclerFactoryProvider {
+
+    @Override
+    public int getOrder() {
+        return 900;
+    }
+
+    @Override
+    public String getName() {
+        return "dummy";
+    }
+
+    @Override
+    public RecyclerFactory createForEnvironment(final PropertyEnvironment environment) {
+        return DummyRecyclerFactory.INSTANCE;
+    }
+
+    // Visible for testing
+    static final class DummyRecyclerFactory implements RecyclerFactory {
+
+        private static final DummyRecyclerFactory INSTANCE = new DummyRecyclerFactory();
+
+        private DummyRecyclerFactory() {}
+
+        @Override
+        public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
+            requireNonNull(supplier, "supplier");
+            return new DummyRecycler<>(supplier);
+        }
+
+        private static final class DummyRecycler<V> extends AbstractRecycler<V> {
+
+            private DummyRecycler(final Supplier<V> supplier) {
+                super(supplier);
+            }
+
+            @Override
+            public V acquire() {
+                return createInstance();
+            }
+
+            @Override
+            public void release(final V value) {}
+        }
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/QueueingRecyclerFactoryProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/QueueingRecyclerFactoryProvider.java
new file mode 100644
index 0000000000..655e9e4a9e
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/QueueingRecyclerFactoryProvider.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.logging.log4j.spi.recycler.Recycler.DEFAULT_CAPACITY;
+
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.spi.recycler.AbstractRecycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+/**
+ * A {@link Recycler} factory provider such that the recycler pools objects in a fixed-size queue.
+ */
+@ServiceProvider(RecyclerFactoryProvider.class)
+public final class QueueingRecyclerFactoryProvider implements RecyclerFactoryProvider {
+
+    @Override
+    public int getOrder() {
+        return 800;
+    }
+
+    @Override
+    public String getName() {
+        return "queue";
+    }
+
+    @Override
+    public RecyclerFactory createForEnvironment(final PropertyEnvironment environment) {
+        requireNonNull(environment, "environment");
+        final int capacity = environment.getIntegerProperty(LoggingSystemProperty.RECYCLER_CAPACITY, DEFAULT_CAPACITY);
+        if (capacity < 1) {
+            throw new IllegalArgumentException("was expecting a `capacity` greater than 1, found: " + capacity);
+        }
+        return new QueueingRecyclerFactory(capacity);
+    }
+
+    // Visible for testing
+    static final class QueueingRecyclerFactory implements RecyclerFactory {
+
+        // Visible for testing
+        final int capacity;
+
+        private QueueingRecyclerFactory(int capacity) {
+            this.capacity = capacity;
+        }
+
+        @Override
+        public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
+            requireNonNull(supplier, "supplier");
+            requireNonNull(cleaner, "cleaner");
+            final Queue<V> queue = new ArrayBlockingQueue<>(capacity);
+            return new QueueingRecycler<>(supplier, cleaner, queue);
+        }
+
+        // Visible for testing
+        static final class QueueingRecycler<V> extends AbstractRecycler<V> {
+
+            private final Consumer<V> cleaner;
+
+            // Visible for testing
+            final Queue<V> queue;
+
+            private QueueingRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final Queue<V> queue) {
+                super(supplier);
+                this.cleaner = cleaner;
+                this.queue = queue;
+            }
+
+            @Override
+            public V acquire() {
+                final V value = queue.poll();
+                return value != null ? value : createInstance();
+            }
+
+            @Override
+            public void release(final V value) {
+                requireNonNull(value, "value");
+                cleaner.accept(value);
+                queue.offer(value);
+            }
+        }
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProvider.java
new file mode 100644
index 0000000000..6cdbba0863
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProvider.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.logging.log4j.spi.recycler.Recycler.DEFAULT_CAPACITY;
+
+import aQute.bnd.annotation.spi.ServiceProvider;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Queue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.spi.recycler.AbstractRecycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+/**
+ * A {@link Recycler} factory provider such that the recycler pools objects in a fixed-size queue stored in a {@link ThreadLocal}.
+ * <p>
+ * This strategy may not be appropriate in workloads where units of work are independent of operating system threads such as reactive streams, coroutines, or virtual threads.
+ * For such use cases, see {@link QueueingRecyclerFactoryProvider}.
+ * </p>
+ *
+ * @since 3.0.0
+ */
+@ServiceProvider(RecyclerFactoryProvider.class)
+public final class ThreadLocalRecyclerFactoryProvider implements RecyclerFactoryProvider {
+
+    @Override
+    public int getOrder() {
+        return 700;
+    }
+
+    @Override
+    public String getName() {
+        return "threadLocal";
+    }
+
+    @Nullable
+    @Override
+    public RecyclerFactory createForEnvironment(PropertyEnvironment environment) {
+        requireNonNull(environment, "environment");
+        final boolean threadLocalEnabled =
+                environment.getBooleanProperty(LoggingSystemProperty.THREAD_LOCALS_ENABLE, true);
+        if (!threadLocalEnabled) {
+            return null;
+        }
+        final int capacity = environment.getIntegerProperty(LoggingSystemProperty.RECYCLER_CAPACITY, DEFAULT_CAPACITY);
+        if (capacity < 1) {
+            throw new IllegalArgumentException("was expecting a `capacity` greater than 1, found: " + capacity);
+        }
+        return new ThreadLocalRecyclerFactory(capacity);
+    }
+
+    // Visible for testing
+    static final class ThreadLocalRecyclerFactory implements RecyclerFactory {
+
+        /**
+         * Maximum number of objects retained per thread.
+         * <p>
+         * This allows to acquire objects in recursive method calls and maintain minimal overhead in the scenarios where the active instance count goes far beyond this for a brief moment.
+         * </p>
+         */
+        // Visible for testing
+        final int capacity;
+
+        private ThreadLocalRecyclerFactory(int capacity) {
+            this.capacity = capacity;
+        }
+
+        @Override
+        public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
+            requireNonNull(supplier, "supplier");
+            requireNonNull(cleaner, "cleaner");
+            return new ThreadLocalRecycler<>(supplier, cleaner, capacity);
+        }
+
+        // Visible for testing
+        static final class ThreadLocalRecycler<V> extends AbstractRecycler<V> {
+
+            private final Consumer<V> cleaner;
+
+            // Visible for testing
+            final ThreadLocal<Queue<V>> queueRef;
+
+            private ThreadLocalRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final int capacity) {
+                super(supplier);
+                this.queueRef = ThreadLocal.withInitial(() -> new ArrayQueue<>(capacity));
+                this.cleaner = cleaner;
+            }
+
+            @Override
+            public V acquire() {
+                final Queue<V> queue = queueRef.get();
+                final V value = queue.poll();
+                return value != null ? value : createInstance();
+            }
+
+            @Override
+            public void release(final V value) {
+                requireNonNull(value, "value");
+                cleaner.accept(value);
+                final Queue<V> queue = queueRef.get();
+                queue.offer(value);
+            }
+        }
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory.java
index 50f457140e..21176cd0c5 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory.java
@@ -16,7 +16,7 @@
  */
 package org.apache.logging.log4j.message;
 
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 
 /**
  * Creates messages. Implementations can provide different message format syntaxes.
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
index f980631224..aae12e6f37 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
@@ -19,7 +19,7 @@ package org.apache.logging.log4j.message;
 import java.util.Arrays;
 import java.util.Objects;
 import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.StringBuilderFormattable;
 import org.apache.logging.log4j.util.StringBuilders;
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableMessageFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableMessageFactory.java
index a2523c2ae4..7abd8ac064 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableMessageFactory.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableMessageFactory.java
@@ -17,8 +17,8 @@
 package org.apache.logging.log4j.message;
 
 import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.PerformanceSensitive;
 
 /**
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableParameterizedMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableParameterizedMessage.java
index 326da8dabe..928e201f1b 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableParameterizedMessage.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableParameterizedMessage.java
@@ -18,8 +18,8 @@ package org.apache.logging.log4j.message;
 
 import java.util.Arrays;
 import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.Constants;
 import org.apache.logging.log4j.util.PerformanceSensitive;
 import org.apache.logging.log4j.util.StringBuilders;
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 2b5606714c..5dd788c06b 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
@@ -27,6 +27,8 @@ 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.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.Cast;
 import org.apache.logging.log4j.util.LambdaUtil;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DummyRecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DummyRecyclerFactory.java
deleted file mode 100644
index 0076cd49e6..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DummyRecyclerFactory.java
+++ /dev/null
@@ -1,59 +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.spi;
-
-import static java.util.Objects.requireNonNull;
-
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-/**
- * Recycler strategy which doesn't recycle anything; all instances are freshly created.
- *
- * @since 3.0.0
- */
-public final class DummyRecyclerFactory implements RecyclerFactory {
-
-    private static final DummyRecyclerFactory INSTANCE = new DummyRecyclerFactory();
-
-    private DummyRecyclerFactory() {}
-
-    public static DummyRecyclerFactory getInstance() {
-        return INSTANCE;
-    }
-
-    @Override
-    public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
-        requireNonNull(supplier, "supplier");
-        return new DummyRecycler<>(supplier);
-    }
-
-    private static class DummyRecycler<V> extends AbstractRecycler<V> {
-
-        private DummyRecycler(final Supplier<V> supplier) {
-            super(supplier);
-        }
-
-        @Override
-        public V acquire() {
-            return createInstance();
-        }
-
-        @Override
-        public void release(final V value) {}
-    }
-}
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 08daa2e05e..46cf2ae91f 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
@@ -41,13 +41,14 @@ import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.internal.RecyclerFactories;
 import org.apache.logging.log4j.message.DefaultFlowMessageFactory;
 import org.apache.logging.log4j.message.FlowMessageFactory;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.message.ParameterizedMessageFactory;
 import org.apache.logging.log4j.message.ReusableMessageFactory;
 import org.apache.logging.log4j.simple.SimpleLoggerContextFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryRegistry;
 import org.apache.logging.log4j.util.Constants;
 import org.apache.logging.log4j.util.Lazy;
 import org.apache.logging.log4j.util.LoaderUtil;
@@ -105,7 +106,8 @@ public class LoggingSystem {
             environmentLazy.map(environment -> () -> getProvider().createContextMap(environment));
     private final Lazy<Supplier<ThreadContextStack>> threadContextStackFactoryLazy =
             environmentLazy.map(environment -> () -> getProvider().createContextStack(environment));
-    private final Lazy<RecyclerFactory> recyclerFactoryLazy = Lazy.lazy(() -> RecyclerFactories.INSTANCE);
+    private final Lazy<RecyclerFactory> recyclerFactoryLazy =
+            environmentLazy.map(RecyclerFactoryRegistry::findRecyclerFactory);
 
     public LoggingSystem() {}
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystemProperty.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystemProperty.java
index b9d814cb15..63183ae763 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystemProperty.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystemProperty.java
@@ -190,7 +190,15 @@ public enum LoggingSystemProperty implements PropertyKey {
      * (from either {@code javax} or {@code jakarta}) is checked to see if this is a webapp.
      */
     // Web.isWebApp : calculate | true | false
-    IS_WEBAPP(PropertyComponent.WEB, Constant.IS_WEB_APP);
+    IS_WEBAPP(PropertyComponent.WEB, Constant.IS_WEB_APP),
+    /**
+     * Property to override the default recycler factory.
+     */
+    RECYCLER_FACTORY(PropertyComponent.RECYCLER, Constant.RECYCLER_FACTORY),
+    /**
+     * Property to override the default recycler capacity.
+     */
+    RECYCLER_CAPACITY(PropertyComponent.RECYCLER, Constant.RECYCLER_CAPACITY);
 
     public static final String SIMPLE_LOGGER_LOG_LEVEL = "SimpleLogger.%s.level";
     public static final String SYSTEM_PROPERTY_PREFIX = "log4j2.*.";
@@ -476,5 +484,8 @@ public enum LoggingSystemProperty implements PropertyKey {
         static final String IS_WEB_APP = "isWebApp";
         public static final String WEB =
                 LoggingSystemProperty.SYSTEM_PROPERTY_PREFIX + PropertyComponent.Constant.WEB + DELIM + IS_WEB_APP;
+
+        static final String RECYCLER_FACTORY = "factory";
+        static final String RECYCLER_CAPACITY = "capacity";
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/PropertyComponent.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/PropertyComponent.java
index 8a748bc84a..5dd99ef77a 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/PropertyComponent.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/PropertyComponent.java
@@ -35,6 +35,7 @@ public enum PropertyComponent {
     LOGGER(Constant.LOGGER),
     LOGGER_CONTEXT(Constant.LOGGER_CONTEXT),
     MESSAGE(Constant.MESSAGE),
+    RECYCLER(Constant.RECYCLER),
     SCRIPT(Constant.SCRIPT),
     SIMPLE_LOGGER(Constant.SIMPLE_LOGGER),
     STATUS_LOGGER(Constant.STATUS_LOGGER),
@@ -74,6 +75,7 @@ public enum PropertyComponent {
         public static final String LOGGER = "Logger";
         public static final String LOGGER_CONTEXT = "LoggerContext";
         public static final String MESSAGE = "Message";
+        public static final String RECYCLER = "Recycler";
         public static final String SCRIPT = "Script";
         public static final String SIMPLE_LOGGER = "SimpleLogger";
         public static final String STATUS_LOGGER = "StatusLogger";
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/QueueFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/QueueFactory.java
deleted file mode 100644
index 6c41f637fd..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/QueueFactory.java
+++ /dev/null
@@ -1,32 +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.spi;
-
-import java.util.Queue;
-import org.apache.logging.log4j.internal.QueueFactories;
-
-/**
- * A {@link Queue} factory contract.
- *
- * @see QueueFactories
- * @since 3.0.0
- */
-@FunctionalInterface
-public interface QueueFactory {
-
-    <E> Queue<E> create(final int capacity);
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerFactory.java
deleted file mode 100644
index 96c9f010e5..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerFactory.java
+++ /dev/null
@@ -1,58 +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.spi;
-
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-/**
- * Factory for {@link Recycler} strategies. Depending on workloads, different instance recycling strategies may be
- * most performant. For example, traditional multithreaded workloads may benefit from using thread-local instance
- * recycling while different models of concurrency or versions of the JVM may benefit from using an object pooling
- * strategy instead.
- *
- * @since 3.0.0
- */
-@FunctionalInterface
-public interface RecyclerFactory {
-
-    /**
-     * Creates a new recycler using the given supplier function for initial instances. These instances have
-     * no cleaner function and are assumed to always be reusable.
-     *
-     * @param supplier function to provide new instances of a recyclable object
-     * @param <V> the recyclable type
-     * @return a new recycler for V-type instances
-     */
-    default <V> Recycler<V> create(final Supplier<V> supplier) {
-        return create(supplier, ignored -> {});
-    }
-
-    /**
-     * Creates a new recycler using the given functions for providing fresh instances and for cleaning up
-     * existing instances for reuse. For example, a StringBuilder recycler would provide two functions:
-     * a supplier function for constructing a new StringBuilder with a preselected initial capacity and
-     * another function for trimming the StringBuilder to some preselected maximum capacity and setting
-     * its length back to 0 as if it were a fresh StringBuilder.
-     *
-     * @param supplier function to provide new instances of a recyclable object
-     * @param cleaner function to reset a recyclable object to a fresh state
-     * @param <V> the recyclable type
-     * @return a new recycler for V-type instances
-     */
-    <V> Recycler<V> create(Supplier<V> supplier, Consumer<V> cleaner);
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractRecycler.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/AbstractRecycler.java
similarity index 80%
rename from log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractRecycler.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/AbstractRecycler.java
index 6b74698a9e..84b5924f25 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractRecycler.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/AbstractRecycler.java
@@ -14,7 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.spi;
+package org.apache.logging.log4j.spi.recycler;
+
+import static java.util.Objects.requireNonNull;
 
 import java.util.function.Supplier;
 
@@ -22,15 +24,15 @@ public abstract class AbstractRecycler<V> implements Recycler<V> {
 
     private final Supplier<V> supplier;
 
-    public AbstractRecycler(final Supplier<V> supplier) {
-        this.supplier = supplier;
+    protected AbstractRecycler(final Supplier<V> supplier) {
+        this.supplier = requireNonNull(supplier, "supplier");
     }
 
     protected final V createInstance() {
         final V instance = supplier.get();
         if (instance instanceof RecyclerAware) {
             @SuppressWarnings("unchecked")
-            RecyclerAware<V> recyclerAware = (RecyclerAware<V>) instance;
+            final RecyclerAware<V> recyclerAware = (RecyclerAware<V>) instance;
             recyclerAware.setRecycler(this);
         }
         return instance;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Recycler.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/Recycler.java
similarity index 77%
rename from log4j-api/src/main/java/org/apache/logging/log4j/spi/Recycler.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/Recycler.java
index 50cd53a471..4a9db5b0b0 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Recycler.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/Recycler.java
@@ -14,16 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.spi;
+package org.apache.logging.log4j.spi.recycler;
 
 /**
- * Strategy for recycling objects. This is primarily useful for heavyweight objects and buffers.
+ * Contract for recycling strategies.
+ * This is the primary building block for logging components striving for garbage-free operation.
  *
  * @param <V> the recyclable type
  * @since 3.0.0
  */
 public interface Recycler<V> {
 
+    /**
+     * The default recycler capacity: {@code max(2C+1, 8)}, {@code C} denoting the number of available processors
+     */
+    int DEFAULT_CAPACITY = Math.max(2 * Runtime.getRuntime().availableProcessors() + 1, 8);
+
     /**
      * Acquires an instance of V. This may either be a fresh instance of V or a recycled instance of V.
      * Recycled instances will be modified by their cleanup function before being returned.
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerAware.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerAware.java
similarity index 95%
rename from log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerAware.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerAware.java
index 42e4d72163..4483ed3a02 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerAware.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerAware.java
@@ -14,7 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.spi;
+package org.apache.logging.log4j.spi.recycler;
+
 /**
  * Interface implemented by classes that need to interact with the {@link Recycler} that created them.
  */
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactory.java
new file mode 100644
index 0000000000..bb935b1958
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactory.java
@@ -0,0 +1,58 @@
+/*
+ * 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.recycler;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Contract for {@link Recycler} factories.
+ *
+ * @since 3.0.0
+ */
+public interface RecyclerFactory {
+
+    /**
+     * Creates a new recycler using the given supplier function for initial instances.
+     *
+     * @param supplier a function to provide initial instances
+     * @param <V> the recyclable type
+     * @return a new recycler
+     */
+    default <V> Recycler<V> create(final Supplier<V> supplier) {
+        return create(supplier, ignored -> {});
+    }
+
+    /**
+     * Creates a new recycler using the given supplier and cleaner functions.
+     * <p>
+     * The provided supplier needs to make sure that generated instances are always clean.
+     * </p>
+     * <p>
+     * Recycled instances are always guaranteed to be clean.
+     * The cleaning of an instance can take place either just before acquisition or prior to admitting it back into the reusable instances pool.
+     * The moment when the cleaning will be carried out is implementation dependent.
+     * Though a released instance should ideally be cleaned immediately to avoid keeping references to unused objects.
+     * </p>
+     *
+     * @param supplier a function to provide initial (and clean!) instances
+     * @param cleaner function to reset an instance before reuse
+     * @param <V> the recyclable type
+     * @return a new recycler
+     */
+    <V> Recycler<V> create(Supplier<V> supplier, Consumer<V> cleaner);
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryProvider.java
new file mode 100644
index 0000000000..d049e2d01e
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryProvider.java
@@ -0,0 +1,59 @@
+/*
+ * 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.recycler;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+/**
+ * Contract for providing {@link RecyclerFactory} instances.
+ *
+ * @since 3.0.0
+ */
+public interface RecyclerFactoryProvider {
+
+    /**
+     * Denotes the value to be used while sorting recycler factory providers to determine the precedence order.
+     * Values will be sorted naturally, that is, lower values will imply higher precedence.
+     *
+     * @return the value to be used while sorting
+     */
+    default int getOrder() {
+        return 100;
+    }
+
+    /**
+     * The name of this recycler factory provider.
+     * Recycler factory providers are required to have unique names.
+     *
+     * @return the name of this recycler factory provider
+     */
+    String getName();
+
+    /**
+     * Creates a recycler factory for the provided environment.
+     * <p>
+     * The return value can be null indicating that the recycler factory is not available for the provided environment.
+     * For instance, the provider of a {@link ThreadLocal}-based recycler factory can return null if the environment is of a web application.
+     * </p>
+     *
+     * @param environment an environment
+     * @return either a recycler factory instance, or null, if the associated recycler factory is not available for the given environment
+     */
+    @Nullable
+    RecyclerFactory createForEnvironment(PropertyEnvironment environment);
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryRegistry.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryRegistry.java
new file mode 100644
index 0000000000..ba17fc98e9
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryRegistry.java
@@ -0,0 +1,119 @@
+/*
+ * 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.recycler;
+
+import static java.util.Objects.requireNonNull;
+
+import aQute.bnd.annotation.spi.ServiceConsumer;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+import org.apache.logging.log4j.util.ServiceLoaderUtil;
+
+/**
+ * Registry of available {@link RecyclerFactoryProvider}s.
+ */
+@ServiceConsumer(RecyclerFactoryProvider.class)
+public final class RecyclerFactoryRegistry {
+
+    private static final Map<String, RecyclerFactoryProvider> PROVIDER_BY_NAME = loadProviders();
+
+    private static Map<String, RecyclerFactoryProvider> loadProviders() {
+        return ServiceLoaderUtil.safeStream(RecyclerFactoryProvider.class, null)
+                .sorted(Comparator.comparing(RecyclerFactoryProvider::getOrder))
+                .collect(Collectors.toMap(
+                        RecyclerFactoryProvider::getName,
+                        Function.identity(),
+                        (provider1, provider2) -> {
+                            final String message = String.format(
+                                    "recycler factory providers `%s` (order=%d) and `%s` (order=%d) have conflicting names: `%s`",
+                                    provider1.getClass().getCanonicalName(),
+                                    provider1.getOrder(),
+                                    provider2.getClass().getCanonicalName(),
+                                    provider2.getOrder(),
+                                    provider2.getName());
+                            throw new IllegalStateException(message);
+                        },
+                        LinkedHashMap::new));
+    }
+
+    private RecyclerFactoryRegistry() {}
+
+    public static Collection<RecyclerFactoryProvider> getRecyclerFactoryProviders() {
+        return PROVIDER_BY_NAME.values();
+    }
+
+    public static RecyclerFactory findRecyclerFactory(final PropertyEnvironment environment) {
+
+        // Check arguments
+        requireNonNull(environment, "environment");
+
+        // Short-circuit if there are no recycler factories available
+        if (PROVIDER_BY_NAME.isEmpty()) {
+            throw new IllegalStateException("couldn't find any recycler factories");
+        }
+
+        // If there is a particular recycler factory requested, get that done.
+        @Nullable final String name = environment.getStringProperty(LoggingSystemProperty.RECYCLER_FACTORY);
+        if (name != null) {
+            return findRecyclerFactoryByName(environment, name);
+        }
+
+        // Otherwise, find one available
+        return PROVIDER_BY_NAME.values().stream()
+                .sorted(Comparator.comparing(RecyclerFactoryProvider::getOrder))
+                .map(provider -> provider.createForEnvironment(environment))
+                .filter(Objects::nonNull)
+                .findFirst()
+                .orElseThrow(() -> {
+                    final String names = PROVIDER_BY_NAME.keySet().stream()
+                            .map(name_ -> '`' + name_ + '`')
+                            .collect(Collectors.joining(", "));
+                    final String message = String.format(
+                            "Couldn't find a recycler factory provider available for the provided environment. Names of the available recycler factory providers: %s",
+                            names);
+                    return new IllegalArgumentException(message);
+                });
+    }
+
+    private static RecyclerFactory findRecyclerFactoryByName(final PropertyEnvironment environment, final String name) {
+        @Nullable final RecyclerFactoryProvider provider = PROVIDER_BY_NAME.get(name);
+        if (provider == null) {
+            final String names = PROVIDER_BY_NAME.keySet().stream()
+                    .map(name_ -> '`' + name_ + '`')
+                    .collect(Collectors.joining(", "));
+            final String message = String.format(
+                    "Couldn't find a recycler factory provider of name `%s`. Names of the available recycler factory providers: %s",
+                    name, names);
+            throw new IllegalArgumentException(message);
+        }
+        @Nullable final RecyclerFactory factory = provider.createForEnvironment(environment);
+        if (factory == null) {
+            final String message = String.format(
+                    "failed to configure recycler factory of name `%s` for the provided environment", name);
+            throw new IllegalArgumentException(message);
+        }
+        return factory;
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/package-info.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/package-info.java
new file mode 100644
index 0000000000..094ac880cd
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+/**
+ * Internal interfaces and classes to be used by authors of logging implementations or for internal use by
+ * API classes.
+ */
+@Export
+@Version("2.20.1")
+package org.apache.logging.log4j.spi.recycler;
+
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
index d5134593ef..f198bbee3e 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
@@ -21,12 +21,12 @@ import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
 import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
-import org.apache.logging.log4j.internal.QueueFactories;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
 import org.apache.logging.log4j.simple.SimpleLogger;
@@ -63,7 +63,7 @@ public final class StatusLogger extends AbstractLogger {
 
     private final ReadWriteLock listenersLock = new ReentrantReadWriteLock();
 
-    private final Queue<StatusData> messages;
+    private final Queue<StatusData> messages = new ConcurrentLinkedQueue<>();
 
     private int listenersLevel;
 
@@ -97,7 +97,6 @@ public final class StatusLogger extends AbstractLogger {
         this.logger = logger;
         this.configuration = configuration;
         this.listenersLevel = configuration.getDefaultLevel().intLevel();
-        messages = QueueFactories.INSTANCE.create(configuration.getMaxEntries());
     }
 
     /**
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceLoaderUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceLoaderUtil.java
index b9be78fcd1..7fe952cbd0 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceLoaderUtil.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceLoaderUtil.java
@@ -16,6 +16,9 @@
  */
 package org.apache.logging.log4j.util;
 
+import static java.util.Objects.requireNonNull;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.ServiceConfigurationError;
@@ -36,7 +39,15 @@ public final class ServiceLoaderUtil {
 
     private ServiceLoaderUtil() {}
 
+    public static <S> Stream<S> safeStream(final Class<S> type, @Nullable final ClassLoader classLoader) {
+        requireNonNull(type, "type");
+        final ClassLoader effectiveLoader = classLoader != null ? classLoader : type.getClassLoader();
+        final ServiceLoader<S> serviceLoader = ServiceLoader.load(type, effectiveLoader);
+        return safeStream(serviceLoader);
+    }
+
     public static <S> Stream<S> safeStream(final ServiceLoader<S> serviceLoader) {
+        requireNonNull(serviceLoader, "serviceLoader");
         final Set<Class<?>> classes = new HashSet<>();
         return StreamSupport.stream(new ServiceLoaderSpliterator<>(serviceLoader), false)
                 // only the first occurrence of a class
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java
index d5a2c6f168..0d93461512 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java
@@ -39,7 +39,7 @@ import org.apache.logging.log4j.core.util.NetUtils;
 import org.apache.logging.log4j.core.util.WatchManager;
 import org.apache.logging.log4j.plugins.Node;
 import org.apache.logging.log4j.plugins.di.Key;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.PropertyEnvironment;
 
 /**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
index 408d0845d3..2a21b56b22 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
@@ -35,8 +35,8 @@ import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginAttribute;
 import org.apache.logging.log4j.plugins.PluginElement;
 import org.apache.logging.log4j.plugins.PluginFactory;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
 import org.apache.logging.log4j.util.PerformanceSensitive;
 import org.apache.logging.log4j.util.StringBuilders;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java
index d4ae74c892..7d9091071b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java
@@ -45,6 +45,7 @@ import org.apache.logging.log4j.plugins.SingletonFactory;
 import org.apache.logging.log4j.plugins.condition.ConditionalOnMissingBinding;
 import org.apache.logging.log4j.plugins.di.ConfigurableInstanceFactory;
 import org.apache.logging.log4j.spi.*;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 
 /**
  * Provides instance binding defaults.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/MutableLogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/MutableLogEvent.java
index 4e64b8c2cc..44e442eb52 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/MutableLogEvent.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/MutableLogEvent.java
@@ -34,7 +34,7 @@ import org.apache.logging.log4j.message.ParameterVisitable;
 import org.apache.logging.log4j.message.ReusableMessage;
 import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.logging.log4j.message.TimestampMessage;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.StackLocatorUtil;
 import org.apache.logging.log4j.util.StringBuilders;
 import org.apache.logging.log4j.util.StringMap;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
index 1ac3f5dd89..6f40131165 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
@@ -29,8 +29,8 @@ import org.apache.logging.log4j.core.time.Clock;
 import org.apache.logging.log4j.core.time.NanoClock;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.plugins.Inject;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 
 /**
  * Garbage-free LogEventFactory that recycles mutable {@link LogEvent} instances.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/AbstractStringLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/AbstractStringLayout.java
index dceca48d88..569e3873f0 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/AbstractStringLayout.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/AbstractStringLayout.java
@@ -24,8 +24,8 @@ import org.apache.logging.log4j.core.config.LoggerConfig;
 import org.apache.logging.log4j.core.impl.Log4jPropertyKey;
 import org.apache.logging.log4j.core.impl.LogEventFactory;
 import org.apache.logging.log4j.plugins.PluginElement;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.StringBuilders;
 
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
index f2fe28baa5..09354f7902 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
@@ -36,7 +36,7 @@ import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
 import org.apache.logging.log4j.plugins.PluginElement;
 import org.apache.logging.log4j.plugins.PluginFactory;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.apache.logging.log4j.util.Strings;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder.java
index e61083552d..360dd56e06 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder.java
@@ -23,7 +23,7 @@ import java.nio.charset.CharsetEncoder;
 import java.nio.charset.CodingErrorAction;
 import java.util.Objects;
 import org.apache.logging.log4j.core.util.Constants;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 
 /**
  * {@link Encoder} for {@link StringBuilder}s.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/JsonUtils.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/JsonUtils.java
index e2a3c74213..a60d6e4c9c 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/JsonUtils.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/JsonUtils.java
@@ -17,7 +17,7 @@
 package org.apache.logging.log4j.core.util;
 
 import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.Lazy;
 
 /**
diff --git a/log4j-api-queue-jctools/pom.xml b/log4j-jctools/pom.xml
similarity index 76%
rename from log4j-api-queue-jctools/pom.xml
rename to log4j-jctools/pom.xml
index 18dd10d906..a66acc8017 100644
--- a/log4j-api-queue-jctools/pom.xml
+++ b/log4j-jctools/pom.xml
@@ -26,9 +26,9 @@
     <relativePath>../log4j-parent</relativePath>
   </parent>
 
-  <artifactId>log4j-api-queue-jctools</artifactId>
-  <name>Apache Log4j API queue provider (JCTools)</name>
-  <description>Provides JCTools-based queue implementations for the Apache Log4j API.</description>
+  <artifactId>log4j-jctools</artifactId>
+  <name>Apache Log4j JCTools-based supplements</name>
+  <description>Provides JCTools-based data structure implementations for the Apache Log4j.</description>
 
   <properties>
 
@@ -37,7 +37,6 @@
     <!--
       ~ OSGi and JPMS options
       -->
-    <bnd-module-name>org.apache.logging.log4j.api.queue.jctools</bnd-module-name>
     <Fragment-Host>org.apache.logging.log4j.core</Fragment-Host>
 
   </properties>
@@ -46,7 +45,7 @@
 
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
-      <artifactId>log4j-api</artifactId>
+      <artifactId>log4j-core</artifactId>
     </dependency>
 
     <dependency>
@@ -54,6 +53,18 @@
       <artifactId>jctools-core</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
 
 </project>
diff --git a/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProvider.java b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProvider.java
new file mode 100644
index 0000000000..3a8fe11305
--- /dev/null
+++ b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProvider.java
@@ -0,0 +1,106 @@
+/*
+ * 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.jctools;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.logging.log4j.spi.recycler.Recycler.DEFAULT_CAPACITY;
+
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.Queue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.spi.recycler.AbstractRecycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+import org.jctools.queues.MpmcArrayQueue;
+
+/**
+ * A multi-producer-multi-consumer, thread-safe {@link Recycler} factory provider implementation based on <a href="https://jctools.github.io/JCTools/">JCTools</a>.
+ */
+@ServiceProvider(RecyclerFactoryProvider.class)
+public final class JCToolsMpmcRecyclerFactoryProvider implements RecyclerFactoryProvider {
+
+    @Override
+    public int getOrder() {
+        return 600;
+    }
+
+    @Override
+    public String getName() {
+        return "jctools-mpmc";
+    }
+
+    @Override
+    public RecyclerFactory createForEnvironment(final PropertyEnvironment environment) {
+        requireNonNull(environment, "environment");
+        final int capacity = environment.getIntegerProperty(LoggingSystemProperty.RECYCLER_CAPACITY, DEFAULT_CAPACITY);
+        if (capacity < 1) {
+            throw new IllegalArgumentException("was expecting a `capacity` greater than 1, found: " + capacity);
+        }
+        return new JCToolsMpmcRecyclerFactory(capacity);
+    }
+
+    // Visible for testing
+    static final class JCToolsMpmcRecyclerFactory implements RecyclerFactory {
+
+        // Visible for testing
+        private final int capacity;
+
+        private JCToolsMpmcRecyclerFactory(final int capacity) {
+            this.capacity = capacity;
+        }
+
+        @Override
+        public <V> Recycler<V> create(Supplier<V> supplier, Consumer<V> cleaner) {
+            requireNonNull(supplier, "supplier");
+            requireNonNull(cleaner, "cleaner");
+            final MpmcArrayQueue<V> queue = new MpmcArrayQueue<>(capacity);
+            return new JCToolsMpmcRecycler<>(supplier, cleaner, queue);
+        }
+
+        // Visible for testing
+        static final class JCToolsMpmcRecycler<V> extends AbstractRecycler<V> {
+
+            private final Consumer<V> cleaner;
+
+            // Visible for testing
+            final Queue<V> queue;
+
+            private JCToolsMpmcRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final Queue<V> queue) {
+                super(supplier);
+                this.cleaner = cleaner;
+                this.queue = queue;
+            }
+
+            @Override
+            public V acquire() {
+                final V value = queue.poll();
+                return value != null ? value : createInstance();
+            }
+
+            @Override
+            public void release(final V value) {
+                requireNonNull(value, "value");
+                cleaner.accept(value);
+                queue.offer(value);
+            }
+        }
+    }
+}
diff --git a/log4j-api-queue-jctools/src/main/java/org/apache/logging/log4j/api/queue/JCToolsQueueFactory.java b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProviderTest.java
similarity index 50%
rename from log4j-api-queue-jctools/src/main/java/org/apache/logging/log4j/api/queue/JCToolsQueueFactory.java
rename to log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProviderTest.java
index 57a39e31f8..b936c14309 100644
--- a/log4j-api-queue-jctools/src/main/java/org/apache/logging/log4j/api/queue/JCToolsQueueFactory.java
+++ b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsMpmcRecyclerFactoryProviderTest.java
@@ -14,24 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.api.queue;
+package org.apache.logging.log4j.jctools;
 
-import aQute.bnd.annotation.spi.ServiceProvider;
-import java.util.Queue;
-import org.apache.logging.log4j.spi.QueueFactory;
-import org.jctools.queues.MpmcArrayQueue;
+import static org.assertj.core.api.Assertions.assertThat;
 
-/**
- * A multi-producer-multi-consumer, thread-safe {@link QueueFactory} implementation based on <a href="https://jctools.github.io/JCTools/">JCTools</a>.
- */
-@ServiceProvider(QueueFactory.class)
-public final class JCToolsQueueFactory implements QueueFactory {
+import java.util.Comparator;
+import java.util.List;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryRegistry;
+import org.junit.jupiter.api.Test;
+
+class JCToolsMpmcRecyclerFactoryProviderTest {
 
-    @Override
-    public <E> Queue<E> create(final int capacity) {
-        if (capacity < 1) {
-            throw new IllegalArgumentException("invalid capacity: " + capacity);
-        }
-        return new MpmcArrayQueue<>(capacity);
+    @Test
+    void verify_is_the_first() {
+        final List<Class<?>> providerClasses = RecyclerFactoryRegistry.getRecyclerFactoryProviders().stream()
+                .sorted(Comparator.comparing(RecyclerFactoryProvider::getOrder))
+                .<Class<?>>map(RecyclerFactoryProvider::getClass)
+                .toList();
+        assertThat(providerClasses).startsWith(JCToolsMpmcRecyclerFactoryProvider.class);
     }
 }
diff --git a/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider b/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider
new file mode 100644
index 0000000000..36741a8bb5
--- /dev/null
+++ b/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider
@@ -0,0 +1,6 @@
+# `META-INF/services/*` files are automatically generated by `bnd-maven-plugin`.
+# We use `bnd:jar` goal, which only places them into the JAR.
+# As a result, tests of the same artifact (running against `target/` contents, not the JAR) don't have them.
+# To mitigate this, we manually create this file here only for tests.
+# `logging-parent` version `10.5.0` will switch from `bnd:jar` to `bnd:bnd-process`, then this hack won't be needed.
+org.apache.logging.log4j.jctools.JCToolsMpmcRecyclerFactoryProvider
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java
index 30a668b311..f8d92fd4d5 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java
@@ -34,7 +34,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 import org.apache.logging.log4j.layout.template.json.util.Uris;
 import org.apache.logging.log4j.plugins.*;
 import org.apache.logging.log4j.plugins.di.Key;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.Strings;
 
 @Configurable(elementType = Layout.ELEMENT_TYPE)
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java
index eebc2e9f2b..77a80af09c 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java
@@ -23,7 +23,7 @@ import java.util.concurrent.locks.LockSupport;
 import java.util.function.Consumer;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 
 /**
  * Resolves a number from an internal counter.
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java
index 27db91b743..0f7fc723cc 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java
@@ -21,7 +21,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.ParameterConsumer;
 import org.apache.logging.log4j.message.ParameterVisitable;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 
 /**
  * {@link Message} parameter (i.e., {@link Message#getParameters()}) resolver.
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
index f6da114660..64356bfe2e 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
@@ -22,8 +22,8 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.TriConsumer;
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
index 451c190e97..8d505eff8f 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
@@ -25,8 +25,8 @@ import java.util.stream.Collectors;
 import org.apache.logging.log4j.layout.template.json.util.CharSequencePointer;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 import org.apache.logging.log4j.layout.template.json.util.TruncatingBufferedPrintWriter;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 
 /**
  * Exception stack trace to JSON string resolver used by {@link ExceptionResolver}.
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java
index 403aeaa09d..58ad4360fd 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java
@@ -23,11 +23,9 @@ import java.util.Map;
 import java.util.UnknownFormatConversionException;
 import java.util.concurrent.ConcurrentHashMap;
 import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.internal.RecyclerFactories;
 import org.apache.logging.log4j.plugins.Inject;
 import org.apache.logging.log4j.plugins.Singleton;
 import org.apache.logging.log4j.plugins.util.TypeUtil;
-import org.apache.logging.log4j.spi.RecyclerFactory;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Cast;
 import org.apache.logging.log4j.util.EnglishEnums;
@@ -60,7 +58,6 @@ public class TypeConverterFactory {
         registerTypeAlias(Integer.class, Integer.TYPE);
         registerTypeConverter(Long.class, Long::valueOf);
         registerTypeAlias(Long.class, Long.TYPE);
-        registerTypeConverter(RecyclerFactory.class, RecyclerFactories::ofSpec);
         registerTypeConverter(Short.class, Short::valueOf);
         registerTypeAlias(Short.class, Short.TYPE);
         registerTypeConverter(String.class, s -> s);
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 64be7b447c..919cf2dee5 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
@@ -24,7 +24,7 @@ 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;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.slf4j.LoggerFactory;
 import org.slf4j.MarkerFactory;
 import org.slf4j.spi.LocationAwareLogger;
diff --git a/pom.xml b/pom.xml
index 8862927529..2344d4c60f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -234,7 +234,6 @@
          Note that modules here must have a corresponding entry in `dependencyManagement > dependencies` block below! -->
     <module>log4j-1.2-api</module>
     <module>log4j-api</module>
-    <module>log4j-api-queue-jctools</module>
     <module>log4j-api-test</module>
     <module>log4j-appserver</module>
     <module>log4j-core</module>
@@ -246,6 +245,7 @@
     <module>log4j-gctests</module>
     <module>log4j-iostreams</module>
     <module>log4j-jcl</module>
+    <module>log4j-jctools</module>
     <module>log4j-jdbc</module>
     <module>log4j-jdbc-dbcp2</module>
     <module>log4j-jndi</module>


(logging-log4j2) 01/04: Remove the need for an SPSC queue

Posted by vy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch api-queue
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 8954e0770a7824e6199d515dcb63db11c040dcd3
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Tue Dec 12 11:48:15 2023 +0100

    Remove the need for an SPSC queue
---
 .../logging/log4j/internal/ArrayQueueTest.java     | 101 +++++++++++++++++++++
 .../apache/logging/log4j/internal/ArrayQueue.java  | 100 ++++++++++++++++++++
 .../logging/log4j/internal/QueueFactories.java     |   6 --
 .../log4j/internal/ThreadLocalRecyclerFactory.java |  11 ++-
 4 files changed, 207 insertions(+), 11 deletions(-)

diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ArrayQueueTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ArrayQueueTest.java
new file mode 100644
index 0000000000..e063e7f81c
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ArrayQueueTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Queue;
+import java.util.Random;
+import java.util.concurrent.ArrayBlockingQueue;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class ArrayQueueTest {
+
+    @ParameterizedTest
+    @ValueSource(ints = {-1, 0})
+    void invalid_capacity_should_not_be_allowed(final int invalidCapacity) {
+        assertThatThrownBy(() -> new ArrayQueue<>(invalidCapacity))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("invalid capacity: " + invalidCapacity);
+    }
+
+    @Test
+    void should_work_with_capacity_1() {
+
+        // Verify initials
+        final Queue<String> queue = new ArrayQueue<>(1);
+        assertThat(queue.size()).isEqualTo(0);
+        assertThat(queue.peek()).isNull();
+        assertThat(queue.poll()).isNull();
+        assertThat(queue).isEmpty();
+
+        // Verify enqueue & deque
+        assertThat(queue.offer("foo")).isTrue();
+        assertThat(queue.offer("bar")).isFalse();
+        assertThat(queue.size()).isEqualTo(1);
+        assertThat(queue).containsOnly("foo");
+        assertThat(queue.peek()).isEqualTo("foo");
+        assertThat(queue.poll()).isEqualTo("foo");
+
+        // Verify final state
+        assertThat(queue.size()).isEqualTo(0);
+        assertThat(queue.peek()).isNull();
+        assertThat(queue.poll()).isNull();
+        assertThat(queue).isEmpty();
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+        "1,0.3", "1,0.5", "1,0.8", "2,0.3", "2,0.5", "2,0.8", "3,0.3", "3,0.5", "3,0.8", "4,0.3", "4,0.5", "4,0.8"
+    })
+    void ops_should_match_with_std_lib(final int capacity, final double pollRatio) {
+
+        // Set the stage
+        final Random random = new Random(0);
+        final int opCount = random.nextInt(100);
+        final Queue<String> queueRef = new ArrayBlockingQueue<>(capacity);
+        final Queue<String> queueTarget = new ArrayQueue<>(capacity);
+
+        for (int opIndex = 0; opIndex < opCount; opIndex++) {
+
+            // Verify entry
+            assertThat(queueTarget.size()).isEqualTo(queueRef.size());
+            assertThat(queueTarget.peek()).isEqualTo(queueRef.peek());
+            assertThat(queueTarget).containsExactlyElementsOf(queueRef);
+
+            // Is this a `poll()`?
+            if (pollRatio >= random.nextDouble()) {
+                assertThat(queueTarget.poll()).isEqualTo(queueRef.poll());
+            }
+
+            // Then this is an `offer()`
+            else {
+                final String item = "op@" + opIndex;
+                assertThat(queueTarget.offer(item)).isEqualTo(queueRef.offer(item));
+            }
+
+            // Verify exit
+            assertThat(queueTarget.size()).isEqualTo(queueRef.size());
+            assertThat(queueTarget.peek()).isEqualTo(queueRef.peek());
+            assertThat(queueTarget).containsExactlyElementsOf(queueRef);
+        }
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ArrayQueue.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ArrayQueue.java
new file mode 100644
index 0000000000..238efa8796
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ArrayQueue.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal;
+
+import java.util.AbstractQueue;
+import java.util.Iterator;
+import java.util.stream.IntStream;
+import javax.annotation.concurrent.NotThreadSafe;
+import org.apache.logging.log4j.util.InternalApi;
+
+/**
+ * An array-backed, fixed-length, not-thread-safe {@link java.util.Queue} implementation.
+ *
+ * @param <E> the element type
+ */
+@InternalApi
+@NotThreadSafe
+final class ArrayQueue<E> extends AbstractQueue<E> {
+
+    private final E[] buffer;
+
+    private int head;
+
+    private int tail;
+
+    private int size;
+
+    @SuppressWarnings("unchecked")
+    ArrayQueue(final int capacity) {
+        if (capacity < 1) {
+            throw new IllegalArgumentException("invalid capacity: " + capacity);
+        }
+        buffer = (E[]) new Object[capacity];
+        head = 0;
+        tail = -1;
+        size = 0;
+    }
+
+    @Override
+    public Iterator<E> iterator() {
+        int[] i = {head};
+        return IntStream.range(0, size)
+                .mapToObj(ignored -> {
+                    final E item = buffer[i[0]];
+                    i[0] = (i[0] + 1) % buffer.length;
+                    return item;
+                })
+                .iterator();
+    }
+
+    @Override
+    public boolean offer(final E item) {
+        if (size == buffer.length) {
+            return false;
+        }
+        tail = (tail + 1) % buffer.length;
+        buffer[tail] = item;
+        size++;
+        return true;
+    }
+
+    @Override
+    public E poll() {
+        if (isEmpty()) {
+            return null;
+        }
+        final E item = buffer[head];
+        buffer[head] = null; // Clear refs for GC
+        head = (head + 1) % buffer.length;
+        size--;
+        return item;
+    }
+
+    @Override
+    public E peek() {
+        if (isEmpty()) {
+            return null;
+        }
+        return buffer[head];
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
index 3db9e5d6e9..3d0313057b 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
@@ -26,7 +26,6 @@ import org.apache.logging.log4j.util.Cast;
 import org.apache.logging.log4j.util.InternalApi;
 import org.apache.logging.log4j.util.LoaderUtil;
 import org.jctools.queues.MpmcArrayQueue;
-import org.jctools.queues.SpscArrayQueue;
 
 /**
  * Provides {@link QueueFactory} instances for different use cases.
@@ -40,11 +39,6 @@ import org.jctools.queues.SpscArrayQueue;
 @InternalApi
 public enum QueueFactories implements QueueFactory {
 
-    /**
-     * Provides a bounded queue for single-producer/single-consumer usage.
-     */
-    SPSC(() -> SpscArrayQueue::new),
-
     /**
      * Provides a bounded queue for multi-producer/multi-consumer usage.
      */
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java
index 56e8930547..a05e9f3166 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java
@@ -70,17 +70,17 @@ final class ThreadLocalRecyclerFactory implements RecyclerFactory {
 
         private final Consumer<V> cleaner;
 
-        private final ThreadLocal<Queue<V>> holder;
+        private final ThreadLocal<Queue<V>> queueRef;
 
         private ThreadLocalRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final int capacity) {
             super(supplier);
-            this.holder = ThreadLocal.withInitial(() -> QueueFactories.SPSC.create(capacity));
+            this.queueRef = ThreadLocal.withInitial(() -> new ArrayQueue<>(capacity));
             this.cleaner = cleaner;
         }
 
         @Override
         public V acquire() {
-            final Queue<V> queue = holder.get();
+            final Queue<V> queue = queueRef.get();
             final V value = queue.poll();
             return value != null ? value : createInstance();
         }
@@ -89,12 +89,13 @@ final class ThreadLocalRecyclerFactory implements RecyclerFactory {
         public void release(final V value) {
             requireNonNull(value, "value");
             cleaner.accept(value);
-            holder.get().offer(value);
+            final Queue<V> queue = queueRef.get();
+            queue.offer(value);
         }
 
         // Visible for testing
         Queue<V> getQueue() {
-            return holder.get();
+            return queueRef.get();
         }
     }
 }