等这篇苍穹多线程案例秘籍等到花儿都谢了,看完这篇,麻麻都说你变了个人!原创
金蝶云社区-醉倾梦
醉倾梦
2人赞赏了该文章 17次浏览 未经作者许可,禁止转载编辑于2024年09月30日 16:25:32
summary-icon摘要由AI智能服务提供

本文通过母子对话形式引入,介绍了苍穹多线程技术的基础知识和使用案例。概述了多线程的概念、传统方法的局限性及线程池的优势,并详细讲解了如何在苍穹环境中利用线程池创建和管理线程。文章还对比了不同线程池类型(如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字段打印出来,我们看看是什么效果。


image.png


我们可以看到,代码成功执行,数据被成功打印出来了,并且我们还可以在控制台中看到我们ThreadForTest线程的DB执行记录。


4 自定义线程池

那么,我们可不可以创建一个我们自定义的线程池呢?当然是可以的!我们反编译一下ThreadPools类,我们可以看到这几个函数。

image.png

image.png

没错,可以在上图中看到,ThreadPools类中,提供了newCachedThreadPool和newFixedThreadPool方法来创建线程池。那么这两种线程池有什么区别呢?我们可以看到以上不同函数的参数。

newFixedThreadPool中,nThreads用于指定线程池中固定线程的数量。因此使用newFixedThreadPool方法来创建的线程池的大小是固定的

在newCachedThreadPool中,coreThread为核心线程数,即线程池中的核心线程数,即使这些线程处于空闲状态,线程池也不会回收它们。maxThread指线程池中允许的最大线程数,也就是总线程数。当然,该参数可以填写Integer.MAX_VALUE,这意味着线程池几乎可以无限制地创建新线程(受限于JVM的内存和线程创建的开销)。

因此,再次查阅相关资料,我们可以得出一个结论:newCachedThreadPool拥有在处理大量短生命周期任务和突发性高并发请求时的优势。它能够根据系统的运行情况动态地调整线程的数量,从而更加高效地利用系统资源。当然,使用newFixedThreadPool方法创建的线程池由于其特性,适用于需要控制并发线程数量的场景,如服务器处理并发请求时限制线程数量以避免资源耗尽。由于线程数量固定,因此可以更好地预测和控制系统的性能。

当主线程提交任务之后,一个线程池的执行顺序如下(图来自搜狗百科):

image.png

从图中我们可以看出:

  • 若核心线程池未满,则创建核心线程执行任务

  • 若核心线程池已满,队列未满,则添加任务到队列

  • 若队列已满,线程池未满,则创建非核心线程池执行任务

  • 若线程池已满,则执行饱和策略


好了,理论的话就说到这,大家可以根据自己的业务需求来创建合适的线程池,接下来我们就以newCachedThreadPool为例,为大家创建一个线程池吧!

public static ThreadPool pool = ThreadPools.newCachedThreadPool("CustomThreadPooldick", 3, 10);

没错,就这一行代码,我们就创建了我们自己的线程池。

注意!注意!注意!:线程池不要创建在插件内,由于一些插件是动态加载的,如果多次载入插件,则系统就会检测到重复创建了线程池,从而终止代码的执行,因此会造成插件无法执行等情况。

因此,我们的线程池可以创建到DebugApplication类中,因为这个类是个启动类,它只会加载一遍,如下图:

image.png


5 使用自定义线程池

因此,问题来了,我们要怎么使用我们的线程池来创建一个任务呢?我们前往ThreadPool类一探究竟!

image.png

我们可以看到,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()来获取相应的值即可。

image.png

6 多线程使用案例

介绍完了多线程使用方法之后,我们就以一个实际案例来为大家进行讲解吧!

该案例需要准备:一个GPT提示、一个表单、一个工具栏按钮、三个Markdown控件、一个进度条控件

实现功能:以图书管理系统为案例,指定三个图书,利用多线程调用Cosmic GPT开发平台微服务,每个线程利用大模型进行图书分析,并将结果异步实时地显示在表单的Markdown控件中,实现结果图:

image.png


那么开始之前,我们为大家介绍一下案例实现思路。

  1. 制作GPT提示

  2. 添加一个工具栏按钮,添加三个Markdown控件,其中控件名称分别为开发商标识_markdownap_0、开发商标识_markdownap_1、开发商标识_markdownap_2,这样命名的原因是在循环中判断的时候能够根据index来判断需要对哪个控件进行赋值。

  3. 接入进度条接口,指定一个String[]全局变量表示三本书籍,并且注册工具栏按钮,注册进度条事件

  4. 对于使用pool中的execute函数还是使用submit函数都可以,我们将异步生成的结果存储在缓存中

  5. 进度条控件起一个定时器的作用,每隔500毫秒检测一次缓存,若缓存生成出了结果,则将结果在Markdown控件中进行呈现

  6. 若三个线程都执行完成之后,停止执行进度条

对于第一步,我们需要前往Cosmic AI开发平台添加一个GPT提示,我们可以这样来设置

image.png

第二步,大家根据自己的表单和样式自行设置控件即可,我是这样设置的,大家可以作为参考

image.png

而进度条控件,我则是设置了宽和高为1px,目的是对其进行一个隐藏

注意,这里不能直接将进度条进行隐藏元素和不可见,否则无法实现进度条功能

image.png

对于第三步,我们可以这样写,在类中接入了进度条接口,这样写之后我们就成功注册了事件:

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();
    }
}


好了,这个案例中的代码我们就写完了,我们执行一下最终的结果。

image.png

点击测试按钮之后,程序就正在生成结果了

image.png

继续等待之后,我们可以看到结果就异步呈现在了Markdown控件当中,再次稍等片刻,我们的所有结果都成功进行了呈现

image.png

当然,如果你配置了Monitor监控中心,前往监控中心的集群监控中的线程监控,你还可以看到程序在进行多线程执行的细节,如下:

image.png

我们可以看到,在Monitor监控中心中,我们可以查看到更多该线程的执行日志,还可以查看调用链等,可谓是非常方便。


7 总结

那么在此,我们已经将多线程以及案例成功讲解完毕,相信你能够通过这篇文章,解决你目前遇到的一系列难题,找到对于自己业务系统的执行思路。

另外,如果存在大任务,我们不建议大家利用多线程来进行大任务的调用,建议大家利用苍穹MQ服务来进行大任务的执行。

最后祝大家:0 error(s),0 warning(s),0 bug(s)~, all pass! 


8 参考文章

为了这份线程池使用指南,我鸽了隔壁女神的约会

线程池

知乎:线程池详解

外部链接CSDN:揭秘“newCachedThreadPool“:高效、灵活的Java线程池深度剖析

百度百科:线程池

赞 2