Unfortunately this information is misleading if it comes to GroovyScriptEngineImpl
class. The Javadoc you mentioned says:
"MULTITHREADED" - The engine implementation is internally thread-safe and scripts may execute concurrently although effects of script execution on one thread may be visible to scripts on other threads.
GroovyScriptEngineImpl
does not apply to this, because e.g. you can change the classloader with GroovyScriptEngineImpl.setClassLoader(GroovyClassLoader classLoader)
method and it may cause unpredictable behavior when it happens in concurrent execution (this method is not even atomic and does not synchronize execution between threads).
Regarding scriptEngine.eval(script, bindings)
execution, you have to be aware of its non-deterministic nature when you share same bindings
across many different threads. javax.script.SimpleBindings
default constructor uses HashMap
and you should definitely avoid it - in case of multithreaded execution it's better to use ConcurrentHashMap<String,Object>
to at least allow safe concurrent access. But even though you can't get any guarantee when you evaluate concurrently multiple scripts and those scripts will change global bindings. Consider following example:
import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl
import javax.script.ScriptContext
import javax.script.SimpleBindings
import javax.script.SimpleScriptContext
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future
class GroovyScriptEngineExample {
static void main(args) {
def script1 = '''
def x = 4
y++
x++
'''
def script2 = '''
def y = 10
x += y
'''
final GroovyScriptEngineImpl engine = new GroovyScriptEngineImpl()
final ExecutorService executorService = Executors.newFixedThreadPool(5)
(0..3).each {
List<Future> tasks = []
final SimpleBindings bindings = new SimpleBindings(new ConcurrentHashMap<String, Object>())
bindings.put('x', 1)
bindings.put('y', 1)
(0..<5).each {
tasks << executorService.submit {
engine.setClassLoader(new GroovyClassLoader())
engine.eval(script1, bindings)
}
tasks << executorService.submit {
println engine.getClassLoader()
engine.eval(script2, bindings)
}
}
tasks*.get()
println bindings.entrySet()
}
executorService.shutdown()
}
}
In this example we define two Groovy scripts:
def x = 4
y++
x++
and:
def y = 10
x += y
In the first script we define a local variable def x = 4
and x++
increments only our local script variable. When we print x
binding after running this script we will see that it won't change during the execution. However y++
in this case incremented y
binding value.
In the second script we define local variable def y = 10
and we add value of local y
(10
in this case) to current global x
binding value.
As you can see both scripts modify global bindings. In the exemplary code shown in this post we run both scripts 20 times concurrently. We have no idea in what order both scripts get executed (imagine that there is a random timeout in each execution, so one script may hang for couple of seconds). Our bindings
use ConcurrentHashMap
internally so we are only safe if it comes to concurrent access - two threads won't update same binding at the same time. But we have no idea what is the result. After each execution. The first level loop executes 4 times and internal loop executes 5 times and during each execution it submits script evaluation using shared script engine and shared bindings. Also first task replaces GroovyClassLoader
in the engine to show you that it is not safe to share its instance across multiple threads. Below you can find exemplary output (exemplary, because each time you'll run there is a high probability you will get different results):
groovy.lang.GroovyClassLoader@1d6b34d4
groovy.lang.GroovyClassLoader@1d6b34d4
groovy.lang.GroovyClassLoader@64f061f1
groovy.lang.GroovyClassLoader@1c8107ef
groovy.lang.GroovyClassLoader@1c8107ef
[x=41, y=2]
groovy.lang.GroovyClassLoader@338f357a
groovy.lang.GroovyClassLoader@2bc966b6
groovy.lang.GroovyClassLoader@2bc966b6
groovy.lang.GroovyClassLoader@48469ff3
groovy.lang.GroovyClassLoader@48469ff3
[x=51, y=4]
groovy.lang.GroovyClassLoader@238fb21e
groovy.lang.GroovyClassLoader@798865b5
groovy.lang.GroovyClassLoader@17685149
groovy.lang.GroovyClassLoader@50d12b8b
groovy.lang.GroovyClassLoader@1a833027
[x=51, y=6]
groovy.lang.GroovyClassLoader@62e5f0c5
groovy.lang.GroovyClassLoader@62e5f0c5
groovy.lang.GroovyClassLoader@7c1f39b5
groovy.lang.GroovyClassLoader@657dc5d2
groovy.lang.GroovyClassLoader@28536260
[x=51, y=6]
A few conclusions:
- replacing
GroovyClassLoader
is non-deterministic (in the 1st loop 3 different classloader instances were printed while in the 3rd one we have printed 5 different classloader instances)
- final bindings calculation is non-deterministic. We have avoided concurrent write with
ConcurrentHashMap
but we have no control over the execution order, so in case of relying on binding value from previous execution you never know what value to expect.
So, how to be thread-safe when using GroovyScriptEngineImpl
in multithreaded environment?
- don't use global bindings
- when using global bindings, make sure that scripts don't override bindings (you can use
new SimpleBindings(Collections.unmodifiableMap(map))
for that
- otherwise you have to accept non-deterministic nature of
bindings
state modification
- extend
GroovyScriptEngineImpl
and don't allow changing classloader after object was initialized
- otherwise accept that some other thread may mess up a little bit.
Hope it helps.