Groovy ScriptingEngine 线程安全吗?

Is Groovy ScriptingEngine thread safe?

当你打电话时:

 Object isThreadSafe = scriptEngine.getFactory().getParameter("THREADING");

它returnsMULTITHREADED按照:

但不清楚具体的影响是什么。

是否意味着:

如果答案能用代码说明就好了。

不幸的是,如果涉及 GroovyScriptEngineImpl class,此信息会产生误导。您提到的 Javadoc 说:

"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 不适用于此,因为例如您可以使用 GroovyScriptEngineImpl.setClassLoader(GroovyClassLoader classLoader) 方法更改 classloader 并且当它发生在并发执行时可能会导致不可预测的行为(此方法甚至不是原子的并且不会在线程之间同步执行)。

关于 scriptEngine.eval(script, bindings) 执行,当您在许多不同的线程之间共享相同的 bindings 时,您必须意识到它的不确定性。 javax.script.SimpleBindings 默认构造函数使用 HashMap 并且你绝对应该避免它 - 在多线程执行的情况下最好使用 ConcurrentHashMap<String,Object> 至少允许安全的并发访问。但是,即使您同时评估多个脚本并且这些脚本将更改全局绑定时也无法获得任何保证。考虑以下示例:

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()
    }
}

在这个例子中我们定义了两个Groovy脚本:

def x = 4
y++
x++

和:

def y = 10
x += y

在第一个脚本中,我们定义了一个局部变量 def x = 4 并且 x++ 仅递增我们的局部脚本变量。当我们在 运行 之后打印 x 绑定这个脚本时,我们会看到它在执行期间不会改变。但是 y++ 在这种情况下增加了 y 绑定值。

在第二个脚本中,我们定义局部变量 def y = 10 并将局部变量 y 的值(在本例中为 10)添加到当前全局 x 绑定值。

如您所见,这两个脚本都修改了全局绑定。在此 post 中显示的示例代码中,我们 运行 两个脚本并发 20 次。我们不知道这两个脚本的执行顺序是什么(假设每次执行都有一个随机超时,所以一个脚本可能会挂起几秒钟)。我们的 bindings 在内部使用 ConcurrentHashMap 所以我们只有在并发访问时才是安全的——两个线程不会同时更新同一个绑定。但我们不知道结果如何。每次执行后。第一级循环执行 4 次,内部循环执行 5 次,在每次执行期间,它使用共享脚本引擎和共享绑定提交脚本评估。此外,第一个任务替换了引擎中的 GroovyClassLoader 以向您展示跨多个线程共享其实例是不安全的。您可以在下面找到示例输出(示例,因为每次您 运行 很可能会得到不同的结果):

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]

几个结论:

  • 替换 GroovyClassLoader 是不确定的(在第一个循环中打印了 3 个不同的 classloader 实例,而在第三个循环中我们打印了 5 个不同的 classloader 实例)
  • 最终绑定计算是不确定的。我们使用 ConcurrentHashMap 避免了并发写入,但我们无法控制执行顺序,因此在依赖先前执行的绑定值的情况下,您永远不知道期望值是什么。

那么,在多线程环境下使用GroovyScriptEngineImpl时如何做到线程安全呢?

  • 不要使用全局绑定
  • 使用全局绑定时,请确保脚本不会覆盖绑定(您可以使用 new SimpleBindings(Collections.unmodifiableMap(map))
  • 否则你必须接受 bindings 状态修改的非确定性
  • 扩展 GroovyScriptEngineImpl 并且不允许在对象初始化后更改 classloader
  • 否则接受其他线程可能会有点混乱。

希望对您有所帮助。