实战0-1,Java开发者也能看懂的大模型应用开发实践!!!

实战,java,开发者,能看懂,模型,应用,开发,实践 · 浏览次数 : 9

小编点评

## 代码生成内容时需要带简单的排版 以下是代码生成内容时需要带简单的排版的内容: 1. **使用合适的格式输出数据**: - 在输出文本时,使用统一的格式输出数据,例如:`\n`或`\t`等,避免乱码。 - 可使用格式化输出数据,例如:`double`、`int`等,以确保数据格式准确。 2. **使用简单的格式控制工具**: - 可使用`StringBuilder`等类,创建并控制文本格式。 - 可使用`String`等类,在文本中添加格式化符号,例如:`${}`等。 3. **使用简单的代码控制格式**: - 可使用`if`、`else`等语句,控制文本格式。 - 可使用`for`等循环,遍历文本并进行格式化。 4. **使用简单的代码控制逻辑**: - 可使用`switch`等语句,进行文本格式化。 - 可使用`if`、`else`等语句,控制文本格式。 5. **使用简单的代码控制格式**: - 可使用`StringBuilder`等类,创建并控制文本格式。 - 可使用`String`等类,在文本中添加格式化符号,例如:`${}`等。 6. **使用简单的代码控制格式**: - 可使用`if`、`else`等语句,控制文本格式。 - 可使用`for`等循环,遍历文本并进行格式化。 7. **使用简单的代码控制格式**: - 可使用`StringBuilder`等类,创建并控制文本格式。 - 可使用`String`等类,在文本中添加格式化符号,例如:`${}`等。 **示例代码**: ```java // 使用StringBuilder格式化输出数据 StringBuilder builder = new StringBuilder(); builder.append("这是一个文本"); builder.append("\n"); // 使用格式化输出数据 String text = builder.toString(); // 使用switch格式化输出数据 switch (text) { case "这是一个文本": System.out.println("这是一个文本"); break; case "这是一个数字": System.out.println("这是一个数字"); break; default: System.out.println("格式不正确"); } // 使用for循环遍历文本并进行格式化 for (int i = 0; i < text.length; i++) { if (text[i] == "${") { System.out.print("格式化符号"); } else { System.out.print(text[i]); } } ``` **注意**: 1. 以上只是示例代码,可根据实际需求进行修改。 2. 可使用其他代码控制工具,例如:`String`等类,进行文本格式化。

正文

前言

在前几天的文章《续写AI技术新篇,融汇工程化实践》中,我分享说在RAG领域,很多都是工程上的实践,做AI大模型应用的开发其实Java也能写,那么本文就一个Java开发者的立场,构建实现一个最基础的大模型应用系统。

而大模型应用系统其实在目前阶段,可能应用最广的还是RAG领域,因此,本文也是通过在RAG领域的基础架构下,来实现应用的开发,主要需求点:让大模型理解文本(知识库)内容,基于知识库范围内的内容进行回答对话

而基于知识库的回答会帮助我们解决哪些问题呢?

  • 节省大模型训练成本:我们知道ChatGPT的知识内容停留在2021年,最新的知识它并不知道,而检索增强生成则可以解决大模型无法快速学习的问题,训练大模型代价是非常昂贵的,不仅仅只是金钱,还包括时间,随着模型的参数大小成本成正相关。
  • 让大模型更聪明:很多企业内部的私有数据大模型并没有学习,而通过RAG的方式可以让大模型在知识库范围的领域进行回答,避免胡说八道,基于底层大模型的基座,可以让我们的应用系统看上去更加的聪明。

在本文中,你将学习到:

  • ✅ RAG工程的基本处理框架流程(基于Java)
  • ✅ 向量数据库的基础使用及了解

技术栈

考虑到作者也是Java开发者,因此本文所选择的技术栈以及中间件也是Java人员都耳熟能详的,主要技术栈如下:

1、开发框架:Spring BootSpring Shell(命令行对话)

