Unfortunately, this information is misleading when it comes to the GroovyScriptEngineImpl class. The Javadoc you mentioned says:
"MULTITHREADED" - the implementation of the mechanism is internally thread safe, and scripts can be executed simultaneously, although the effects of executing a script in one thread may be visible to scripts in other threads.
GroovyScriptEngineImpl does not apply to this, since, for example, you can change the classloader using GroovyScriptEngineImpl.setClassLoader(GroovyClassLoader classLoader) , and this can cause unpredictable behavior when it happens in parallel execution (this method is not even atomic and does not synchronize execution between threads).
Regarding the execution of scriptEngine.eval(script, bindings) , you should be aware of its non-deterministic nature when you use the same bindings for many different threads. javax.script.SimpleBindings constructor uses HashMap , and you should definitely avoid it - in case of multi-threaded execution, it is better to use ConcurrentHashMap<String,Object> at least to allow secure simultaneous access. But even if you cannot get any guarantee while evaluating multiple scenarios at the same time, these scenarios will change the global bindings. Consider the 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 the local variable def x = 4 , and x++ only increments our local script variable. When we print the x binding after running this script, we will see that it does not change at runtime. However, y++ in this case increases the value of the y binding.
In the second script, we define the local variable def y = 10 and add the value local y ( 10 in this case) to the current global binding value x .
As you can see, both scenarios change global bindings. In the sample code shown in this post, we run both scripts 20 times at the same time. We do not know in what order both scripts are executed (imagine that in each execution there is a random timeout, so one script can hang for several seconds). Our bindings use the ConcurrentHashMap internally, so we are only safe when it comes to concurrent access - two threads will not update the same binding at the same time. But we do not know what the result is. After each performance. The first-level loop runs 4 times, and the inner loop runs 5 times, and during each run, it sends a script evaluation using a common script engine and common bindings. Also, the first task replaces the GroovyClassLoader in the engine to show you that it is not safe to share your instance with multiple threads. Below you can find an approximate result (approximate, because every time you run, there is a high probability that 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:
- the
GroovyClassLoader replacement GroovyClassLoader not deterministic (in the first cycle, 3 instances of the loader class were printed, while in the third we printed 5 different instances of the classes) - the final set of bindings is not deterministic. We avoided recording at the same time using
ConcurrentHashMap , but we do not control the execution order, so if you use the binding value from the previous execution, you never know what value to expect.
So, how to be thread safe when using GroovyScriptEngineImpl in a multi-threaded environment?
- do not use global bindings
- when using global bindings, make sure the scripts do not override the bindings (you can use
new SimpleBindings(Collections.unmodifiableMap(map)) for this - otherwise, you must accept the non-deterministic nature of the
bindings state change - expand
GroovyScriptEngineImpl and not allow class loader changes after object initialization - otherwise accept that some other threads may mess up a bit.
Hope this helps.