Should I use a separate ScriptEngine and CompiledScript instances per each thread?

And390 picture And390 · May 9, 2015 · Viewed 17.4k times · Source

My program uses Java Scripting API and can eval some scripts concurrently. They don't use shared script objects, Bindings or Context, but can use same ScriptEngine and CompiledScript objects. I see that Oracle Nashorn implementation in Java 8 is not multithreaded, ScriptEngineFactory.getParameter('THREADING') returns null about which the documentation says:

The engine implementation is not thread safe, and cannot be used to execute scripts concurrently on multiple threads.

Does it mean that I should create a separate instance of ScriptEngine for each thread? Besides, documentation says nothing about CompiledScript concurrent usage but:

Each CompiledScript is associated with a ScriptEngine

It can be assumed that CompiledScript thread-safety depends on related ScriptEngine, i.e. I should use separate CompiledScript instance for each thread with Nashorn.

If I should, what is the appropriate solution for this (I think very common) case, using ThreadLocal, a pool or something else?

final String script = "...";
final CompiledScript compiled = ((Compilable)scriptEngine).compile(script);
for (int i=0; i<50; i++) {
    Thread thread = new Thread () {
        public void run() {
            try {
                scriptEngine.eval(script, new SimpleBindings ());  //is this code thread-safe?
                compiled.eval(new SimpleBindings ());  //and this?
            }
            catch (Exception e)  {  throw new RuntimeException (e);  }
        }
    };
    threads.start();
}

Answer

Attila Szegedi picture Attila Szegedi · May 11, 2015

You can share a ScriptEngine and CompiledScript objects across threads. They are threadsafe. Actually, you should share them, as a single engine instance is a holder for a class cache and for JavaScript objects' hidden classes, so by having only one you cut down on repeated compilation.

What you can't share is Bindings objects. The bindings object basically corresponds to the JavaScript runtime environment's Global object. The engine starts with a default bindings instance, but if you use it in multithreaded environment, you need to use engine.createBindings() to obtain a separate Bindings object for every thread -- its own global, and evaluate the compiled scripts into it. That way you'll set up isolated global scopes with the same code. (Of course, you can also pool them, or synchronize on 'em, just make sure there's never more than one thread working in one bindings instance). Once you evaluated the script into the bindings, you can subsequently efficiently invoke functions it defined with ((JSObject)bindings.get(fnName).call(this, args...)

If you must share state across threads, then at least try to make it not mutable. If your objects are immutable, you might as well evaluate the script into single Bindings instance and then just use that across threads (invoking hopefully side-effect free functions). If it is mutable, you'll have to synchronize; either the whole bindings, or you can also use the var syncFn = Java.synchronized(fn, lockObj) Nashorn-specific JS API to obtain versions of JS functions that synchronize on a specific object.

This presupposes that you share single bindings across threads. If you want to have multiple bindings share a subset of objects (e.g. by putting the same object into multiple bindings), again, you'll have to somehow deal with ensuring that access to shared objects is thread safe yourself.

As for THREADING parameter returning null: yeah, initially we planned on not making the engine threadsafe (saying that the language itself isn't threadsafe) so we chose the null value. We might need to re-evaluate that now, as in the meantime we did make it so that engine instances are threadsafe, just the global scope (bindings) isn't (and never will be, because of JavaScript language semantics.)