• 0
  • 0
分享
  • 利用虚拟线程重写自定义异步功能
  • FunTeste 2024-01-03 15:31:42 字数 4488 阅读 276 收藏 0


最近在使用JDK 21的虚拟线程功能,感觉对于性能测试来说,还是非常值得推广的。通过之前文章介绍,相比各位也有所了解了,这里跳过Java虚拟线程的介绍了。


在官方文档中,虚拟线程其中一个适用场景就是处理多个小异步任务时,本着随用随创建,用完即销毁的理念,不要进行过的的多线程管理和多线程同步设计。


这一点说完是否有些似曾相识,跟Golang应用关键字 `go` 非常一致,可以说一模一样了。我感觉这个非常适合处理异步任务,所以对原来的自定义异步关键字进行了新版本的开发。旧版本的功能也是根据 `go` 关键字功能进行开发的。


# 方案设计


下面分享方案设计的要点


1. 没有采用无限创建虚拟线程的方式,还是用了一个最大并行虚拟线程数量限制

2. 使用任务队列设计,使用了线程安全队列,存储待执行的任务

3. 设计了同款daemon线程,功能与上篇自定义异步文章类似,功能从任务队列中获取并执行任务

4. 在通用的工具类中自定义关键字方法,功能向任务队列中添加任务


# 代码实现


## 任务队列


```Java

    /**

     * 待执行任务队列,最大容量为MAX_WAIT_TASK

     */

    static LinkedBlockingQueue<Closure> queue = new LinkedBlockingQueue(MAX_WAIT_TASK)

```


这段代码是在Java中创建了一个静态的待执行任务队列,使用了 `LinkedBlockingQueue` 类型,并命名为 `queue`。在创建队列时,使用了 `MAX_WAIT_TASK` 常量来指定队列的最大容量。


根据代码片段提供的信息,这个队列 `queue` 的元素类型是 `Closure`,这可能是一个自定义类型或者来自某个框架或库的特定类。`LinkedBlockingQueue` 是 Java 中的一个线程安全的队列实现,它使用链表实现了一个阻塞队列,在队列已满或为空时,会对添加或获取元素的操作进行阻塞,直到条件满足。


这段代码创建了一个具有最大容量为 `MAX_WAIT_TASK` 的阻塞队列,用于存储待执行的任务(`Closure` 类型的任务)。队列的容量限制可以确保队列不会无限增长,防止内存溢出或其他资源问题。当往队列中添加元素时,如果队列已满,则添加操作会被阻塞,直到有空间可用。


添加任务方法:


```Java

  

/**  

 * 添加任务  

 * @param closure  

 * @return  

 */  

static def add(Closure closure) {  

    queue.add(closure)  

}

```


## 执行方法


这里写了两个方法,一个执行 `java.lang.Runnable` ,另外一个执行 `groovy.lang.Closure`。


```Java

/**  

 * 执行任务  

 * @param runnable  

 * @return  

 */  

static def execute(Runnable runnable) {  

    daemon()  

    Thread.startVirtualThread {  

        index.getAndIncrement()  

        SourceCode.noError {  

            runnable.run()  

        }  

        index.getAndDecrement()  

    }  

}  

  

/**  

 * 执行任务  

 * @param closure 任务闭包  

 * @return  

 */  

static def execute(Closure closure) {  

    daemon()  

    Thread.startVirtualThread {  

        index.getAndIncrement()  

        SourceCode.noError {  

            closure()  

        }  

        index.getAndDecrement()  

    }  

}

```


这段代码片段展示了两个重载的 `execute()` 方法,用于执行任务。这些方法主要负责启动线程执行任务,并且对执行任务的计数进行增减操作。


1. `execute(Runnable runnable)` 方法:接受一个 `Runnable` 参数,该方法会在内部调用 `daemon()` 方法,确保守护线程已经启动。然后,使用 `Thread.startVirtualThread` 启动一个虚拟线程,对 `index` 进行增减操作,并执行传入的 `runnable.run()`。


