Support for Virtual Threads in Executor Services (#5648)

* Support for Virtual Threads in Executor Services

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
index 1a548dc..c430512 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
@@ -34,7 +34,6 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -88,6 +87,7 @@
 import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
 import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
 import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.innate.VirtualThreadUtil;
 import org.glassfish.jersey.message.internal.OutboundMessageContext;
 import org.glassfish.jersey.netty.connector.internal.NettyEntityWriter;
 
@@ -129,14 +129,15 @@
 
     NettyConnector(Client client) {
 
-        final Map<String, Object> properties = client.getConfiguration().getProperties();
+        final Configuration configuration = client.getConfiguration();
+        final Map<String, Object> properties = configuration.getProperties();
         final Object threadPoolSize = properties.get(ClientProperties.ASYNC_THREADPOOL_SIZE);
 
         if (threadPoolSize != null && threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) {
-            executorService = Executors.newFixedThreadPool((Integer) threadPoolSize);
+            executorService = VirtualThreadUtil.withConfig(configuration).newFixedThreadPool((Integer) threadPoolSize);
             this.group = new NioEventLoopGroup((Integer) threadPoolSize);
         } else {
-            executorService = Executors.newCachedThreadPool();
+            executorService = VirtualThreadUtil.withConfig(configuration).newCachedThreadPool();
             this.group = new NioEventLoopGroup();
         }
 
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java
index 4164e3a..a6d823c 100644
--- a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java
+++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServerFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -18,10 +18,15 @@
 
 import java.io.IOException;
 import java.net.URI;
+import java.util.concurrent.ThreadFactory;
 
 import javax.ws.rs.ProcessingException;
+import javax.ws.rs.core.Configuration;
 
 import org.glassfish.jersey.grizzly2.httpserver.internal.LocalizationMessages;
+import org.glassfish.jersey.innate.VirtualThreadSupport;
+import org.glassfish.jersey.innate.VirtualThreadUtil;
+import org.glassfish.jersey.innate.virtual.LoomishExecutors;
 import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder;
 import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler;
 import org.glassfish.jersey.server.ApplicationHandler;
@@ -281,11 +286,20 @@
                 : uri.getPort();
 
         final NetworkListener listener = new NetworkListener("grizzly", host, port);
+        final Configuration configuration = handler != null ? handler.getConfiguration().getConfiguration() : null;
 
-        listener.getTransport().getWorkerThreadPoolConfig().setThreadFactory(new ThreadFactoryBuilder()
+        final LoomishExecutors executors = VirtualThreadUtil.withConfig(configuration, false);
+        final ThreadFactory threadFactory = new ThreadFactoryBuilder()
                 .setNameFormat("grizzly-http-server-%d")
                 .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler())
-                .build());
+                .setThreadFactory(executors.getThreadFactory())
+                .build();
+
+        if (executors.isVirtual()) {
+            listener.getTransport().setWorkerThreadPool(executors.newCachedThreadPool());
+        } else {
+            listener.getTransport().getWorkerThreadPoolConfig().setThreadFactory(threadFactory);
+        }
 
         listener.setSecure(secure);
         if (sslEngineConfigurator != null) {
diff --git a/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/GrizzlyWebContainerFactory.java b/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/GrizzlyWebContainerFactory.java
index 1eb0bde..0fefd14 100644
--- a/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/GrizzlyWebContainerFactory.java
+++ b/containers/grizzly2-servlet/src/main/java/org/glassfish/jersey/grizzly2/servlet/GrizzlyWebContainerFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -23,6 +23,7 @@
 import javax.servlet.Servlet;
 
 import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
+import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.servlet.ServletContainer;
 import org.glassfish.jersey.uri.UriComponent;
 
@@ -251,11 +252,13 @@
             }
         }
 
+        ResourceConfig configuration = new ResourceConfig();
         if (initParams != null) {
             registration.setInitParameters(initParams);
+            configuration.addProperties((Map) initParams);
         }
 
-        HttpServer server = GrizzlyHttpServerFactory.createHttpServer(u);
+        HttpServer server = GrizzlyHttpServerFactory.createHttpServer(u, configuration);
         context.deploy(server);
         return server;
     }
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
index b0f7663..9927954 100644
--- a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
+++ b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainerFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -20,7 +20,10 @@
 import java.util.concurrent.ThreadFactory;
 
 import javax.ws.rs.ProcessingException;
+import javax.ws.rs.core.Configuration;
 
+import org.glassfish.jersey.innate.VirtualThreadUtil;
+import org.glassfish.jersey.innate.virtual.LoomishExecutors;
 import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder;
 import org.glassfish.jersey.jetty.internal.LocalizationMessages;
 import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler;