Java开发者对于Spring Boot的生态应该是非常熟悉的,而选择Spring Shell工具包主要是为了演示命令行的交互问答效果,和本次的技术无太大关系,算是一个最小雏形的产品交互体验。

2、HTTP组件:OkHTTPOkHTTP-SSE

此次我们选择的大模型是以智谱AI开放的ChatGLM系列为主,因此我们需要HTTP组件和商业大模型的API进行接口的对接,当然开发者如果有足够的条件,也是可以在本地部署开源大模型并且开放API接口进行调试的,这个并不冲突,本文只是为了方便演示效果,所以使用了智谱的大模型API接口,而智谱AI注册后,默认提供了一个18元的免费Token消费额度,因此接口的API-Key只需要注册一个即可快速获取。

3、工具包:Hutool

非常好用的一个基础工具包组件,封装了很多工具类方法,包含字符、文件、时间、集合等等

本文会使用到Hutool包的文本读取和切割方法。

4、向量数据库:ElasticSearch

向量数据库是RAG应用程序的基础中间件,所有的文本Embedding向量都需要存储在向量数据库中间件中进行召回计算,当然在Java领域并没有类似Python中numpy这类本地化工具组件包,即可快速实现矩阵计算等需求(PS:最近Java21的发布中,不仅仅只是虚拟线程等新特性,提供的向量API相信在未来AI领域,Java也会有一席之地的),所以选择了独立部署的中间件。

本文选择ElasticSearch可能对于Java开发人员也是比较熟悉的一个组件,毕竟ES在Java领域用途还是非常广的,只是可能很多开发者并不知道ElasticSearch居然还有存储向量数据的功能?

对于向量数据库中间件的选择,目前市面上有非常多的向量数据库,包括:MilvusQdrantPostgres(pgvector)Chroma 等等,Java开发者可以在熟悉当前流程后,根据自己的实际需求,选择符合企业生产环境的向量数据库。

5、LLM大模型ChatGLM-Std

为了演示方便,本文直接使用开放API接口的商业大模型,智谱AI提供的ChatGLM-Std

RAG工程的基本处理流程

在RAG检索增强生成领域中,最简单的核心处理流程架构图如下:

该架构图图是一个非常简单的流程图,在RAG领域中其实有非常多的处理细节,当我们深入了解后就会知道

我们后续根据该图来进行Java编码实现。

图1-RAG通用框架流程架构示意图

在RAG应用工程领域,其实整个程序的处理包含两部分:

  • 问答:对用户提问的问题通过向量Embedding模型处理,然后通过查询向量数据库(ElasticSearch)进行相似度计算获取和用户问题最相似的知识库段落内容,获取成功后,构建Prompt,最终发送给大模型获取最终的答案。
  • 数据处理:数据的处理是将用户私有的数据进行提取,包括各种结构化及非结构化数据(例如PDF/Word/Text等等),提取文本数据后进行分割处理,最终通过向量Embedding模型将这些分割后的段落进行向量化,最终向量数据存储在基础设施向量数据库组件中,以供后续的问答流程使用。

从图中我们可以知道,在我们所需要的大模型处于什么位置,以及它的作用,主要是两个模型的应用:

  • 向量Embedding模型:对我们本地知识的向量表征处理,将文本内容转化为便于计算机理解的向量表示
  • LLM问答大模型:大模型负责将我们通过语义召回的段落+用户的问题结合,构建的Prompt送给大模型以获取最终的答案,问答大模型在这里充当的角色是理解我们送给他的内容,然后进行精准回答

Java编码实践

我们理解了基础的架构流程,接下来就是编码实现了

环境准备

Java:JDK 1.8

ElasticSearch:7.16.1

对于ElasticSearch的安装,可以通过docker-compose在本地快速部署一个

编写docker-compose.yml配置文件,当前部署目录建data文件夹挂载数据目录