2. `execute(Closure closure)` 方法:接受一个闭包(Closure)作为参数。与前一个方法类似,它也会调用 `daemon()` 方法以确保守护线程已经启动。然后,使用 `Thread.startVirtualThread` 启动一个虚拟线程,对 `index` 进行增减操作,并执行传入的 `closure()`。


这两个方法的共同点是它们都启动了一个虚拟线程(Virtual Thread),在这些线程中执行了传入的任务(`runnable` 或 `closure`),同时通过 `index.getAndIncrement()` 和 `index.getAndDecrement()` 对执行任务的计数进行了管理。


## daemon线程


```

  

/**  

 * daemon线程状态,保障只执行一次  

 * @param closure  

 * @return  

 */  

static AtomicBoolean DaemonState = new AtomicBoolean(false)  

  

/**  

 * 最大并发执行任务数量  

 */  

static int MAX_THREAD = 10  

  

/**  

 * 执行daemon线程,保障main方法结束后关闭线程池  

 * @return  

 */  

static def daemon() {  

    def set = DaemonState.getAndSet(true)  

    if (set) return  

    new Thread(new Runnable() {  

  

        @Override  

        void run() {  

            SourceCode.noError {  

                while (ThreadPoolUtil.checkMain()) {  

                    while (index.get() < MAX_THREAD) {  

                        def poll = queue.poll(100, TimeUnit.MILLISECONDS)  

                        if (poll != null) {  

                            execute(poll)  

                        } else {  

                            break  

                        }  

                    }  

                    sleep(0.3)  

                }  

            }  

        }  

    }, "FV").start()  

}

```



这段代码的功能是创建一个名为 `daemon()` 的方法,它涉及了一些多线程处理和任务执行控制的逻辑。


1. `AtomicBoolean DaemonState = new AtomicBoolean(false)`:创建了一个名为 `DaemonState` 的 `AtomicBoolean` 类型的变量,用于控制 `daemon()` 方法是否执行的状态。


2. `static int MAX_THREAD = 10`:定义了一个整数常量 `MAX_THREAD`,表示最大并发执行任务数量。


3. `daemon()` 方法:这是一个多线程的方法,用于执行后台守护线程任务。这个方法通过 `DaemonState` 的状态控制确保只执行一次。具体实现逻辑如下:

   - 首先,使用 `DaemonState.getAndSet(true)` 方法检查 `DaemonState` 的状态,如果已经为 `true`,则直接返回,确保方法只执行一次。

   - 然后,创建一个新的线程,该线程实现了一个 `Runnable` 接口,在 `run()` 方法中执行具体的任务逻辑。

   - 在 `run()` 方法中,通过 `ThreadPoolUtil.checkMain()` 方法检查主线程状态,然后进入一个循环。在循环中,检查当前线程池中任务执行的数量,如果小于 `MAX_THREAD`,则从 `queue` 中获取任务并执行。

   - `queue.poll(100, TimeUnit.MILLISECONDS)` 从任务队列 `queue` 中获取任务,设置了超时时间为 100 毫秒,如果获取到任务则执行 `execute(poll)` 方法,否则跳出内部循环。

   - 外部循环控制着守护线程的执行条件,使用 `sleep(0.3)` 控制循环的时间间隔,确保不会过于频繁地检查任务队列。


这里复用了检查main线程的方法,没有进行兜底执行逻辑,所以可能会因为main线程结束过早,导致任务队列积压任务未被执行。我们有增加这个功能也是保持了虚拟线程非线程的思想,这一点跟 `go` 也保持了一致。


如果想等待的话,可以使用一下方法:


```Java

waitFor {  

    VirtualThreadTool.queue.size() == 0  

}

```


# 总结


一个简单的异步任务执行框架就完成了,各路大神已经测试过Java虚拟线程和Golang语言的 `goroutine` 性能,我就不画蛇添足了。


虚拟线程提供了更轻量级的并发模型,能够有效地管理大规模的并发操作,提升应用程序的性能。在性能测试阶段,可以利用虚拟线程模拟并发场景,评估系统在高并发负载下的表现,检测潜在的性能瓶颈,并进行性能优化。


Java虚拟线程拥有广阔的应用前景,但就目前进展上业务服务还需要时间,但是对于性能测试来讲,已经可以提前下手了。

  • 【留下美好印记】
    赞赏支持
