34
BlockHound: how it works
One of the talks in my current portfolio is Migrating from Imperative to Reactive. The talk is based on a demo migrating from Spring WebMVC to Spring WebFlux in a step-by-step approach. One of the steps involves installing BlockHound: it allows to check whether a blocking call occurs in a thread it shouldn't happen and throws an exception at runtime when it happens.
I've presented this talk several times in the previous week, both in its Java version and its Kotlin one. One such presentation was at Javaday Istanbul. After the talk, one of the questions I received was, "How does BlockHound work?" I admit that at the time, I couldn't remember. After the surprise had passed, but too late, I remembered it involved a Java agent. But I wanted to go down the rabbit hole.
This post tries to explain what I discovered.
Reactive Programming is based on asynchronous message passing. Different frameworks/libraries will differ in their approach: for example, in Project Reactor, an API call is not a blocking request-response call but a subscription to a message(s) that the publisher will deliver in the future.
A standard call chain rarely involves only a publisher and a subscriber. In general, there are multiple steps between them. Each intermediate step, called a processor, acts as a subscriber for the next step and a producer for the previous one. When the target producer emits, it sends a message to its subscriber, which sends a message to its subscriber, and so on, up to the first subscriber in the chain.
If one call in the chain is blocking, it "freezes" the whole chain until the work has finished. In that case, it dramatically diminishes the benefits of the Reactive approach. Enters BlockHound:
BlockHound will transparently instrument the JVM classes and intercept blocking calls (e.g. IO) if they are performed from threads marked as "non-blocking operations only" (ie. threads implementing Reactor's NonBlocking
marker interface, like those started by Schedulers.parallel()
). If and when this happens (but remember, this should never happen!😜), an error will be thrown.
In few words, BlockHound checks for blocking calls in places where there shouldn't be.
Using BlockHound is straightforward:
BlockHound.install()
At this point, I naively figured that every blocking call would throw an exception. That's not the case. You can see for yourself by executing the following code:
public static void main(String[] args) throws InterruptedException {
BlockHound.install();
Thread.currentThread().sleep(200);
}
Though
sleep()
is blocking, the program executes normally.Getting a bit into BlockHound's source code reveals that
sleep()
is indeed configured as blocking (along with several others):public static class Builder {
private final Map<String, Map<String, Set<String>>> blockingMethods = new HashMap<String, Map<String, Set<String>>>() {{
put("java/lang/Thread", new HashMap<String, Set<String>>() {{
put("sleep", singleton("(J)V"));
put("yield", singleton("()V"));
put("onSpinWait", singleton("()V"));
}});
}
So, what's the problem?
Blocking calls are not a problem per se. A lot of calls are blocking but required anyway. It wouldn't be possible to throw on every blocking call.
The problem is to waste CPU cycles unnecessarily:
Hence, blocking methods should run on their dedicated thread. This approach wastes nothing.
Because of that, BlockHound doesn't throw on every blocking call but only on such calls that happen on threads that should be non-blocking. To mark a thread as non-blocking, we need to configure a predicate.
public static void main(String[] args) throws InterruptedException {
BlockHound.install(builder -> {
builder.nonBlockingThreadPredicate(current ->
current.or(thread -> thread.getName().equals("main"))); // 1
});
Thread.currentThread().sleep(200);
}
main
thread is marked as non-blockingRunning the above code expectedly throws the following:
Exception in thread "main" reactor.blockhound.BlockingOperationError: \
Blocking call! java.lang.Thread.sleep
at java.base/java.lang.Thread.sleep(Thread.java)
at ch.frankel.blog.blockhound.B.main(B.java:11)
To be entirely sure, let's tweak our code to mark another thread only as non-blocking.
sleep()
in main
shouldn't throw while sleep()
in this other thread should.public static void main(String[] args) throws InterruptedException {
var name = "non-blocking";
BlockHound.install(builder -> {
builder.nonBlockingThreadPredicate(current ->
current.or(thread -> thread.getName().equals(name))); // 1
});
var thread = new Thread(() -> {
try {
System.out.println("Other thread started");
Thread.currentThread().sleep(2000); // 2
System.out.println("Other thread finished");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, name);
thread.start(); // 3
Thread.currentThread().sleep(200); // 4
System.out.println("Main thread finished");
}
As expected, the previous code outputs the following log:
Other thread started
Exception in thread "non-blocking" reactor.blockhound.BlockingOperationError: \
Blocking call! java.lang.Thread.sleep
at java.base/java.lang.Thread.sleep(Thread.java)
at ch.frankel.blog.blockhound.C.lambda$main$3(C.java:16)
at java.base/java.lang.Thread.run(Thread.java:829)
Main thread finished
Though its artifact's
groupId
is io.projectreactor
, BlockHound is generic:With that, it's time to check the BlockHound API a bit.

For example, the
RxJava2Integration
integration configures BlockHound for RxJava 2:builder.nonBlockingThreadPredicate(current -> current.or(NonBlockingThread.class::isInstance));
It's time to get back to our original question, how does BlockHound work?
Here's what happens when you call
BlockHound.install()
:
The magic happens in the
instrument()
method. There, BlockHound uses bytecode instrumentation to inject a code snippet in front of every method marked as blocking. A method is blocking if:Builder.markAsBlocking()
The snippet's code flow looks like this:

Using BlockHound is straightforward, i.e.,
BlockHound.install()
. A short explanation is that BlockHound is a Java agent.But in this post, we dug a bit under the cover to understand how this simple call worked.
Thanks a lot to Violeta Georgieva for her review!
To go further:
Originally published at A Java Geek on June 20th 2021
34