Skip to content
Tech News
← Back to articles

Virtual Threads for a scripting language in Java 8 without Loom

read original more articles
Why This Matters

This article highlights the development of Jactl, a secure scripting language for Java that supports non-blocking execution even on Java 8 and 11, long before Virtual Threads were introduced. By enabling scripts to pause and resume around blocking operations, it offers a way to improve concurrency and performance in Java applications without requiring the latest Java versions. This approach is significant for developers maintaining legacy systems or seeking more efficient, non-blocking scripting solutions in their Java environments.

Key Takeaways

Jactl is a secure, embeddable scripting language for Java applications. When I first started developing Jactl, I wanted a scripting language that compiled to bytecode for optimum performance, was secure so applications could control exactly what scripts could and couldn't do, and most of all, did not block the execution thread when long-running, blocking operations were performed.

When I began developing Jactl, Java 21 and Virtual Threads did not exist, and event-driven, reactive applications (such as ones based on Vert.x) were the way in which high-throughput Java applications were written. I also had a need for a scripting language that would work on applications still stuck on Java 8 or Java 11.

note Now, with the later versions of Java, Java applications that want to use Virtual Threads rather than adopting an event-based architecture can choose to disable the built-in Jactl async mechanism described here by configuring the JactlContext.async(false) flag.

A reactive application is one where a pool of event-loop threads process events from a queue. The golden rule is that events should never block as this will pause one of the event-loop threads, preventing it from processing any more events until that blocking operation completes. A blocking operation is one where the thread is no longer actively processing code but is waiting for the result of an operation such as a database request or a remote procedure call. If blocking operations can occur on an event-loop thread, you will eventually see situations where all threads are waiting for long-running operations and no events are being processed.

I wanted a scripting language that could be invoked from an event-loop thread but, when it performed any blocking operation, it would somehow save its state and return, freeing up the thread to process further events. When the result of the long-running operation was available the script would be resumed from the point where it left off and continue with its processing. In Java 21 and later, Virtual Threads provide the same functionality - they preserve the call stack with all the local variables and allow the thread to continue performing other work and when the blocking operation is complete the call stack is restored and the program continues from where it left off.

The goal was only to save the execution state of the Jactl code, not the state of the Java code that was invoking a Jactl script. Since the Java application is event-based, the script will complete as a new event on an event-loop thread and a completion callback will be invoked once the script finishes that calls back into the Java application with the script result. The callback provided by the application can hold onto any state that the application needs.

In Java 8, of course, there is no way to preserve the call stack, either in Java or in JVM bytecode, so I had to use a different mechanism to achieve the same end.

Imagine that we have a script that needs to invoke a function (or method) that performs a long-running operation. For the sake of the example, let's assume that the function needs to perform sleep() for some period of time before it does something else. There will be a Java call stack with a stack frame for each nested method call and then the rest of the stack will be Jactl stack frames, one for each nested Jactl function call, with the topmost stack frame being the stack frame for the sleep() function itself. Each stack frame tracks where in the code the function invocation occurs, along with the values for its local variables:

To capture the execution state, I figured that the easiest thing to do would be to throw an exception at the start of a long-running operation like sleep() and generate code in each Jactl method/function that catches the exception, saves its state, and throws a new exception that is chained to the one it just caught. I called the class for the exception being thrown Continuation since a continuation is a representation of the execution state of a program.

The implementation of the sleep() function will then look something like this:

... continue reading