登录 后发表评论
+ 关注

热门文章

    最新讲堂

      • 推荐阅读
      • 换一换
          •   前言  写本篇的原因很简单,2023年还有3个月就结束了,要给自己及其他小伙伴做下总结;  以前呢,都是自己做总结,围绕的无非就是对团队的贡献,个人成长;  但是现在不一样,需要帮小伙伴做总结,也需要为测试团队做总结,突然觉得压力山大,而且也要给优秀同学提名奖项;  因此,就有了本篇的内容,目的很简单,测试岗在评绩效时,到底有哪些维度?  业务测试  测试岗位的分工,粗略分为业务测试跟测试开发,两者因岗位的不同,而要求自然也会有区别,这里就先聊聊业务测试;  从结论而言,业务测试肯定是第一位的,是产品的基础,因此围绕业务会有很多衍生品,比如性能、兼容性、稳定性、安全等等,尽管如此,业务测试...
            0 0 371
            分享
          •   前言  虽然笔者是一个测试老人了,但是基本上所有的测试经验都停留在手工测试方面,对于自动化测试方面的实战经验少之又少,可以说,从这个角度来说,就像生活在原始社会,一切靠双手解决问题。  其实,究其原因:一方面是,自动化方面不求上进,觉得会手工测试就可以了,自动化就能躲就躲吧;另一方面是,觉得自动化是个慢慢积累的过程,不是那么容易学会的,既然不是那么学会的,那是不是......就先不学了,然后,就一拖再拖,能拖就拖,殊不知,自动化已经逐步成为测试领域必备的生存技能了。  所以,为了顺应测试行业发展的潮流,我就开始了从测试“原始人”到测试“现代人”的转变。(顺便说一下,想快速成长,有两个方面的...
            12 13 1782
            分享
          • 作者:Apifox blog 李C理前言伴随互联网革命快速创新发展,API 需求的日益剧增,针对 API 的攻击几乎遍布各个行业,据报道 2022 年全年平均每月遭受攻击的 API 数量超过 21 万,游戏、社交、电商、制造等行业依然是攻击者主要目标。例如社交软件某特,在 2021 年发生数据泄露事件,此次数据泄露影响了多达 540 万用户,产生这场“惨案” 正是攻击者利用了登录 API 端点,产生这一漏洞的原因很可能是 API 过度数据暴露以及安全配置错误(下面我会讲到)。显然无论是 API 攻击整体趋势还是对企业和用户的影响都是不容乐观的。那如何去搭建 API 接口的安全“堡垒”?下面我们...
            0 0 725
            分享
          • 现阶段,发红包抢红包的功能已经十分常见,一些日常使用的app中基本都带有红包这个功能。今天主要来韶一韶测试红包功能时,我们都需要考虑什么呢?~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~【发红包】数字输入框:(1)数字:测试0, 0.009, 0.01,0.011, 01, 199.99, 200, 200.01这些边界值,中文、英文、特殊字符或者这几种的组合这些是禁止输入的。(2)是否支持复制黏贴,数字从别处粘贴过来,能正常复制吗?(3)为空/包含空格是否能过滤,会不会造成数据库报错。(4)金额的增删查改,修...
            1 1 8172
            分享
          • 第4步:评估并选择最适合的测试工具工具选择可能非常棘手,因为市场上有数以千计的可用选项。许多团队仅根据其他人的成功来选择他们的工具。但是一种工具对其他人有效并不一定意味着它对您的团队也有效。考虑您团队的特定需求和资源。也就是说,这里有一些问题需要评估和选择最合适的解决方案:你想解决什么问题?确定您要应用自动化测试(Web、桌面或移动应用程序)的AUT以及您需要的功能(用于测试创建、执行、报告等)谁将使用该工具?如果他们是手动测试人员,那么低代码解决方案会更合适。它可以融入您团队现有的管道和工具链吗?寻找具有本机集成的自动化工具,以减少解决方法的时间。它是面向未来的吗?为避免从一种工具转移到另一...
            0 0 1438
            分享
      • 51testing软件测试圈微信