D2js 是如何处理并发的

d2js 运行于 servlet 容器,如tomcat,由于容器自身支持并发,似乎 d2js 只要使用 nashorn 运行脚本即可。这样我们得到最简单的实现方式:

image

在该方式中,nashorn引擎仅存在于Servlet.service调用栈,在调用完成后即释放。Hotspot 将栈上对象(局部变量、牵连的函数调用中的局部变量)也分配在堆里,但是栈上对象存活时间很短,只要新生代空间足够,其永远不会进入年老代,回收策略简单。对于高并发少持久数据的网站系统,加大新生代当是有效做法。

为每个请求创建ScriptEngine的方式缺陷明显:

1.每次都创建一个 ScriptEngine,输出响应后即释放,代价很高

2.每次脚本都要编译(nashorn 确实编译了js),花销很大

以上缺陷导致该方式没有实用价值,仅在开发原型时有意义。但该方式也有它的参考意义:隔离是并发的秘诀,充分的隔离=完美的并发,该方式对每个请求使用独立的ScriptEngine,开发者在 js 中可以尽情使用全局变量而不用担心全局污染,这也使人员要求大为降低。

本轮优化前 d2js 使用的是这个方案的升级版本,采用“每个 d2js 文件一个 ScriptEngine Pool” 的方案,可以方便的使用全局变量,对开发人员要求较低。这个办法资源消耗巨大,行不通。

性能优化的目标是:让所有d2js文件运行于同一引擎实例!!每个d2js文件只加载一次,除非文件发生写入。

同一引擎不存在难度,就是个单例。但总有一些弯路让我们技痒难耐。比如有一个貌似合理的替代方案:ThreadLocal<ScriptEngine>,这个方案假设 ScriptEngine 只适合单线程,考虑到目前 jdbc 都是同步的,在 d2js函数调用期间,不会发生线程切换,采用 ThreadLocal 即使使用全局变量也是安全的——既然引擎是线程安全的,引擎内的变量自然也是线程安全的。但该方式实测性能不及使用同一引擎。

使用同一引擎后,原来通过全局变量表示的 d2js, request, response,session 等都需要重构。

首先是 d2js 对象。d2js 文件中接口是以 d2js.func = function(){} 形式插拔的。使用同一引擎后,d2js 不再是全局变量,要保持该表达方式,目前采用的办法是对每个d2js文件套用一个模板代码,该模板是一个外层闭包,将 d2js 化为该闭包的变量:

function createD2js(){

    var d2js/**/ = new D2JS();

    // ------- 用户代码插入模板-----------------
    d2js.func = function(){}   // 将用户代码置入模板内
    // --------以上用户代码---------------------

    … …

    return d2js;

} 

加载 d2js 文件时套用模板、执行该模板函数并获得返回值。每个 d2js 文件都得到一个 D2JS 实例,所有D2JS实例存放于 ConcurrentHashMap<String filename, Object D2JS Instance> allD2js 。

处理HTTP请求时,根据文件名找到 D2JS 实例,由相应实例提供服务。

d2js 函数中还需要访问 request, response, session 等对象。这些对象是HTTP请求带来的,每个请求都不同,同一 d2js 对象要支持对并发请求提供服务。由于使用的是同一个ScriptEngine,全局变量表达方式显然行不通了。可行的有两种做法:

1. 扩充参数栈,增加一个参数:d2js.func = function(params, http){}

http 是封装了 {request:request, response: response, session: session} 的一个对象,一个复合粒子。

这个方式的好处是,有时 d2js.func 不需要使用 request 等对象,该形参就可以留空,重构代价不大。缺点是,当函数需要更多参数时(如给其它函数调用的私有函数),很难确切知道应该在哪个位置提供该参数。

2.将 request, response 等表达为 this.request, this.response。这个做法更有效,最终选择了这种方式。

怎么才能让同一个 d2js 实例分别对应不同的 request、response 呢?

办法很有趣。我将上述存放在 allD2js 中的 D2JS 实例作为 prototype,每次请求时创建一个基于该 prototype 的新实例为该请求服务,服务完毕后该实例即释放。

该方式流程如下:

image

在原理上这种一次性的附带上当次调用信息的镜像实例与闭包的偏函数(柯里化)本质相同。该实现方式发挥了 js 语言原型链思想,特别适合按上下文偏分服务的并发情形,和 erlang 将偏分信息全部放置于栈相比,该方式具备基于原型链的面向对象特点,但又延续了栈隔离的特色。未来 nashorn 实现 async/await、 jdbc 实现 async-jdbc 后,该并发表达方式将会获得更强的生命力。

采取该方式后,每次请求仅在栈上生成一个镜像 D2JS 对象,内存消耗少,业务间互相隔绝,除修改D2JS文件导致的并发冲突外,无其它潜在冲突资源,不使用任何锁。实测效率与 JSP 相比约为 JSP 的 50%-90%,对于运行于 JVM 的脚本语言这已经非常不错了。

原文地址:https://www.cnblogs.com/inshua/p/6121657.html