version: "3"
services:
  elasticsearch:
    image: elasticsearch:7.16.1
    ports:
      - "9200:9200"
      - "9300:9300"
    environment:
      node.name: es
      cluster.name: elasticsearch
      discovery.type: single-node  
      ES_JAVA_OPTS: -Xms4096m -Xmx4096m
    volumes:
      - ./data:/usr/share/elasticsearch/data
    deploy:
      resources:
        limits:
          cpus: "4"
          memory: 5G
        reservations:
          cpus: "1"
          memory: 2G
    restart: always

启动Es:docker-compose up -d

应用初体验

先来看整个程序的应用效果,通过Spring Shell环境下,程序启动后,如下图所示:

图2-程序启动效果

程序启动后,在命令行终端,我们可以看到一个可交互的命令行,此时,我们可以通过addchat两个命令完成图1中的整个流程

先使用add命令加载文档,在data目录下分别存储了001.txt002.txt两个文件,通过命令加载向量处理,如下图:

图3-ADD命令加载文件

当日志显示保存向量成功后,此时,我们即可以通过chat命令进行对话了,我们先来看看002.txt的文本主要说了什么内容?

data目录下的文本,开发者在调试时可以自己随意添加,网上随便找的文章都可以

图4-知识文本内容

文章内容是一篇非常具有代表性的时政人物介绍新闻,那么我们就根据该文章的内容进行问答!

问题1:苏州2022年全市的GDP是多少?

图5-问答调试

问题2:吉林省宣传部部长现在是谁?

图6-问答调试1

通过第一个问题,你是否可以发现问题呢?,如果你问ChatGPT一样的问题,它能准确回答吗?

以下是ChatGPT的回答

图7-ChatGPT回答内容

通过对比ChatGPT,开发者应该能看到一个基础的对比效果,主要体现:

  • 我们都知道ChatGPT大模型的内容日期截止到2021年,之后世界发生了什么,它并不知道,同类的GPT大模型也会出现一样的问题,因为训练大模型的代价是非常昂贵的,不可能按周、月,甚至是年的频率去更新大模型。
  • 基于现有的知识回答内容(RAG),能够有效的避免大模型胡说八道,而且回答的更精准

技术实现

进行问答体验后,我们来看具体的Java代码实现。

新建Spring Boot项目,工程目录如下:

GitHubhttps://github.com/xiaoymin/LlmInAction/tree/master/llm_chat_java_hello

图8-Java代码目录结构

从上文的RAG流程图中,我们知道了主要分两个步骤来实现,分别是数据的向量处理问答

由于是通过Spring Shell进行实现,因此这里我也分开,主要实现了两个Command命令:

  • add:在data目录下,为了演示需要,存放了两个txt内容,可以通过add file名称来实现文档的向量化流程加载处理,数据的处理开发者在实际的生产过程中可以通过定时任务、MQ消息等方式进行异步处理。
  • chat:通过命令chat 问题即可在Spring Shell的命令行终端进行对话,可以问data目录下相关的问题

为了方便后续的处理,程序启动时即会自动构建向量数据库的索引集合,代码如下:

/**
     * 初始化向量数据库index
     * @param collectionName 名称
     * @param dim 维度
     */
    public boolean initCollection(String collectionName,int dim){
        log.info("collection:{}", collectionName);
        // 查看向量索引是否存在,此方法为固定默认索引字段
        IndexOperations indexOperations = elasticsearchRestTemplate.indexOps(IndexCoordinates.of(collectionName));
        if (!indexOperations.exists()) {
            // 索引不存在,直接创建
            log.info("index not exists,create");
            //创建es的结构,简化处理
            Document document = Document.from(this.elasticMapping(dim));
            // 创建
            indexOperations.create(new HashMap<>(), document);
            return true;
        }
        return true;
    }

Es中的Index的Mapping结构如下:

图9-ES向量Index-Mapping结构

开发者需要注意vector字段,字段类型时dense_vector,并且指定向量维度为1024

向量维度的长度指定是和最终向量Embedding模型息息相关的,不同的模型有不同的维度,比如ChatGPT的向量模型维度是1536,百度文心一言也有368的,因此根据实际情况进行选择。

