Skip to content

Commit 5e63223

Browse files
authored
Avoid virtual-thread yield spin in ConcurrentBag (#2402)
Excellent! Thanks for the deep-dive and pull request.
1 parent bba167f commit 5e63223

2 files changed

Lines changed: 91 additions & 2 deletions

File tree

src/main/java/com/zaxxer/hikari/util/ConcurrentBag.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import org.slf4j.Logger;
2020
import org.slf4j.LoggerFactory;
2121

22+
import java.lang.invoke.MethodHandle;
23+
import java.lang.invoke.MethodHandles;
24+
import java.lang.invoke.MethodType;
2225
import java.lang.ref.WeakReference;
2326
import java.util.ArrayList;
2427
import java.util.Collections;
@@ -61,6 +64,7 @@
6164
public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable
6265
{
6366
private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentBag.class);
67+
private static final MethodHandle THREAD_IS_VIRTUAL = resolveThreadIsVirtual();
6468

6569
private final CopyOnWriteArrayList<T> sharedList;
6670
private final boolean useWeakThreadLocals;
@@ -72,6 +76,31 @@ public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseab
7276

7377
private final SynchronousQueue<T> handoffQueue;
7478

79+
private static MethodHandle resolveThreadIsVirtual()
80+
{
81+
try {
82+
return MethodHandles.publicLookup()
83+
.findVirtual(Thread.class, "isVirtual", MethodType.methodType(boolean.class));
84+
}
85+
catch (NoSuchMethodException | IllegalAccessException e) {
86+
return null;
87+
}
88+
}
89+
90+
static boolean isCurrentThreadVirtual()
91+
{
92+
if (THREAD_IS_VIRTUAL == null) {
93+
return false;
94+
}
95+
96+
try {
97+
return (boolean) THREAD_IS_VIRTUAL.invokeExact(Thread.currentThread());
98+
}
99+
catch (Throwable t) {
100+
return false;
101+
}
102+
}
103+
75104
/**
76105
* This interface defines the contract for an entry in the ConcurrentBag.
77106
* It provides methods to manage the state of the entry, which can be
@@ -188,11 +217,12 @@ public void requite(final T bagEntry)
188217
{
189218
bagEntry.setState(STATE_NOT_IN_USE);
190219

220+
final var isVirtualThread = isCurrentThreadVirtual();
191221
for (int i = 1, waiting = waiters.get(); waiting > 0; i++, waiting = waiters.get()) {
192222
if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
193223
return;
194224
}
195-
else if ((i & 0xff) == 0xff || (waiting > 1 && i % waiting == 0)) {
225+
else if (isVirtualThread || (i & 0xff) == 0xff || (waiting > 1 && i % waiting == 0)) {
196226
parkNanos(MICROSECONDS.toNanos(10));
197227
}
198228
else {
@@ -319,11 +349,12 @@ public void unreserve(final T bagEntry)
319349
{
320350
if (bagEntry.compareAndSet(STATE_RESERVED, STATE_NOT_IN_USE)) {
321351
// spin until a thread takes it or none are waiting
352+
final var isVirtualThread = isCurrentThreadVirtual();
322353
for (int i = 1, waiting = waiters.get(); waiting > 0; i++, waiting = waiters.get()) {
323354
if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
324355
return;
325356
}
326-
else if ((i & 0xff) == 0xff || (waiting > 1 && i % waiting == 0)) {
357+
else if (isVirtualThread || (i & 0xff) == 0xff || (waiting > 1 && i % waiting == 0)) {
327358
parkNanos(MICROSECONDS.toNanos(10));
328359
}
329360
else {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (C) 2026 Brett Wooldridge
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.zaxxer.hikari.util;
18+
19+
import static org.junit.Assert.assertFalse;
20+
import static org.junit.Assert.assertTrue;
21+
import static org.junit.Assume.assumeTrue;
22+
23+
import java.lang.reflect.Method;
24+
import java.util.concurrent.atomic.AtomicBoolean;
25+
26+
import org.junit.Test;
27+
28+
public class ConcurrentBagVirtualThreadTest
29+
{
30+
@Test
31+
public void detectsCurrentVirtualThread() throws Exception
32+
{
33+
assertFalse(ConcurrentBag.isCurrentThreadVirtual());
34+
35+
final var ofVirtual = virtualThreadBuilderFactory();
36+
assumeTrue(ofVirtual != null);
37+
38+
final var virtualThreadBuilder = ofVirtual.invoke(null);
39+
final var start = Class.forName("java.lang.Thread$Builder$OfVirtual").getMethod("start", Runnable.class);
40+
final var detected = new AtomicBoolean();
41+
42+
final var virtualThread = (Thread) start.invoke(virtualThreadBuilder,
43+
(Runnable) () -> detected.set(ConcurrentBag.isCurrentThreadVirtual()));
44+
virtualThread.join();
45+
46+
assertTrue(detected.get());
47+
}
48+
49+
private static Method virtualThreadBuilderFactory()
50+
{
51+
try {
52+
return Thread.class.getMethod("ofVirtual");
53+
}
54+
catch (NoSuchMethodException e) {
55+
return null;
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)