支持JDK19虚拟线程的web框架之:兴风作浪的ThreadLocal

推荐1年前 (2022)发布 AI工具箱
10 0 0

关于ThreadLocal

  • 既然提到了线程,自然绕不开ThreadLocal类,它提供了线程本地变量,此变量和一般的变量不同。通过get & set 方法,每个线程可以获取到自己独立的变量。这个变量实例通常是私有且静态的,可以存储与线程相关的信息,如产品id、事务id等。
  • 下图很形象的展现了ThreadLocal:是完全属于每个线程自己的集合

虚拟线程中,ThreadLocal的问题

  • 既然每个线程都可以拥有属于自己的ThreadLocal对象,那虚拟线程的情况又如何呢?
  • 虚拟线程的特性,使得我们可以在应用代码中创建成千上万个虚拟线程去执行并发任务,而无需担心线程数量对整体计算资源的负担,如果每个线程都用了ThreadLocal,那会不会出现成千上万的ThreadLocal对象呢?线程是虚拟的,对象可是实实在在的,这样会增加系统资源消耗,或者影响性能吗?
  • 不过转念一想,这么明显的问题,连我都能想到,JDK组织又岂会漏掉?应该是我多虑了吧,凭自己”丰富的经验”,我预测解决方案应该和TLAB(Thread Local Allocation Buffer)类似,为海量虚拟线程的ThreadLoacal对象建立映射关系,做到高效管理
  • 然而现实很残酷,脸,被狠狠地抽打,通过Oracle官方博客,知道实际情况真惨…,如下图,中文注释是我的解读,极具悲观色彩,如果翻译得不准确请您告知,谢谢
  • 对上述内容,个人理解是以下两点:
  • 虚拟线程中使用ThreadLocal确实会带来内存问题,现在还无解,连虚拟线程自身的工程Loom都在自己代码中删除ThreadLocal的使用,那么我们普通用户敢用吗?还是避而远之吧,在虚拟线程中不要用ThreadLocal
  • 编号429的JEP,为我们带来了一个解决方案,一种名为Scoped values的变量,可以在一定范围(scope)内被访问,至于这个scope,可以是一个内存范围(例如临时变量就只能在方法内),另外还有一种范围被称为dynamic scope,这个范围就更加灵活了,不过这个JEP当前的状态还很早期,如下图,还在提案阶段,这要是跳票了或者被否了,那我博客不就白写了?就此打住吧,我不能再研究了
    • 搞清楚以上问题后,自己的八卦之心就控制不住了:既然虚拟线程上的ThreadLocal问题这么严重,放眼Java世界的生态这么繁荣,那么多框架和库,那么…你们说
  • 有没有哪个倒霉蛋掉进这个坑里去?
  • 惨不惨?
  • 从坑里爬出来没有?
    • 你别说,还真有…

    踩坑勇士quarkus

    • 这位踩坑勇士,就是贯穿整个《支持JDK19虚拟线程的web框架》系列的quarkus,来吧,一起围观quarkus踩坑,顺便学点知识
    • 先看quarkus官方文档《virtual-threads.adoc》,如下图
    • 我对上述内容的理解:
  • quarkus的人发现:传统线程池模式改用虚拟线程后,性能提升明显,但是反应式框架改用虚拟线程后的提升并不明显,而且还会带来内存消耗过大的问题(看过前面ThreadLocal分析的您,此刻应该猜到原因了了,嘿嘿,您猜的没错)
  • 如果您的应用对内存有较严要求,quarkus官方建议您继续坚持(stick)使用反应式框架(这话中透露出浓浓的无可奈何,别催了,搞不定…)
    • 接下来官方就要甩锅了,有趣的是,这次接锅的并非JDK,而是大名鼎鼎的…Netty

    Netty的问题

    • 为什么是Netty接锅呢?
    • 首先,Netty使用了Reactor线程模型,而Netty Reactor的核心是Event Loop,下图来自《Netty in Action》,是处理web请求的内部架构图,
    • 那么,应该有多少个EventLoop线程呢?下图是Netty源码,默认值是CPU核数的2倍,看得出这是个很保守的数字
    • 从上面的架构图和代码可以看出,Netty的反应式框架的核心是使用少量线程来分发web请求,这样的结果仅使用了少量线程资源就能高效处理事件
    • 也正式因为有了线程数不多这个前提,在对JSON做序列化处理时,Netty放心的使用了ThreadLocal,毕竟线程少,一个4核的CPU也才8个ThreadLocal,毫无压力
    • 而且,为了更加高效,Netty还对ThreadLoacal进行过改造,也就是他们自研的FastThreadLocal
    • 然后,时间一天天过去,终于等来了JDK19发布,
    • quarkus的反应式web服务模块底层就是Netty,为了用上虚拟线程,他们动手了…咱们脑补一下吧,铺天盖地的虚拟线程线程,铺天盖地的FastThreadLocal对象,炸了吧您…Are U OK ?
    • 快乐之后,咱们还是要正视这个问题,表面上看是个坑,实际上是两种设计思路的冲突:
  • 虚拟线程的特性类似golang的协程,很适合直接拿来处理高并发web请求,为每个请求分配一个虚拟线程,逻辑清晰直白,资源消耗又不高,典型的简单高效
  • Netty的反应式模型,核心思路就是用少量线程高效分发大量请求,本身就很高效,而且就算优化,线程数也不是瓶颈
  • 所以,quarkus拎着虚拟线程冲到Netty的地盘一阵操作猛如虎,一看结果…唉,扯远了,来看quarkus官方的解释吧
    • 上图红框中那句话很有价值,咱们都能从中领悟到一些东西,我的收获是:当线程数不是系统瓶颈的时候,就别冲动,强行上虚拟线程没用

    quarkus强行挽尊

    • 既然虚拟线程不适合反应式模型,个人认为:那就不妨大大方方的承认Netty的Reactor是优秀的,放弃将虚拟线程加入进来,这样不是挺好么?
    • 然而quarkus接下来的操作还是把我吓到了:既然虚拟线程不适合反应式模型?那就想办法强行让它适合,下图就是quarkus的做法:在构建阶段,找到创建ThreadLocal的那段代码,修改它的字节码,以此来解决前面的内存问题
    • 然后我就翻到了上图提到的那段代码
    • 好奇心驱使,我点开上图那个NettyCurrentAdaptor去看了下源码,当时就一阵头晕眼花,ASM风格的代码您能撑多久?试试下图
    • 按照官方的说法,经过他们的优化有百分之八十的提升,终于快要达到之前反应式框架的水平了
    • 呃,搞得这么辛苦,也只是快要追上而已,那行,咱不用了行吗?
    • 另外,上面说的优化手段也不是默认开启的,还要做以下几步操作
  • maven的pom.xml添加以下依赖
  • io.quarkusquarkus-netty-loom-adaptor

  • 编译构建的时候,增加参数-Dnet.bytebuddy.experimental
  • 启动的时候,增加参数–add-opens java.base/java.lang=ALL-UNNAMED
    • 上述操作算,quarkus的手段,我这个草根只能仰望,能开拓自己的见识:原来还可以这样解决问题
    • 但我自己是绝对不敢模仿的,开玩笑,在编辑阶段注入代码,难度太大,并且后面如何维护和交接?
    © 版权声明

    相关文章