而这里因为我们选择的是智谱AI的向量模型,该模型返回的维度为1024,那么我们在向量数据库的维度就设置为1024

首先是add命令实现文档的向量化过程处理,代码如下:

@Slf4j
@AllArgsConstructor
@ShellComponent
public class AddTxtCommand {

    final TxtChunk txtChunk;
    final VectorStorage vectorStorage;
    final ZhipuAI zhipuAI;

    @ShellMethod(value = "add local txt data")
    public String add(String doc){
        log.info("start add doc.");
        // 加载
        List<ChunkResult> chunkResults= txtChunk.chunk(doc);
        // embedding
        List<EmbeddingResult> embeddingResults=zhipuAI.embedding(chunkResults);
        // store vector
        String collection= vectorStorage.getCollectionName();
        vectorStorage.store(collection,embeddingResults);
        log.info("finished");
        return "finished docId:{}"+doc;
    }
}

我们完全按照图1RAG的流程架构图进行代码的变现,主要的步骤:

1、加载指定的文档,并且将文档内容进行分割处理(按固定size大小进行分割处理),得到分割集合chunkResults,代码如下:

@Slf4j
@Component
@AllArgsConstructor
public class TxtChunk {

    public List<ChunkResult> chunk(String docId){
        String path="data/"+docId+".txt";
        log.info("start chunk---> docId:{},path:{}",docId,path);
        // 读取data目录下的文件流
        ClassPathResource classPathResource=new ClassPathResource(path);
        try {
            // 读取为文本
            String txt=IoUtil.read(classPathResource.getInputStream(), StandardCharsets.UTF_8);
            //按固定字数分割,256
            String[] lines=StrUtil.split(txt,256);
            log.info("chunk size:{}", ArrayUtil.length(lines));
            List<ChunkResult> results=new ArrayList<>();
            //此处给每个文档一个固定的chunkId
            AtomicInteger atomicInteger=new AtomicInteger(0);
            for (String line:lines){
                ChunkResult chunkResult=new ChunkResult();
                chunkResult.setDocId(docId);
                chunkResult.setContent(line);
                chunkResult.setChunkId(atomicInteger.incrementAndGet());
                results.add(chunkResult);
            }
            return results;
        } catch (IOException e) {
            log.error(e.getMessage());
        }
        return new ArrayList<>();
    }

}

2、将分块的集合通过智谱AI提供的向量Embedding模型进行向量化处理,代码实现如下:

/**
     * 批量
     * @param chunkResults 批量文本
     * @return 向量
     */
    public List<EmbeddingResult> embedding(List<ChunkResult> chunkResults){
        log.info("start embedding,size:{}",CollectionUtil.size(chunkResults));
        if (CollectionUtil.isEmpty(chunkResults)){
            return new ArrayList<>();
        }
        List<EmbeddingResult> embeddingResults=new ArrayList<>();
        for (ChunkResult chunkResult:chunkResults){
            //分别处理
            embeddingResults.add(this.embedding(chunkResult));
        }
        return embeddingResults;
    }

    public EmbeddingResult embedding(ChunkResult chunkResult){
       //获取智谱AI的开发Key
        String apiKey= this.getApiKey();
        // 初始化http客户端
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(20000, TimeUnit.MILLISECONDS)
                .readTimeout(20000, TimeUnit.MILLISECONDS)
                .writeTimeout(20000, TimeUnit.MILLISECONDS)
                .addInterceptor(new ZhipuHeaderInterceptor(apiKey));
        OkHttpClient okHttpClient = builder.build();
        EmbeddingResult embedRequest=new EmbeddingResult();
        embedRequest.setPrompt(chunkResult.getContent());
        embedRequest.setRequestId(Objects.toString(chunkResult.getChunkId()));
        // 智谱embedding模型接口
        Request request = new Request.Builder()
                .url("https://open.bigmodel.cn/api/paas/v3/model-api/text_embedding/invoke")
                .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), GSON.toJson(embedRequest)))
                .build();
        try {
            Response response= okHttpClient.newCall(request).execute();
            String result=response.body().string();
            ZhipuResult zhipuResult= GSON.fromJson(result, ZhipuResult.class);
            EmbeddingResult ret= zhipuResult.getData();
            ret.setPrompt(embedRequest.getPrompt());
            ret.setRequestId(embedRequest.getRequestId());
            return  ret;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

3、向量处理成功后,我们即可将向量数据存储在向量数据库中间件(ElasticSearch)中,调用vectorStorage.store处理,代码如下:

public void store(String collectionName,List<EmbeddingResult> embeddingResults){
        //保存向量
        log.info("save vector,collection:{},size:{}",collectionName, CollectionUtil.size(embeddingResults));

        List<IndexQuery> results = new ArrayList<>();
        for (EmbeddingResult embeddingResult : embeddingResults) {
            ElasticVectorData ele = new ElasticVectorData();
            ele.setVector(embeddingResult.getEmbedding());
            ele.setChunkId(embeddingResult.getRequestId());
            ele.setContent(embeddingResult.getPrompt());
            results.add(new IndexQueryBuilder().withObject(ele).build());
        }
        // 构建数据包
        List<IndexedObjectInformation> bulkedResult = elasticsearchRestTemplate.bulkIndex(results, IndexCoordinates.of(collectionName));
        int size = CollectionUtil.size(bulkedResult);
        log.info("保存向量成功-size:{}", size);
    }
}