@@ -253,7 +256,8 @@
         }
         final int port = (uri.getPort() == -1) ? defaultPort : uri.getPort();
 
-        final Server server = new Server(new JettyConnectorThreadPool());
+        final Configuration configuration = handler != null ? handler.getConfiguration() : null;
+        final Server server = new Server(new JettyConnectorThreadPool(configuration));
         final HttpConfiguration config = new HttpConfiguration();
         if (sslContextFactory != null) {
             config.setSecureScheme("https");
@@ -291,10 +295,20 @@
     //
     //  Keeping this for backwards compatibility for the time being
     private static final class JettyConnectorThreadPool extends QueuedThreadPool {
-        private final ThreadFactory threadFactory = new ThreadFactoryBuilder()
-                .setNameFormat("jetty-http-server-%d")
-                .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler())
-                .build();
+        private final ThreadFactory threadFactory;
+
+        private JettyConnectorThreadPool(Configuration configuration) {
+            final LoomishExecutors executors = VirtualThreadUtil.withConfig(configuration, false);
+            if (executors.isVirtual()) {
+                super.setMaxThreads(Integer.MAX_VALUE - 1);
+            }
+
+            this.threadFactory = new ThreadFactoryBuilder()
+                    .setNameFormat("jetty-http-server-%d")
+                    .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler())
+                    .setThreadFactory(executors.getThreadFactory())
+                    .build();
+        }
 
         @Override
         public Thread newThread(Runnable runnable) {
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutorProvidersConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutorProvidersConfigurator.java
index df26b22..00b18ef 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutorProvidersConfigurator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientExecutorProvidersConfigurator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -32,11 +32,12 @@
 import org.glassfish.jersey.internal.util.collection.Value;
 import org.glassfish.jersey.internal.util.collection.Values;
 import org.glassfish.jersey.model.internal.ComponentBag;
-import org.glassfish.jersey.model.internal.ManagedObjectsFinalizer;
 import org.glassfish.jersey.process.internal.AbstractExecutorProvidersConfigurator;
 import org.glassfish.jersey.spi.ExecutorServiceProvider;
 import org.glassfish.jersey.spi.ScheduledExecutorServiceProvider;
 
+import javax.ws.rs.core.Configuration;
+
 /**
  * Configurator which initializes and register {@link ExecutorServiceProvider} and
  * {@link ScheduledExecutorServiceProvider}.
@@ -64,7 +65,8 @@
 
     @Override
     public void init(InjectionManager injectionManager, BootstrapBag bootstrapBag) {
-        Map<String, Object> runtimeProperties = bootstrapBag.getConfiguration().getProperties();
+        final Configuration configuration = bootstrapBag.getConfiguration();
+        Map<String, Object> runtimeProperties = configuration.getProperties();
 
         ExecutorServiceProvider defaultAsyncExecutorProvider;
         ScheduledExecutorServiceProvider defaultScheduledExecutorProvider;
@@ -94,12 +96,12 @@
                         .named("ClientAsyncThreadPoolSize");
                 injectionManager.register(asyncThreadPoolSizeBinding);
 
-                defaultAsyncExecutorProvider = new DefaultClientAsyncExecutorProvider(asyncThreadPoolSize);
+                defaultAsyncExecutorProvider = new DefaultClientAsyncExecutorProvider(asyncThreadPoolSize, configuration);
             } else {
                 if (MANAGED_EXECUTOR_SERVICE != null) {
                     defaultAsyncExecutorProvider = new ClientExecutorServiceProvider(MANAGED_EXECUTOR_SERVICE);
                 } else {
-                    defaultAsyncExecutorProvider = new DefaultClientAsyncExecutorProvider(0);
+                    defaultAsyncExecutorProvider = new DefaultClientAsyncExecutorProvider(0, configuration);
                 }
             }
         }
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientAsyncExecutorProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientAsyncExecutorProvider.java
index a875feb..e25e303 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientAsyncExecutorProvider.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/DefaultClientAsyncExecutorProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -20,6 +20,8 @@
 
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.core.Context;
 
 import org.glassfish.jersey.client.internal.LocalizationMessages;
 import org.glassfish.jersey.internal.util.collection.LazyValue;
@@ -46,8 +48,9 @@
      *                 See also {@link org.glassfish.jersey.client.ClientProperties#ASYNC_THREADPOOL_SIZE}.
      */
     @Inject
-    public DefaultClientAsyncExecutorProvider(@Named("ClientAsyncThreadPoolSize") final int poolSize) {
-        super("jersey-client-async-executor");
+    public DefaultClientAsyncExecutorProvider(@Named("ClientAsyncThreadPoolSize") final int poolSize,
+                                              @Context Configuration configuration) {
+        super("jersey-client-async-executor", configuration);
 
         this.asyncThreadPoolSize = Values.lazy(new Value<Integer>() {
             @Override
diff --git a/core-common/pom.xml b/core-common/pom.xml
index a2e68f5..11d3977 100644
--- a/core-common/pom.xml
+++ b/core-common/pom.xml
@@ -782,7 +782,7 @@
                                 <configuration>
                                     <target>
                                         <mkdir dir="${java21.build.outputDirectory}" />
-                                        <javac srcdir="${java21.sourceDirectory}" destdir="${java21.build.outputDirectory}"
+                                        <javac srcdir="${java21.sourceDirectory}${path.separator}${project.basedir}/src/main/java/org/glassfish/jersey/innate/virtual" destdir="${java21.build.outputDirectory}"
                                                classpath="${project.build.outputDirectory}" includeantruntime="false" release="21"/>
                                     </target>
                                 </configuration>
diff --git a/core-common/src/main/java/org/glassfish/jersey/CommonProperties.java b/core-common/src/main/java/org/glassfish/jersey/CommonProperties.java
index bc76ba6..0c5e824 100644
--- a/core-common/src/main/java/org/glassfish/jersey/CommonProperties.java
+++ b/core-common/src/main/java/org/glassfish/jersey/CommonProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -321,6 +321,30 @@
     public static final String PARAM_CONVERTERS_THROW_IAE = "jersey.config.paramconverters.throw.iae";
 
     /**
+     * <p>
+     *     Defines the {@link java.util.concurrent.ThreadFactory} to be used by internal default Executor Services.
+     * </p>
+     * <p>
+     *     The default is {@link  java.util.concurrent.Executors#defaultThreadFactory()} on platform threads and
+     *     {@code Thread.ofVirtual().factory()} on virtual threads.
+     * </p>
+     * @since 2.44
+     */
+    public static String THREAD_FACTORY = "jersey.config.threads.factory";
+
+    /**
+     * <p>
+     *     Defines whether the virtual threads should be used by Jersey on JDK 21+ when not using an exact number
+     *     of threads by {@code FixedThreadPool}.
+     * </p>
+     * <p>
+     *     The default is {@code false} for this version of Jersey, and {@code true} for Jersey 3.1+.
+     * </p>
+     * @since 2.44
+     */
+    public static String USE_VIRTUAL_THREADS = "jersey.config.threads.use.virtual";
+
+    /**
      * Prevent instantiation.
      */
     private CommonProperties() {
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.java b/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.java
new file mode 100644
index 0000000..8d3beea
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.innate;
+
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.innate.virtual.LoomishExecutors;
+
+import javax.ws.rs.core.Configuration;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Factory class to provide JDK specific implementation of bits related to the virtual thread support.
+ */
+public final class VirtualThreadUtil {
+
+    private static final boolean USE_VIRTUAL_THREADS_BY_DEFAULT = false;
+
+    /**
+     * Do not instantiate.
+     */
+    private VirtualThreadUtil() {
+        throw new IllegalStateException();
+    }
+
+    /**
+     * Return an instance of {@link LoomishExecutors} based on a configuration property.
+     * @param config the {@link Configuration}
+     * @return the {@link LoomishExecutors} instance.
+     */
+    public static LoomishExecutors withConfig(Configuration config) {
+        return withConfig(config, USE_VIRTUAL_THREADS_BY_DEFAULT);
+    }
+
+    /**
+     * Return an instance of {@link LoomishExecutors} based on a configuration property.
+     * @param config the {@link Configuration}
+     * @param useVirtualByDefault the default use if not said otherwise by property
+     * @return the {@link LoomishExecutors} instance.
+     */
+    public static LoomishExecutors withConfig(Configuration config, boolean useVirtualByDefault) {
+        ThreadFactory tfThreadFactory = null;
+        boolean useVirtualThreads = useVirtualThreads(config, useVirtualByDefault);
+
+        if (config != null) {
+            Object threadFactory = config.getProperty(CommonProperties.THREAD_FACTORY);
+            if (threadFactory != null && ThreadFactory.class.isInstance(threadFactory)) {
+                tfThreadFactory = (ThreadFactory) threadFactory;
+            }
+        }
+
+        return tfThreadFactory == null
+                ? VirtualThreadSupport.allowVirtual(useVirtualThreads)
+                : VirtualThreadSupport.allowVirtual(useVirtualThreads, tfThreadFactory);
+    }
+
+    /**
+     * Check configuration if the use of the virtual threads is expected or return the default value if not.
+     * @param config the {@link Configuration}
+     * @param useByDefault the default expectation
+     * @return the expected
+     */
+    private static boolean useVirtualThreads(Configuration config, boolean useByDefault) {
+        boolean bUseVirtualThreads = useByDefault;
+        if (config != null) {
+            Object useVirtualThread = config.getProperty(CommonProperties.USE_VIRTUAL_THREADS);
+            if (useVirtualThread != null && Boolean.class.isInstance(useVirtualThread)) {
+                bUseVirtualThreads = (boolean) useVirtualThread;
+            }
+            if (useVirtualThread != null && String.class.isInstance(useVirtualThread)) {
+                bUseVirtualThreads = Boolean.parseBoolean(useVirtualThread.toString());
+            }
+        }
+        return bUseVirtualThreads;
+    }
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/virtual/LoomishExecutors.java b/core-common/src/main/java/org/glassfish/jersey/innate/virtual/LoomishExecutors.java
new file mode 100644
index 0000000..9065746
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/virtual/LoomishExecutors.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.innate.virtual;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * {@link Executors} facade to support virtual threads.
+ */
+public interface LoomishExecutors {
+    /**
+     * Creates a thread pool that creates new threads as needed and uses virtual threads if available.
+     * @return the newly created thread pool
+     */
+    ExecutorService newCachedThreadPool();
+
+    /**
+     * Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue
+     * and uses virtual threads if available
+     * @param nThreads – the number of threads in the pool
+     * @return the newly created thread pool
+     */
+    ExecutorService newFixedThreadPool(int nThreads);
+
+    /**
+     * Returns thread factory used to create new threads
+     * @return thread factory used to create new threads
+     * @see Executors#defaultThreadFactory()
+     */
+    ThreadFactory getThreadFactory();
+
+    /**
+     * Return true if the virtual thread use is requested.
+     * @return whether the virtual thread use is requested.
+     */
+    boolean isVirtual();
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/virtual/package-info.java b/core-common/src/main/java/org/glassfish/jersey/innate/virtual/package-info.java
new file mode 100644
index 0000000..c56f91b
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/virtual/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Jersey innate packages. The innate packages will not be opened by JPMS outside of Jersey.
+ * Not for public use.
+ * This virtual package should contain only classes that do not have dependencies on Jersey, or the REST API to be buildable with
+ * ant for multi-release.
+ */
+package org.glassfish.jersey.innate.virtual;
diff --git a/core-common/src/main/java/org/glassfish/jersey/spi/AbstractThreadPoolProvider.java b/core-common/src/main/java/org/glassfish/jersey/spi/AbstractThreadPoolProvider.java
index 27cf449..ff1b757 100644
--- a/core-common/src/main/java/org/glassfish/jersey/spi/AbstractThreadPoolProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/spi/AbstractThreadPoolProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -29,6 +29,7 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import org.glassfish.jersey.innate.VirtualThreadUtil;
 import org.glassfish.jersey.internal.LocalizationMessages;
 import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder;
 import org.glassfish.jersey.internal.util.ExtendedLogger;
@@ -37,6 +38,8 @@
 import org.glassfish.jersey.internal.util.collection.Values;
 import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler;
 
+import javax.ws.rs.core.Configuration;
+
 /**
  * Abstract thread pool executor provider.
  * <p>
@@ -69,9 +72,7 @@
 
     private final String name;
     private final AtomicBoolean closed = new AtomicBoolean(false);
-    private final LazyValue<E> lazyExecutorServiceProvider =
-            Values.lazy((Value<E>) () -> createExecutor(getCorePoolSize(), createThreadFactory(), getRejectedExecutionHandler()));
-
+    private final LazyValue<E> lazyExecutorServiceProvider;
     /**
      * Inheritance constructor.
      *
@@ -79,7 +80,20 @@
      *             provided thread pool executor.
      */
     protected AbstractThreadPoolProvider(final String name) {
+        this(name, null);
+    }
+
+    /**
+     * Inheritance constructor.
+     *
+     * @param name name of the provided thread pool executor. Will be used in the names of threads created & used by the
+     *             provided thread pool executor.
+     * @param configuration {@link Configuration} properties.
+     */
+    protected AbstractThreadPoolProvider(final String name, Configuration configuration) {
         this.name = name;
+        lazyExecutorServiceProvider = Values.lazy((Value<E>) () ->
+                createExecutor(getCorePoolSize(), createThreadFactory(configuration), getRejectedExecutionHandler()));
     }
 
     /**
@@ -208,9 +222,10 @@
         return null;
     }
 
-    private ThreadFactory createThreadFactory() {
+    private ThreadFactory createThreadFactory(Configuration configuration) {
         final ThreadFactoryBuilder factoryBuilder = new ThreadFactoryBuilder()
                 .setNameFormat(name + "-%d")
+                .setThreadFactory(VirtualThreadUtil.withConfig(configuration).getThreadFactory())
                 .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler());
 
         final ThreadFactory backingThreadFactory = getBackingThreadFactory();
diff --git a/core-common/src/main/java/org/glassfish/jersey/spi/ScheduledThreadPoolExecutorProvider.java b/core-common/src/main/java/org/glassfish/jersey/spi/ScheduledThreadPoolExecutorProvider.java
index 486226c..c516aec 100644
--- a/core-common/src/main/java/org/glassfish/jersey/spi/ScheduledThreadPoolExecutorProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/spi/ScheduledThreadPoolExecutorProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -23,6 +23,7 @@
 import java.util.concurrent.ThreadFactory;
 
 import javax.annotation.PreDestroy;
+import javax.ws.rs.core.Configuration;
 
 /**
  * Default implementation of the Jersey {@link org.glassfish.jersey.spi.ScheduledExecutorServiceProvider
@@ -66,6 +67,17 @@
         super(name);
     }
 
+    /**
+     * Create a new instance of the scheduled thread pool executor provider.
+     *
+     * @param name provider name. The name will be used to name the threads created & used by the
+     *             provisioned scheduled thread pool executor.
+     * @@param configuration {@link Configuration} properties.
+     */
+    public ScheduledThreadPoolExecutorProvider(final String name, Configuration configuration) {
+        super(name, configuration);
+    }
+
     @Override
     public ScheduledExecutorService getExecutorService() {
         return super.getExecutor();
diff --git a/core-common/src/main/java/org/glassfish/jersey/spi/ThreadPoolExecutorProvider.java b/core-common/src/main/java/org/glassfish/jersey/spi/ThreadPoolExecutorProvider.java
index dbcec55..591983f 100644
--- a/core-common/src/main/java/org/glassfish/jersey/spi/ThreadPoolExecutorProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/spi/ThreadPoolExecutorProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -26,6 +26,7 @@
 import java.util.concurrent.TimeUnit;
 
 import javax.annotation.PreDestroy;
+import javax.ws.rs.core.Configuration;
 
 /**
  * Default implementation of the Jersey {@link org.glassfish.jersey.spi.ExecutorServiceProvider executor service provider SPI}.
@@ -61,6 +62,17 @@
         super(name);
     }
 
+    /**
+     * Create a new instance of the thread pool executor provider.
+     *
+     * @param name provider name. The name will be used to name the threads created & used by the
+     *             provisioned thread pool executor.
+     * @param configuration {@link Configuration} properties.
+     */
+    public ThreadPoolExecutorProvider(final String name, Configuration configuration) {
+        super(name, configuration);
+    }
+
     @Override
     public ExecutorService getExecutorService() {
         return super.getExecutor();
diff --git a/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java b/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java
index 90cafba..867a65b 100644
--- a/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java
+++ b/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java
@@ -16,11 +16,19 @@
 
 package org.glassfish.jersey.innate;
 
+import org.glassfish.jersey.innate.virtual.LoomishExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
 /**
  * Utility class for the virtual thread support.
  */
 public final class VirtualThreadSupport {
 
+    private static final LoomishExecutors NON_VIRTUAL = new NonLoomishExecutors(Executors.defaultThreadFactory());
+
     /**
      * Do not instantiate.
      */
@@ -35,4 +43,51 @@
     public static boolean isVirtualThread() {
         return false;
     }
+
+    /**
+     * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
+     * @param allow whether to allow virtual threads.
+     * @return the {@link LoomishExecutors} instance.
+     */
+    public static LoomishExecutors allowVirtual(boolean allow) {
+        return NON_VIRTUAL;
+    }
+
+    /**
+     * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
+     * @param allow whether to allow virtual threads.
+     * @param threadFactory the thread factory to be used by a the {@link ExecutorService}.
+     * @return the {@link LoomishExecutors} instance.
+     */
+    public static LoomishExecutors allowVirtual(boolean allow, ThreadFactory threadFactory) {
+        return new NonLoomishExecutors(threadFactory);
+    }
+
+    private static final class NonLoomishExecutors implements LoomishExecutors {
+        private final ThreadFactory threadFactory;
+
+        private NonLoomishExecutors(ThreadFactory threadFactory) {
+            this.threadFactory = threadFactory;
+        }
+
+        @Override
+        public ExecutorService newCachedThreadPool() {
+            return Executors.newCachedThreadPool();
+        }
+
+        @Override
+        public ExecutorService newFixedThreadPool(int nThreads) {
+            return Executors.newFixedThreadPool(nThreads);
+        }
+
+        @Override
+        public ThreadFactory getThreadFactory() {
+            return threadFactory;
+        }
+
+        @Override
+        public boolean isVirtual() {
+            return false;
+        }
+    }
 }
diff --git a/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java b/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java
index 74f58ba..0e7d695 100644
--- a/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java
+++ b/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java
@@ -16,11 +16,20 @@
 
 package org.glassfish.jersey.innate;
 
+import org.glassfish.jersey.innate.virtual.LoomishExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
 /**
  * Utility class for the virtual thread support.
  */
 public final class VirtualThreadSupport {
 
+    private static final LoomishExecutors VIRTUAL_THREADS = new Java21LoomishExecutors(Thread.ofVirtual().factory());
+    private static final LoomishExecutors NON_VIRTUAL_THREADS = new NonLoomishExecutors(Executors.defaultThreadFactory());
+
     /**
      * Do not instantiate.
      */
@@ -35,4 +44,80 @@
     public static boolean isVirtualThread() {
         return Thread.currentThread().isVirtual();
     }
+
+    /**
+     * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
+     * @param allow whether to allow virtual threads.
+     * @return the {@link LoomishExecutors} instance.
+     */
+    public static LoomishExecutors allowVirtual(boolean allow) {
+        return allow ? VIRTUAL_THREADS : NON_VIRTUAL_THREADS;
+    }
+
+    /**
+     * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
+     * @param allow whether to allow virtual threads.
+     * @param threadFactory the thread factory to be used by a the {@link ExecutorService}.
+     * @return the {@link LoomishExecutors} instance.
+     */
+    public static LoomishExecutors allowVirtual(boolean allow, ThreadFactory threadFactory) {
+        return allow ? new Java21LoomishExecutors(threadFactory) : new NonLoomishExecutors(threadFactory);
+    }
+
+    private static class NonLoomishExecutors implements LoomishExecutors {
+        private final ThreadFactory threadFactory;
+
+        private NonLoomishExecutors(ThreadFactory threadFactory) {
+            this.threadFactory = threadFactory;
+        }
+
+        @Override
+        public ExecutorService newCachedThreadPool() {
+            return Executors.newCachedThreadPool(getThreadFactory());
+        }
+
+        @Override
+        public ExecutorService newFixedThreadPool(int nThreads) {
+            return Executors.newFixedThreadPool(nThreads, getThreadFactory());
+        }
+
+        @Override
+        public ThreadFactory getThreadFactory() {
+            return threadFactory;
+        }
+
+        @Override
+        public boolean isVirtual() {
+            return false;
+        }
+    }
+
+    private static class Java21LoomishExecutors implements LoomishExecutors {
+        private final ThreadFactory threadFactory;
+
+        private Java21LoomishExecutors(ThreadFactory threadFactory) {
+            this.threadFactory = threadFactory;
+        }
+
+        @Override
+        public ExecutorService newCachedThreadPool() {
+            return Executors.newThreadPerTaskExecutor(getThreadFactory());
+        }
+
+        @Override
+        public ExecutorService newFixedThreadPool(int nThreads) {
+            ThreadFactory threadFactory = this == VIRTUAL_THREADS ? Executors.defaultThreadFactory() : getThreadFactory();
+            return Executors.newFixedThreadPool(nThreads, threadFactory);
+        }
+
+        @Override
+        public ThreadFactory getThreadFactory() {
+            return threadFactory;
+        }
+
+        @Override
+        public boolean isVirtual() {
+            return true;
+        }
+    }
 }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ServerExecutorProvidersConfigurator.java b/core-server/src/main/java/org/glassfish/jersey/server/ServerExecutorProvidersConfigurator.java
index 77974f1..ecbccfd 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/ServerExecutorProvidersConfigurator.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/ServerExecutorProvidersConfigurator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2017, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2024 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -21,13 +21,14 @@
 import org.glassfish.jersey.internal.inject.InjectionManager;
 import org.glassfish.jersey.internal.inject.InstanceBinding;
 import org.glassfish.jersey.model.internal.ComponentBag;
-import org.glassfish.jersey.model.internal.ManagedObjectsFinalizer;
 import org.glassfish.jersey.process.internal.AbstractExecutorProvidersConfigurator;
 import org.glassfish.jersey.spi.ExecutorServiceProvider;
 import org.glassfish.jersey.spi.ScheduledExecutorServiceProvider;
 import org.glassfish.jersey.spi.ScheduledThreadPoolExecutorProvider;
 import org.glassfish.jersey.spi.ThreadPoolExecutorProvider;
 
+import javax.ws.rs.core.Configuration;
+
 /**
  * Configurator which initializes and register {@link org.glassfish.jersey.spi.ExecutorServiceProvider} and
  * {@link org.glassfish.jersey.spi.ScheduledExecutorServiceProvider}.
@@ -43,7 +44,7 @@
         ComponentBag componentBag = runtimeConfig.getComponentBag();
 
         // TODO: Do we need to register DEFAULT Executor and ScheduledExecutor to InjectionManager?
-        ScheduledExecutorServiceProvider defaultScheduledExecutorProvider = new DefaultBackgroundSchedulerProvider();
+        ScheduledExecutorServiceProvider defaultScheduledExecutorProvider = new DefaultBackgroundSchedulerProvider(runtimeConfig);
         InstanceBinding<ScheduledExecutorServiceProvider> schedulerBinding = Bindings
                 .service(defaultScheduledExecutorProvider)
                 .to(ScheduledExecutorServiceProvider.class)
@@ -67,8 +68,8 @@
     @BackgroundScheduler
     private static class DefaultBackgroundSchedulerProvider extends ScheduledThreadPoolExecutorProvider {
 
-        public DefaultBackgroundSchedulerProvider() {
-            super("jersey-background-task-scheduler");
+        public DefaultBackgroundSchedulerProvider(Configuration configuration) {
+            super("jersey-background-task-scheduler", configuration);
         }
 
         @Override
diff --git a/docs/src/main/docbook/appendix-properties.xml b/docs/src/main/docbook/appendix-properties.xml
index 6f5036b..8d40af6 100644
--- a/docs/src/main/docbook/appendix-properties.xml
+++ b/docs/src/main/docbook/appendix-properties.xml
@@ -203,6 +203,39 @@
                         </entry>
                     </row>
                     <row>
+                        <entry>&jersey.common.CommonProperties.THREAD_FACTORY;(Jersey 2.44 or later)
+                        </entry>
+                        <entry>
+                            <literal>jersey.config.threads.factory</literal>
+                        </entry>
+                        <entry>
+                            <para>
+                                Defines the <literal>java.util.concurrent.ThreadFactory</literal> to be used by internal default
+                                <literal>ExecutorServices</literal>.
+                            </para>
+                            <para>
+                                The default is <literal>java.util.concurrent.Executors#defaultThreadFactory()</literal> on
+                                platform threads and<literal>Thread.ofVirtual().factory()</literal> on virtual threads.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
+                        <entry>&jersey.common.CommonProperties.USE_VIRTUAL_THREADS;(Jersey 2.44 or later)
+                        </entry>
+                        <entry>
+                            <literal>jersey.config.threads.use.virtual</literal>
+                        </entry>
+                        <entry>
+                            <para>
+                                Defines whether the virtual threads should be used by Jersey on JDK 21+ when not using an exact number
+                                of threads by <literal>FixedThreadPool</literal>.
+                            </para>
+                            <para>
+                                The default is &lit.false; for this version of Jersey, and &lit.true; for Jersey 3.1+.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
                         <entry>&jersey.logging.LoggingFeature.LOGGING_FEATURE_LOGGER_NAME;
                         </entry>
                         <entry>
diff --git a/docs/src/main/docbook/dependencies.xml b/docs/src/main/docbook/dependencies.xml
index b58ebd8..8124dfa 100644
--- a/docs/src/main/docbook/dependencies.xml
+++ b/docs/src/main/docbook/dependencies.xml
@@ -62,6 +62,21 @@
                 </listitem>
             </itemizedlist>
         </para>
+        <section>
+            <title>Virtual Threads and Thread Factories</title>
+            <para>
+                With JDK 21 and above, Jersey (since 2.44) has the ability to use virtual threads instead of
+                the <literal>CachedThreadPool</literal> in the internal <literal>ExecutorServices</literal>.
+                Jersey also has the ability to specify the backing <literal>ThreadFactory</literal> for the
+                default <literal>ExecutorServices</literal> (the default <literal>ExecutorServices</literal>
+                can be overridden by the &jersey.common.spi.ExecutorServiceProvider; SPI).
+            </para>
+            <para>
+                To enable virtual threads and/or specify the <literal>ThreadFactory</literal>, use
+                &jersey.common.CommonProperties.USE_VIRTUAL_THREADS; and/or &jersey.common.CommonProperties.THREAD_FACTORY;
+                properties, respectively. See also the <xref linkend="appendix-properties-common"/> in appendix for property details.
+            </para>
+        </section>
     </section>
     <section>
         <title>Introduction to Jersey dependencies</title>
diff --git a/docs/src/main/docbook/jersey.ent b/docs/src/main/docbook/jersey.ent
index ccbe021..8cf043f 100644
--- a/docs/src/main/docbook/jersey.ent
+++ b/docs/src/main/docbook/jersey.ent
@@ -408,6 +408,8 @@
 <!ENTITY jersey.common.CommonProperties.JSON_JACKSON_DISABLED_MODULES_CLIENT "<link xlink:href='&jersey.javadoc.uri.prefix;/CommonProperties.html#JSON_JACKSON_DISABLED_MODULES'>CommonProperties.JSON_JACKSON_DISABLED_MODULES_CLIENT</link>" >
 <!ENTITY jersey.common.CommonProperties.JSON_JACKSON_DISABLED_MODULES_SERVER "<link xlink:href='&jersey.javadoc.uri.prefix;/CommonProperties.html#JSON_JACKSON_DISABLED_MODULES'>CommonProperties.JSON_JACKSON_DISABLED_MODULES_SERVER</link>" >
 <!ENTITY jersey.common.CommonProperties.PARAM_CONVERTERS_THROW_IAE "<link xlink:href='&jersey.javadoc.uri.prefix;/CommonProperties.html#PARAM_CONVERTERS_THROW_IAE'>CommonProperties.PARAM_CONVERTERS_THROW_IAE</link>" >
+<!ENTITY jersey.common.CommonProperties.THREAD_FACTORY "<link xlink:href='&jersey.javadoc.uri.prefix;/CommonProperties.html#THREAD_FACTORY'>CommonProperties.THREAD_FACTORY</link>" >
+<!ENTITY jersey.common.CommonProperties.USE_VIRTUAL_THREADS "<link xlink:href='&jersey.javadoc.uri.prefix;/CommonProperties.html#USE_VIRTUAL_THREADS'>CommonProperties.USE_VIRTUAL_THREADS</link>" >
 <!ENTITY jersey.common.internal.inject.DisposableSupplier "<link xlink:href='&jersey.javadoc.uri.prefix;/internal/inject/DisposableSupplier.html'>DisposableSupplier</link>">
 <!ENTITY jersey.common.internal.inject.InjectionManager "<link xlink:href='&jersey.javadoc.uri.prefix;/internal/inject/InjectionManager.html'>InjectionManager</link>">
 <!ENTITY jersey.common.internal.inject.InjectionResolver "<link xlink:href='&jersey.javadoc.uri.prefix;/internal/inject/InjectionResolver.html'>InjectionResolver</link>">
diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java
index a4b4e31..43564d8 100644
--- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java
+++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/RestClientBuilderImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2024 Oracle and/or its affiliates. All rights reserved.
  * Copyright (c) 2019, 2021 Payara Foundation and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -33,7 +33,6 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 import java.util.logging.Level;
@@ -66,6 +65,7 @@
 import org.glassfish.jersey.client.Initializable;
 import org.glassfish.jersey.client.spi.ConnectorProvider;
 import org.glassfish.jersey.ext.cdi1x.internal.CdiUtil;
+import org.glassfish.jersey.innate.VirtualThreadUtil;
 import org.glassfish.jersey.internal.ServiceFinder;
 import org.glassfish.jersey.internal.inject.InjectionManager;
 import org.glassfish.jersey.internal.inject.InjectionManagerSupplier;
@@ -111,7 +111,7 @@
         asyncInterceptorFactories = new ArrayList<>();
         config = ConfigProvider.getConfig();
         configWrapper = new ConfigWrapper(clientBuilder.getConfiguration());
-        executorService = Executors::newCachedThreadPool;
+        executorService = () -> VirtualThreadUtil.withConfig(configWrapper).newCachedThreadPool();
     }
 
     @Override
diff --git a/tests/e2e-jdk-specifics/src/test/java/org/glassfish/jersey/tests/e2e/jdk21/ThreadFactoryUsageTest.java b/tests/e2e-jdk-specifics/src/test/java/org/glassfish/jersey/tests/e2e/jdk21/ThreadFactoryUsageTest.java
new file mode 100644
index 0000000..9c9547f
--- /dev/null
+++ b/tests/e2e-jdk-specifics/src/test/java/org/glassfish/jersey/tests/e2e/jdk21/ThreadFactoryUsageTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.jdk21;
+
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.innate.VirtualThreadSupport;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.core.Response;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+public class ThreadFactoryUsageTest {
+    @Test
+    public void testThreadFactory() throws ExecutionException, InterruptedException {
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        ThreadFactory threadFactory = VirtualThreadSupport.allowVirtual(true).getThreadFactory();
+        ThreadFactory countDownThreadFactory = r -> {
+            countDownLatch.countDown();
+            return threadFactory.newThread(r);
+        };
+
+        CompletionStage<Response> r = ClientBuilder.newClient()
+                .property(CommonProperties.THREAD_FACTORY, countDownThreadFactory)
+                .property(CommonProperties.USE_VIRTUAL_THREADS, true)
+                .register((ClientRequestFilter) requestContext -> requestContext.abortWith(Response.ok().build()))
+                .target("http://localhost:58080/test").request().rx().get();
+
+        MatcherAssert.assertThat(r.toCompletableFuture().get().getStatus(), Matchers.is(200));
+        countDownLatch.await(10, TimeUnit.SECONDS);
+        MatcherAssert.assertThat(countDownLatch.getCount(), Matchers.is(0L));
+    }
+}