封面《きまぐれテンプテーション2 ゆうやみ廻奇譚》
前言
在公司里做项目的时候有一个需要使用 groovy 来计算表达式结果的情景。但是在使用过程中经常碰到一些计算结果不符合预期的问题,最后查阅文档在公司里一篇 13 年的文章找到答案,因此来记录一下。
问题复现
场景描述
公司里面使用 groovy 的场景是一个对树做递归场景,先对表达式求值结果,然后根据表达式结果 dfs 求取下一个表达式。由于对树做递归不影响其他表达式求值,因此在递归的过程中做了并行流来优化速度。在此时出现了表达式求值结果与预期不符合的场景
问题代码复现
由于 Groovy 在解析脚本的时候耗时很大以及为了避免缓存过多 class 导致永久代内存 OOM,因此在项目中我们使用了 ConcurrentHashMap 来缓存解析后实例化的Script对象。在这之中发现了 Groovy 脚本在多线程中出现 binding 混乱导致表达式求值结果不符合预期。下面是一个简单的代码复现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import groovy.lang.Binding; import groovy.lang.GroovyClassLoader; import groovy.lang.Script;
import java.util.ArrayList; import java.util.List;
public class Main {
public static void main(String[] args){ String exp = "return aVal"; try (GroovyClassLoader classLoader = new GroovyClassLoader()) { Class scriptClass = classLoader.parseClass(exp); Script script = (Script) scriptClass.newInstance(); runScript(script); } catch (Exception e) { throw new RuntimeException(e); } }
public static void runScript(Script script) throws InterruptedException { List<Thread> threads = new ArrayList<>(); for (int i = 0;i<100;i++) { final int index= i; final boolean aVal = i % 2 == 0; Thread thread = new Thread(() -> { Binding binding = new Binding(); binding.setVariable("aVal",aVal); script.setBinding(binding); boolean result = (boolean) script.run(); if (result != aVal) { System.out.printf("index=%d Expected %b but got %b%n",index, aVal, result); } }); threads.add(thread); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } } }
|
在预想之中结果应该是 stdout 不会有任何输出,但是实际的输出结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| index=20 Expected true but got false index=18 Expected true but got false index=0 Expected true but got false index=24 Expected true but got false index=34 Expected true but got false index=60 Expected true but got false index=10 Expected true but got false index=46 Expected true but got false index=44 Expected true but got false index=40 Expected true but got false index=36 Expected true but got false index=4 Expected true but got false index=94 Expected true but got false index=50 Expected true but got false index=6 Expected true but got false index=8 Expected true but got false index=78 Expected true but got false index=66 Expected true but got false index=42 Expected true but got false index=64 Expected true but got false index=52 Expected true but got false index=12 Expected true but got false index=62 Expected true but got false index=22 Expected true but got false index=88 Expected true but got false index=80 Expected true but got false index=58 Expected true but got false index=16 Expected true but got false index=54 Expected true but got false index=30 Expected true but got false index=76 Expected true but got false index=82 Expected true but got false index=98 Expected true but got false index=86 Expected true but got false index=74 Expected true but got false index=32 Expected true but got false index=38 Expected true but got false index=26 Expected true but got false index=68 Expected true but got false index=2 Expected true but got false index=90 Expected true but got false index=28 Expected true but got false index=72 Expected true but got false index=48 Expected true but got false index=96 Expected true but got false index=84 Expected true but got false index=56 Expected true but got false index=70 Expected true but got false index=14 Expected true but got false index=92 Expected true but got false
|
可以看到有很多结果并不符合预期,这主要是因为在高并发的情况下,第一个线程获取到 script 对象后进行参数绑定,但是还没有执行逻辑方法时,第二个进程进行参数绑定导致执行了第二个进程的参数。
解决方案
使用 synchronized 关键字
最简单的解决方案就是在执行脚本的地方加上 synchronized 关键字。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class Main {
public static void main(String[] args){ String exp = "return aVal"; try (GroovyClassLoader classLoader = new GroovyClassLoader()) { Class scriptClass = classLoader.parseClass(exp); Script script = (Script) scriptClass.newInstance(); runScript(script); } catch (Exception e) { throw new RuntimeException(e); } }
public static void runScript(Script script) throws InterruptedException { List<Thread> threads = new ArrayList<>(); for (int i = 0;i<100;i++) { final int index= i; final boolean aVal = i % 2 == 0; Thread thread = new Thread(() -> { synchronized (script) { Binding binding = new Binding(); binding.setVariable("aVal", aVal); script.setBinding(binding); boolean result = (boolean) script.run(); if (result != aVal) { System.out.printf("index=%d Expected %b but got %b%n", index, aVal, result); } } }); threads.add(thread); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } } }
|
使用 synchronized 关键字后,stdout 没有任何输出,说明结果符合预期。
缓存 Script 的 Class 对象
由于 synchronized 关键字会影响并发性能,因此更好的解决方案是缓存Script的 Class 对象,在每个线程中使用 newInstance 方法创建独立的Script对象。代码如下。这样每个 Script 对象都是独立的,互不影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import groovy.lang.Binding; import groovy.lang.GroovyClassLoader; import groovy.lang.Script;
import java.util.ArrayList; import java.util.List;
public class Main {
public static void main(String[] args){ String exp = "return aVal"; try (GroovyClassLoader classLoader = new GroovyClassLoader()) { Class<?> scriptClass = classLoader.parseClass(exp); runScript((Class<Script>) scriptClass); } catch (Exception e) { throw new RuntimeException(e); } }
public static void runScript(Class<Script> scriptClass) throws InterruptedException { List<Thread> threads = new ArrayList<>(); for (int i = 0;i<100;i++) { final int index= i; final boolean aVal = i % 2 == 0; Thread thread = new Thread(() -> { Script script = null; try { script = scriptClass.newInstance(); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } Binding binding = new Binding(); binding.setVariable("aVal", aVal); script.setBinding(binding); boolean result = (boolean) script.run(); if (result != aVal) { System.out.printf("index=%d Expected %b but got %b%n", index, aVal, result); } }); threads.add(thread); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } } }
|
可以看到使用这种方式后,stdout 没有任何输出,说明结果符合预期。而且脚本也是并行执行,但需要注意解析出来的 class 没有被使用的时候可能无法被 JVM 回收,因此需要使用 new GroovyClassLoader () 的方式来加载 class,避免永久代内存溢出。
参考资料