至此,整个文本数据的Embedding处理就完成了。

数据处理完成后,接下来我们需要实现问答chat命令,来看代码实现:

@AllArgsConstructor
@Slf4j
@ShellComponent
public class ChatCommand {

    final VectorStorage vectorStorage;
    final ZhipuAI zhipuAI;

    @ShellMethod(value = "chat with files")
    public String chat(String question){
        if (StrUtil.isBlank(question)){
            return "You must send a question";
        }
        //句子转向量
        double[] vector=zhipuAI.sentence(question);
        // 向量召回
        String collection= vectorStorage.getCollectionName();
        String vectorData=vectorStorage.retrieval(collection,vector);
        if (StrUtil.isBlank(vectorData)){
            return "No Answer!";
        }
        // 构建Prompt
        String prompt= LLMUtils.buildPrompt(question,vectorData);
        zhipuAI.chat(prompt);
        // 大模型对话
        //return "you question:{}"+question+"finished.";
        return StrUtil.EMPTY;
    }

}

Chat命令主要包含的步骤如下:

1、将用户的问句首先通过向量Embedding模型转化得到一个多维的浮点型向量数组,代码如下:

/**
     * 获取句子的向量
     * @param sentence 句子
     * @return 向量
     */
    public double[] sentence(String sentence){
        ChunkResult chunkResult=new ChunkResult();
        chunkResult.setContent(sentence);
        chunkResult.setChunkId(RandomUtil.randomInt());
        EmbeddingResult embeddingResult=this.embedding(chunkResult);
        return embeddingResult.getEmbedding();
    }

2、根据向量数据查询向量数据库召回相似的段落内容,vectorStorage.retrieval方法代码如下:

public String retrieval(String collectionName,double[] vector){
        // Build the script,查询向量
        Map<String, Object> params = new HashMap<>();
        params.put("query_vector", vector);
        // 计算cos值+1,避免出现负数的情况,得到结果后,实际score值在减1再计算
        Script script = new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, "cosineSimilarity(params.query_vector, 'vector')+1", params);
        ScriptScoreQueryBuilder scriptScoreQueryBuilder = new ScriptScoreQueryBuilder(QueryBuilders.boolQuery(), script);
        // 构建请求
        NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder()
                .withQuery(scriptScoreQueryBuilder)
                .withPageable(Pageable.ofSize(3)).build();
        SearchHits<ElasticVectorData> dataSearchHits = this.elasticsearchRestTemplate.search(nativeSearchQuery, ElasticVectorData.class, IndexCoordinates.of(collectionName));
        //log.info("检索成功,size:{}", dataSearchHits.getTotalHits());
        List<SearchHit<ElasticVectorData>> data = dataSearchHits.getSearchHits();
        List<String> results = new LinkedList<>();
        for (SearchHit<ElasticVectorData> ele : data) {
            results.add(ele.getContent().getContent());
        }
        return CollectionUtil.join(results,"");
    }

