本文通过母子对话形式引入,介绍了苍穹多线程技术的基础知识和使用案例。概述了多线程的概念、传统方法的局限性及线程池的优势,并详细讲解了如何在苍穹环境中利用线程池创建和管理线程。文章还对比了不同线程池类型(如newCachedThreadPool和newFixedThreadPool)的适用场景,并介绍了Runnable、Callable及Future接口在多线程编程中的应用。最后,通过一个图书管理系统案例,展示了如何使用多线程调用GPT开发平台微服务,实现异步实时数据展示,并详细说明了案例实现步骤和代码编写方法。
:“麻麻,你看我最近是不是变得有点不一样了?”你得意洋洋地问着正在忙碌的麻麻。
:“哎呀,你这孩子,又在搞什么名堂?”麻麻头也不回,继续的做着手中的活。
:“嘿嘿,你可知道我最近学习了一篇秘籍?”你故作神秘地凑近麻麻,小声说道。
:“秘籍,什么秘籍能让孩子变得神神秘秘的?”麻麻终于停下手中的活,好奇地看着你。
:“那就是—苍穹多线程案例秘籍!”你大声宣布,仿佛手里真的拿着一本金光闪闪的秘籍。
:“哟,多线程,讲了什么玩意儿,还能在苍穹中灵活使用多线程?!”麻麻一脸疑惑地看着你。
:“欸,你这就OUT了!这篇多线程案例秘籍,讲了如何在苍穹中使用线程池,以及如何利用线程池创建线程,甚至,还有一个使用多线程的案例,麻麻再也不用担心我搞不懂多线程啦!”
:“你这孩子,让我看看怎么个事儿!”麻麻翻开了秘籍...
本篇文章将从苍穹多线程基础+一个案例的视角轻松带你拿下苍穹多线程的使用方法。
读前准备:一个可编写代码的如IDEA苍穹环境;Cosmic AI开发平台配置好GPT大模型
准备好了吗,那么我们就开始吧!
1 多线程简介
多线程是指在计算机程序中,能够同时运行多个线程的技术。线程是进程中的一个执行单元,它负责在单个程序中执行多任务。多线程技术使得操作系统能够更有效地利用系统资源,提高程序的运行效率和响应速度。
在Java中,一般我们可以用传统的Thread类以及Runnable接口来实现多线程的编程。但是,如果我们只使用Thread类以及Runnable接口会带来什么问题呢?
资源消耗大: 每个线程都需要占用一定的系统资源,包括内存和CPU时间。如果创建过多的线程,会导致系统资源的浪费,甚至可能导致系统性能下降。
管理复杂: 线程的生命周期管理相对复杂,需要手动创建、启动、停止和销毁线程。这增加了编程的复杂性和出错的可能性。 线程之间的同步和通信也需要额外的设计和实现,以确保数据的一致性和线程的安全性。
可扩展性差: 如果一个类已经继承了其他类,就不能再继承Thread类,这限制了类的扩展性。 实现Runnable接口的类虽然可以避免多重继承的问题,但仍然需要手动创建和管理线程,增加了代码的复杂性。
而在实际苍穹开发中,只是单单使用Thread以及Runnable,会造成没有指定上下文的问题,若没有指定上下文,则访问DB接口时,DB无法根据上下文选择一个数据源进行数据库的访问,因此,我们需要在苍穹开发中引入线程池进行线程管理。
2 线程池
线程池是一个管理线程创建、生命周期以及任务调度的框架或模式。它允许开发者创建一组工作线程,这些线程可以反复被用来执行不同的任务,而不是每次执行任务时都创建和销毁线程。线程池中的线程通常会被放置在一个队列中,等待被分配任务。因此,线程池拥有以下优势:
降低资源消耗:通过重用已存在的线程,线程池避免了频繁创建和销毁线程所带来的资源消耗。
提高响应速度:由于线程已经准备好,新任务可以更快地得到执行,无需等待新线程的创建。
简化线程管理:线程池提供了统一的线程管理接口,简化了线程管理的复杂性。
提高系统稳定性:通过限制线程的数量,线程池可以防止过多的线程同时执行,从而避免系统资源耗尽和崩溃的风险。
那么在苍穹的实际开发中,我们应该如何来使用线程池呢?
3 利用线程池快速创建线程
首先,告诉大家一个最简单的线程池使用方法,那就是在ThreadPools类中,直接调用executeOnce方法,该方法能够先初始化一个线程池,初始化之后在线程池的管理下进行方法的执行。不仅实现了多线程,还能进行DB的操作。因此,我们来实操一下。大家可以找到自己系统中有数据的表单来进行一个测试,在这里,我将依照我自己系统中的表单来进行以下代码的编写:
ThreadPools.executeOnce("ThreadForTest", () -> { DynamicObject dynamicObject = BusinessDataServiceHelper.loadSingle("ozwe_book",new QFilter[]{new QFilter("number", QCP.equals, "9787040396638")}); System.out.println("书籍的名称是:"+dynamicObject.getString("name")); });
首先我们使用了ThreadPools类,来执行了其中的executeOnce方法。在方法中,我使用了BusinessDataServiceHelper中的loadSingle方法加载了一个表单,并且对这个表单进行了QFilter过滤处理。处理之后,我将获取好的DynamicObject对象中的name字段打印出来,我们看看是什么效果。
我们可以看到,代码成功执行,数据被成功打印出来了,并且我们还可以在控制台中看到我们ThreadForTest线程的DB执行记录。
4 自定义线程池
那么,我们可不可以创建一个我们自定义的线程池呢?当然是可以的!我们反编译一下ThreadPools类,我们可以看到这几个函数。
没错,可以在上图中看到,ThreadPools类中,提供了newCachedThreadPool和newFixedThreadPool方法来创建线程池。那么这两种线程池有什么区别呢?我们可以看到以上不同函数的参数。
newFixedThreadPool中,nThreads用于指定线程池中固定线程的数量。因此使用newFixedThreadPool方法来创建的线程池的大小是固定的。
在newCachedThreadPool中,coreThread为核心线程数,即线程池中的核心线程数,即使这些线程处于空闲状态,线程池也不会回收它们。maxThread指线程池中允许的最大线程数,也就是总线程数。当然,该参数可以填写Integer.MAX_VALUE,这意味着线程池几乎可以无限制地创建新线程(受限于JVM的内存和线程创建的开销)。
因此,再次查阅相关资料,我们可以得出一个结论:newCachedThreadPool拥有在处理大量短生命周期任务和突发性高并发请求时的优势。它能够根据系统的运行情况动态地调整线程的数量,从而更加高效地利用系统资源。当然,使用newFixedThreadPool方法创建的线程池由于其特性,适用于需要控制并发线程数量的场景,如服务器处理并发请求时限制线程数量以避免资源耗尽。由于线程数量固定,因此可以更好地预测和控制系统的性能。
当主线程提交任务之后,一个线程池的执行顺序如下(图来自搜狗百科):
从图中我们可以看出:
若核心线程池未满,则创建核心线程执行任务
若核心线程池已满,队列未满,则添加任务到队列
若队列已满,线程池未满,则创建非核心线程池执行任务
若线程池已满,则执行饱和策略
好了,理论的话就说到这,大家可以根据自己的业务需求来创建合适的线程池,接下来我们就以newCachedThreadPool为例,为大家创建一个线程池吧!
public static ThreadPool pool = ThreadPools.newCachedThreadPool("CustomThreadPooldick", 3, 10);
没错,就这一行代码,我们就创建了我们自己的线程池。
注意!注意!注意!:线程池不要创建在插件内,由于一些插件是动态加载的,如果多次载入插件,则系统就会检测到重复创建了线程池,从而终止代码的执行,因此会造成插件无法执行等情况。
因此,我们的线程池可以创建到DebugApplication类中,因为这个类是个启动类,它只会加载一遍,如下图:
5 使用自定义线程池
因此,问题来了,我们要怎么使用我们的线程池来创建一个任务呢?我们前往ThreadPool类一探究竟!
我们可以看到,ThreadPool类是一个接口类,他拥有execute和submit函数。我们来解读一下。
其中RequestContext类为上下文,可以获取当前登录用户等,相关文档可跳转查看。
而execute的参数一个是Runnable,submit的参数一个是Callable,并且返回Future类型。那这两种我们应该如何进行使用呢?在这里我们简单介绍一下Runnable、Callable和Future。
Runnable
定义:Runnable是Java中的一个接口,用于指定线程要执行的任务。它只定义了一个方法:
run()
,没有返回值,也不会抛出异常。作用:实现Runnable接口是Java中实现多线程的一种方式。通过实现Runnable接口并重写其
run()
方法,可以定义一个线程要执行的具体任务。
由于Runnable接口中只有一个run方法,因此,在使用接口时,我们可以直接使用lambda表达式来进行使用,使用如下:
pool.execute(() -> { System.out.println("execute方法实现"); });
Runnable方法没有返回值,所以execute方法返回的是void,那如果我们想要多线程能够获取到他的返回值,那我们应该如何使用呢?
Future与Callable
在Java 5之后,Future与Callable也登上了Java的大舞台,以下是对于Callable的定义与作用。
定义:Callable是Java中的一个接口,类似于Runnable接口,但提供了更强大的功能。Callable接口的
call()
方法不仅有返回值,而且可以抛出异常。作用:Callable接口用于定义那些需要返回值或者可能抛出异常的任务。与Runnable相比,Callable提供了更丰富的任务描述能力。
以下是对于Future的定义与作用。
定义:Future是Java中的一个接口,代表异步计算的结果。它提供了检查任务是否完成、取消任务以及获取任务结果的方法。
作用:Future接口使得我们可以对异步执行的任务进行监控和管理。通过Future,我们可以知道任务是否已经开始、是否完成、是否被取消,以及获取任务的结果或异常。
由于Callable接口只有一个call方法,因此我们同样也可以使用lambda表达式来进行函数的编写。同时,我们的submit函数还有一个返回值为Future。利用Future,我们就可以知道Callable中的方法的执行情况。所以,submit函数的使用方式如下:
Future<String> future = pool.submit(() -> { System.out.println("submit"); return "success"; }); boolean isDone = future.isDone(); boolean isCancelled = future.isCancelled(); try { String result = future.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }
其中,isDone()函数则是说明该任务是否执行完成,isCancelled()函数则说明该任务是否被取消,get()函数则是获取该任务的返回值。
注意,如果任务未执行完成,那么使用get()函数将会占线,直到任务执行完成返回结果。
最后,他们的关系用一张图来进行展示,对于Runnable,它execute之后就没有进行返回了,而对于Callable,它submit之后,还可以有返回值,因此,我们只需要在主线程中利用get()来获取相应的值即可。
6 多线程使用案例
介绍完了多线程使用方法之后,我们就以一个实际案例来为大家进行讲解吧!
该案例需要准备:一个GPT提示、一个表单、一个工具栏按钮、三个Markdown控件、一个进度条控件
实现功能:以图书管理系统为案例,指定三个图书,利用多线程调用Cosmic GPT开发平台微服务,每个线程利用大模型进行图书分析,并将结果异步实时地显示在表单的Markdown控件中,实现结果图:
那么开始之前,我们为大家介绍一下案例实现思路。
制作GPT提示
添加一个工具栏按钮,添加三个Markdown控件,其中控件名称分别为开发商标识_markdownap_0、开发商标识_markdownap_1、开发商标识_markdownap_2,这样命名的原因是在循环中判断的时候能够根据index来判断需要对哪个控件进行赋值。
接入进度条接口,指定一个String[]全局变量表示三本书籍,并且注册工具栏按钮,注册进度条事件
对于使用pool中的execute函数还是使用submit函数都可以,我们将异步生成的结果存储在缓存中
进度条控件起一个定时器的作用,每隔500毫秒检测一次缓存,若缓存生成出了结果,则将结果在Markdown控件中进行呈现
若三个线程都执行完成之后,停止执行进度条
对于第一步,我们需要前往Cosmic AI开发平台添加一个GPT提示,我们可以这样来设置
第二步,大家根据自己的表单和样式自行设置控件即可,我是这样设置的,大家可以作为参考
而进度条控件,我则是设置了宽和高为1px,目的是对其进行一个隐藏
注意,这里不能直接将进度条进行隐藏元素和不可见,否则无法实现进度条功能
对于第三步,我们可以这样写,在类中接入了进度条接口,这样写之后我们就成功注册了事件:
public class BorrowBookPlugin extends AbstractBillPlugIn implements ProgresssListener { String[] strings = {"高等数学","数据结构与算法","计算机网络"}; @Override public void registerListener(EventObject e) { this.addItemClickListeners("tbmain"); ProgressBar barAudio = this.getView().getControl("ozwe_progressbarap"); //进度条控件标识 barAudio.addProgressListener(this); } }
注册成功之后,我们就进入第四步,可以添加itemClick事件了
@Override public void itemClick(ItemClickEvent ee) { if ("ozwe_baritemap".equalsIgnoreCase(ee.getItemKey())) { this.getView().showSuccessNotification("正在生成书籍数据"); this.getView().addClientCallBack("ACTION_CLICK", 0); //使用addClientCallBack函数,避免页面卡死 } }
在这里我们使用了addClientCallBack函数是为了防止页面线程卡死,避免在页面线程中执行时间长的任务。
因此,我们还需要clientCallBack函数来继续执行
@Override public void clientCallBack(ClientCallBackEvent e) { //获取一个缓存,该缓存用于存放结果 DistributeSessionlessCache cache = CacheFactory.getCommonCacheFactory().getDistributeSessionlessCache("bookResult"); if("ACTION_CLICK".equals(e.getName())) { //循环全局变量strings字符串 for (int i = 0; i<strings.length; i++) { String bookName = strings[i]; cache.put(bookName, "正在生成结果"); //将Markdown控件更改为正在生成结果 Markdown markdown = this.getControl("ozwe_markdownap_"+i); markdown.setText(cache.get(strings[i])); this.getView().updateView(); //使用submit方法,利用Future和Callable的方式进入多线程 Future<String> future = pool.submit(() -> { Map<String , String> variableMap = new HashMap<>(); Object[] params = new Object[] { //GPT提示编码 getPromptFid("GPT提示编号"), bookName, variableMap }; Map<String, Object> result = DispatchServiceHelper.invokeBizService("ai", "gai", "GaiPromptService", "syncCall", params); JSONObject jsonObjectResult = new JSONObject(result); System.out.println("书籍 "+bookName+" 执行完成"); cache.put(bookName, jsonObjectResult.getJSONObject("data").getString("llmValue")); return jsonObjectResult.getJSONObject("data").getString("llmValue"); }); } //循环完成之后,启动进度条,开始检测 ProgressBar bar = getView().getControl("ozwe_progressbarap"); IClientViewProxy proxy= getView().getService(IClientViewProxy.class); proxy.setFieldProperty(bar.getKey(), ClientProperties.Percent, 0); bar.start(); } } //获取GPT提示的fid public long getPromptFid(String billNo) { DynamicObject dynamicObject = BusinessDataServiceHelper.loadSingle("gai_prompt", "number," + "id", (new QFilter("number", QCP.equals, billNo)).toArray()); return dynamicObject.getLong("id"); }
最后,我们添加进度条功能
@Override public void onProgress(ProgressEvent progressEvent) { //新增一个boolean用于检测是否全部完成 boolean finishFlag = true; DistributeSessionlessCache cache = CacheFactory.getCommonCacheFactory().getDistributeSessionlessCache("bookResult"); for (int i = 0; i<strings.length; i++) { //若缓存结果不为"正在生成结果",及缓存结果已经被覆盖为了最终结果,则说明生成完成 if (!cache.get(strings[i]).equalsIgnoreCase("正在生成结果")) { Markdown markdown = this.getControl("ozwe_markdownap_"+i); markdown.setText(cache.get(strings[i])); } else { finishFlag=false; } } //停止进度条及后续操作 if (finishFlag) { this.getView().showSuccessNotification("所有内容生成成功"); ProgressBar bar = getView().getControl("ozwe_progressbarap"); IClientViewProxy proxy= getView().getService(IClientViewProxy.class); proxy.setFieldProperty(bar.getKey(), ClientProperties.Percent, 0); bar.stop(); } }
好了,这个案例中的代码我们就写完了,我们执行一下最终的结果。
点击测试按钮之后,程序就正在生成结果了
继续等待之后,我们可以看到结果就异步呈现在了Markdown控件当中,再次稍等片刻,我们的所有结果都成功进行了呈现
当然,如果你配置了Monitor监控中心,前往监控中心的集群监控中的线程监控,你还可以看到程序在进行多线程执行的细节,如下:
我们可以看到,在Monitor监控中心中,我们可以查看到更多该线程的执行日志,还可以查看调用链等,可谓是非常方便。
7 总结
那么在此,我们已经将多线程以及案例成功讲解完毕,相信你能够通过这篇文章,解决你目前遇到的一系列难题,找到对于自己业务系统的执行思路。
另外,如果存在大任务,我们不建议大家利用多线程来进行大任务的调用,建议大家利用苍穹MQ服务来进行大任务的执行。
最后祝大家:0 error(s),0 warning(s),0 bug(s)~, all pass!
8 参考文章
推荐阅读