这里主要利用了ElasticSearch提供的cosineSimilarity余弦相似性函数,计算向量得到相似度的分值,分值会在区间[0,1]之间,如果无限趋近于1那么代表用户输入的句子和之前我们存储在向量中的句子是非常相似的,越相似代表我们找到了语义相近的文档内容,可以作为最终构建大模型Prompt的基础内容。

向量矩阵的计算除了余弦相似性,还有IP点积、欧几里得距离等等,根据实际情况选择不同的算法实现。

3、向量召回Top3得到相似的语义文本内容后,我们就可以构建Prompt了,并且发送给大模型,Prompt如下:

public static String buildPrompt(String question,String context){
        return "请利用如下上下文的信息回答问题:" + "\n" +
                question + "\n" +
                "上下文信息如下:" + "\n" +
                context + "\n" +
                "如果上下文信息中没有帮助,则不允许胡乱回答!";
    }

而在构建Prompt时,我们可以遵循一个最简单的框架范式,RTF框架(Role-Task-Format)

  • R-Role:指定GPT大模型担任特定的角色
  • T-Task:任务,需要大模型做的事情
  • F-Format:大模型返回的内容格式(常规情况下可以忽略)

4、最后是调用大模型,实现sse流式调用输出,代码如下:

 public void chat(String prompt){
        try {
            OkHttpClient.Builder builder = new OkHttpClient.Builder()
                    .connectTimeout(20000, TimeUnit.MILLISECONDS)
                    .readTimeout(20000, TimeUnit.MILLISECONDS)
                    .writeTimeout(20000, TimeUnit.MILLISECONDS)
                    .addInterceptor(new ZhipuHeaderInterceptor(this.getApiKey()));
            OkHttpClient okHttpClient = builder.build();

            ZhipuChatCompletion zhipuChatCompletion=new ZhipuChatCompletion();
            zhipuChatCompletion.addPrompt(prompt);
            // 采样温度,控制输出的随机性,必须为正数
            // 值越大,会使输出更随机,更具创造性;值越小,输出会更加稳定或确定
            zhipuChatCompletion.setTemperature(0.7f);
            zhipuChatCompletion.setTop_p(0.7f);

            EventSource.Factory factory = EventSources.createFactory(okHttpClient);
            ObjectMapper mapper = new ObjectMapper();
            String requestBody = mapper.writeValueAsString(zhipuChatCompletion);
            Request request = new Request.Builder()
                    .url("https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_std/sse-invoke")
                    .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody))
                    .build();
            CountDownLatch countDownLatch=new CountDownLatch(1);
            // 创建事件,控制台输出
            EventSource eventSource = factory.newEventSource(request, new ConsoleEventSourceListener(countDownLatch));
            countDownLatch.await();

        } catch (Exception e) {
            log.error("llm-chat异常:{}", e.getMessage());
        }
    }

SSE流式的调用我们使用了okhttp-sse组件提供的功能快速实现。

好了,整个工程层面的Java代码实现就已经全部完成了。

最后

以上就是本片分享的全部内容了,通过Java开发语言,实现一个最小可用级别的RAG大模型应用!相信你看完本文后,也能够对AI大模型应用的开发有一个基本的了解。

如果你也在关注大模型、RAG检索增强生成技术,欢迎关注我,一起探索学习、成长~!

图10-微信公众号"八一菜刀"

附录

本文代码Github:https://github.com/xiaoymin/LlmInAction

智谱AI:https://open.bigmodel.cn/

与实战0-1,Java开发者也能看懂的大模型应用开发实践!!!相似的内容:

实战0-1,Java开发者也能看懂的大模型应用开发实践!!!

前言 在前几天的文章《续写AI技术新篇,融汇工程化实践》中,我分享说在RAG领域,很多都是工程上的实践,做AI大模型应用的开发其实Java也能写,那么本文就一个Java开发者的立场,构建实现一个最基础的大模型应用系统。 而大模型应用系统其实在目前阶段,可能应用最广的还是RAG领域,因此,本文也是通过

[转帖]自动化回归测试工具 —— AREX 上手实践

https://my.oschina.net/arextest/blog/8589156 AREX 是一款开源的自动化测试工具平台,基于 Java Agent 技术与比对技术,通过流量录制回放能力实现快速有效的回归测试。同时提供了接口测试、接口比对测试等丰富的自动化测试功能,无需编程能力也可快速上手

基于Protege的知识建模实战

一.Protege简介、用途和特点 1.Protege简介 Protege是斯坦福大学医学院生物信息研究中心基于Java开发的本体编辑和本体开发工具,也是基于知识的编辑器,属于开放源代码软件。这个软件主要用于语义网中本体的构建,是语义网中本体构建的核心开发工具,下面操作使用版本为5.5.0。 2.P

面试官:核心线程数为0时,线程池如何执行?

线程池是 Java 中用于提升程序执行效率的主要手段,也是并发编程中的核心实现技术,并且它也被广泛的应用在日常项目的开发之中。那问题来了,如果把线程池中的核心线程数设置为 0 时,线程池是如何执行的? 要回答这个问题,我们首先要了解在正常情况下,线程池的执行流程,也就是说当有一个任务来了之后,线程池

递归在多级数据结构中的简单应用

哈喽,我是小码,半年多没更新了,这段时间换了新工作,工作也很忙。后续会尽量多写点,坚持确实是一件很难,很酷的事情。最近在公司负责开发商品有关的开发,商品包含类型、款式等属性,而类型可能有一级类型、二级类型甚至是三级类型,针对这种多级分类,这就就不好使用简单的查询了。之前也写了一篇文章,Java递归实

奇葩需求记录 各个系统取数据联表展示

首先,我刚进公司没多长时间,然后介绍一下背景,这边是个工厂,上了很多个系统搞信息化,这边是有自己的研发团队的(C#),还做了一套系统出来搞生产管理。为了实现信息化呢,这边叫了很多个外包团队开发很多个系统,有些系统语言也不一样(java,C#,我甚至看到了jsp,不过也有springcloud),数据

网易面试:SpringBoot如何开启虚拟线程?

虚拟线程(Virtual Thread)也称协程或纤程,是一种轻量级的线程实现,与传统的线程以及操作系统级别的线程(也称为平台线程)相比,它的创建开销更小、资源利用率更高,是 Java 并发编程领域的一项重要创新。 PS:虚拟线程正式发布于 Java 长期支持版(Long Term Suort,LT

Java 断言 Assert 使用教程与最佳实践

本文收录于 Github.com/niumoo/JavaNotes,Java 系列文档,数据结构与算法! 本文收录于网站:https://www.wdbyte.com/,我的公众号:程序猿阿朗 作为一个 Java 开发者,如果要问你 Java 中有哪些关键字,你可能会随口说出一串,如果问你 Java

技术解密Java Chassis 3超实用的可观测性

本文分享自华为云社区《Java Chassis 3技术解密:实用的可观测性》,作者:liubao68。 狭义的可观测性,指日志、调用链和指标,广义的可观测性则包含更多的内容,一般的,应用程序暴露出来的便于理解其运行状态、运行轨迹、内部结构和功能集合的信息,都是可观测性的范围,本文只讨论狭义的可观测性

Java 抽象类和接口

抽象类与接口的理解、设计思路与实际用途 在面向对象的编程中,抽象类和接口是两个非常重要的概念,它们为开发者提供了创建可重用、可扩展和可维护代码的基础。下面我们将从理解、设计思路和实际用途三个方面来探讨抽象类和接口。 1. 抽象类(Abstract Class) 理解: 抽象类是一种不能被实例化的类,