【JavaEE】 多线程-初阶
多线程-初阶
1. 认识线程
1.1 概念
1) 线程是什么
-
多个线程组成了一个进程,线程好比是一跟光纤中的一个玻璃丝,进程是整根光纤。
-
一个进程中的线程共享这个进程中的资源(内存、硬盘)
2) 为什么需要线程
单核CPU发展出现瓶颈,想要再提高算力,只能增加CPU个数,并发编程就是利用多核CPU的绝佳方式.
使用进程也可以实现并发编程,只是进程重量大,创建销毁消耗资源多, 所以更好的方式是使用线程进行并发编程.
3) 线程和进程的区别
- 线程包含于进程
- 每个进程至少有一个线程, 即main线程(main thread)
- 进程之间互不干扰, 但是线程之间耦合度高(一个线程出现问题, 其他线程也会崩溃)
- 进程是系统分配资源的最小单位, 线程是系统调度的最小单位
4) Java中线程 和 操作系统线程 的关系
Java中线程是对于操作系统线程的封装和抽象.
1.2 第一个多线程程序
public class Main {public static void main(String[] args) {Runnable r1 = new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello thread1.");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};Thread t1 = new Thread(r1);Runnable r2 = new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello thread2.");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};Thread t2 = new Thread(r2);t1.start();t2.start();}
}
运行结果:
hello thread1.
hello thread2.
hello thread1.
hello thread2.
hello thread1.
hello thread2.
hello thread2.
hello thread1.
hello thread1.
hello thread2.
1.3 创建线程
1) 继承Thread
class MyThread extends Thread{public void run() {System.out.println("继承Thread得到");}
}
public class Demo1 {public static void main(String[] args) {MyThread t1 = new MyThread();t1.start();}
}
运行结果:
继承Thread得到
2) 实现Runnable接口
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("实现Runnable接口得到");}
}
public class Demo2 {public static void main(String[] args) {MyRunnable r1 = new MyRunnable();Thread t1 = new Thread(r1);t1.start();}
}
运行结果:
实现Runnable接口得到
继承Thread和实现Runnable接口的this指代的对象不同, 前者直接指代这个线程, 后者指代接口, 想要指代线程需要使用Thread.currentThread().
4) 使用匿名内部类
public class Demo3 {public static void main(String[] args) {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("使用Thread匿名内部类,直接传参new Runnable接口得到");}});Thread t2 = new Thread() {public void run() {System.out.println("使用Thread匿名内部类,直接重写run方法得到");}};t1.start();t2.start();}
}
运行结果:
使用Thread匿名内部类,直接传参new Runnable接口得到
使用Thread匿名内部类,直接重写run方法得到
5) 使用lamda表达式
public class Demo4 {public static void main(String[] args) {Thread t = new Thread(()-> {// 不需要重写run,lamda表达式就相当于是run方法System.out.println("lamda表达式创建得到");});t.start();}
}
运行结果:
lamda表达式创建得到
1.4 使用多线程编程可以增加程序的运行速度
但是可能导致程序线程不安全, 需要合理加锁.
2. Thread类常见方法
2.1 Thread常见构造方法
| 构造方法名 | 说明 |
|---|---|
| Thread() | 普通构造(仅分配空间) |
| Thread(Runnable) | 根据所给的run()构造对象 |
| Thread(String) | 为将构造出的线程进行命名 |
| Thread(Runnable, String) | 根据run()创建对象并命名 |
命名主要是为了方便调试.
2.2 Thread常见属性
| 方法名 | 作用 |
|---|---|
| start() | 创建线程并运行 |
| getId() | 返回线程的Id (这个Id不同于操作系统未进程分配的Id,也不是PCB中的Id,仅仅是JVM分配的Id) |
| getName() | 返回线程名字 |
| getPriority() | 返回优先级 |
| getState() | 返回线程目前的状态(NEW, RUNNABLE, WAITING, TIMED_WAITING,BLOCKED,TERMINATED) |
| isDaemon() | 判断是否为后台进程(后台进程不决定一个线程的存亡,只有前台进程才决定) |
| isAlive() | 判断是否存活 |
| isInterrupted() | 判断是否被中断 |
2.3 让一个Thread跑起来
使用start()方法即可使其开始运行.
之前写过的run方法, 只是为这个线程规定要怎么做, 只有start方法才能启动线程.
2.3.1 start 和 run 的区别
start会调用系统api进行创建线程
run只是一个普通的方法,告诉线程的执行逻辑,不会创建线程
2.4 中断一个线程
有两种方式:
- 设置一个记号,
线程A和线程B共享这个记号, 两个线程约定一个在其为true时工作, 一个在其为false时工作, 此时如果在A中对于这个记号进行更改, 那就能够使得B停止工作.
public class Demo5 {// 设置共同变量public static boolean flag = true;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{System.out.println("t1");// 在第一个线程执行完后暂停3秒try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}// 3秒后,设置共同变量为falseflag = false;});Thread t2 = new Thread(()->{while (flag == true) {System.out.println("t2");}});t1.start();// 在第二个线程执行前暂停2秒,让t1线程运行2秒Thread.sleep(2000);// 意味着t2只能执行1秒t2.start();}
}
运行结果:
t1
(等待3秒)
t2
t2
...
t2
t2
在3秒后,t1线程将共享变量修改为false, 所以t2被中断.
- 调用interrupt()进行通知
public class Demo6 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{while (!Thread.interrupted()) {System.out.println("t1尚未被中断");try {Thread.sleep(100);} catch (InterruptedException e) {System.out.println("t1收到中断信号");throw new RuntimeException(e);}break;}});System.out.println(t1.getState());t1.start();System.out.println(t1.getState());// 暂停2秒后进行中断Thread.sleep(1);t1.interrupt();}
}
运行结果:
NEW
RUNNABLE
t1尚未被中断
t1收到中断信号
Exception in thread "Thread-0" java.lang.RuntimeException: java.lang.InterruptedException: sleep interruptedat Demo6.lambda$main$0(Demo6.java:10)at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at Demo6.lambda$main$0(Demo6.java:7)... 1 more
interrupted() 和 currentThread().isInterrupted() 截然不同:
| 方法名 | 说明 |
|---|---|
| interrupted() | 查看当前线程是否被中断, 清除标记为false |
| currentThread().isInterrupted() | 查看当前线程是否中断,仅作判断, 不清除标记 |
演示:
public class Demo7 {public static void main(String[] args) {Thread t1 = new Thread(()->{for (int i = 0; i < 10; i++) {//System.out.println(Thread.interrupted());System.out.println(Thread.currentThread().isInterrupted());}});t1.start();t1.interrupt();} }运行结果: true true true true true true true true true true //(这种方法不清除中断标记, 仅作判断)
public class Demo7 {public static void main(String[] args) {Thread t1 = new Thread(()->{for (int i = 0; i < 10; i++) {System.out.println(Thread.interrupted());}});t1.start();t1.interrupt();} }运行结果: true false false false false false false false false //(这种方法清除中断标记, 恢复为未被中断状态)
2.5 等待一个线程
线程执行有先后顺序的时候**(线程A的执行需要依赖于线程B的执行结果), **那就需要使用join()方法, 这个方法能够保护当前的线程执行完毕后,其他线程才会去执行.
// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {public static int count = 0;// 1public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()-> {System.out.println("t11");for (int i = 0; i < 50000; i++) {count++;}System.out.println("t11");});Thread t2 = new Thread(()-> {System.out.println("t21");for (int i = 0; i < 50000; i++) {count++;}System.out.println("t21");});t1.start();t1.join();t2.start();Thread.sleep(100);System.out.println(count);}
}
在都对count进行++五万次的操作中,可以不加锁,也能使得count得到预期值的方法就是让t2在t1执行结束之后才启动,这样两个线程都能完成自己的任务,得到预期count。
2.5.1 方法中不能够加public、static等修饰词
访问局部变量的过程:对象-> 方法->局部变量。访问局部变量就已经有了访问权限的设定了。由此加修饰符也成了摆设。
对应static来说,因为static只能修饰成员变量和成员方法,在局部变量中用static修饰,又不能直接被类调用。
2.6 获取当前线程的引用
使用 Thread.currentThread();进行获取。
2.7 休眠线程
使用 Thread.sleep(long mills)实现。
在线程内部需要捕获异常,在方法中使用需要抛出异常。
Thread t2 = new Thread(()-> {System.out.println("t21");for (int i = 0; i < 50000; i++) {count++;}// 捕获异常try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}
}, "t2");
// 抛出异常
public static void main(String[] args) throws InterruptedException {
3. 线程的状态
3.1 线程的所有状态
NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了
使用isAlive()方法可以观察线程是否存活。
使用yield()方法会使线程重新排队。
4. 线程不安全
4.1 线程不安全发生的时机
在两个线程进行对于同一个变量进行修改的时候会出现线程不安全的问题。
4.2 线程不安全发生的原因
-
指令非原子性
即使是“++”这个操作,仅有一条语句,也是由3条微指令构成的:
-
从内存中读出count到寄存器
-
在寄存器中完成++
-
++后的值放进寄存器。
在多个线程的这三个操作如果相互穿插进行,那么就可能会读入”脏值“。
-
-
内存可见性
内存可见性,一个线程对共享变量值的修改,能够及时地被其他线程看到.
对于多次重复的读入同一个数据,编译器会对其进行优化,直接在寄存器中使用这份数据的拷贝值,不再从内存中进行读取,对这个变量的修改操作也都是在这个拷贝值身上完成,在这个线程使用完此变量后才会将最终值写进内存。
这种方式对于单线程来说是一种优化,简便了数据的读取操作,但是对于多线程来说,如果在线程A频繁修改变量count的同时,
线程B需要对count进行修改,那么就会读到“脏值”。
4.2.1 解决内存可见性问题
使用volatile关键字,忽略编译器对其的优化。
5. synchronized 关键字——解决线程不安全问题
synchronized 会将其所在的代码块进行加锁。
5.1 synchronized 特性
1) 互斥
如果说一个代码块相当于是一间房,那么一个synchronized就相当于是给这个房间进行上锁,其他人想进去必须要等到里面的人把锁打开,两个人进行争夺房间的使用权的过程也称为“锁竞争”。
锁的作用就是让不同的线程拥有同一个对象的锁的时候,只有执行顺序靠前的线程能够正常运行,后面的线程需要等待前面的线程释放锁以后才能继续正常运行。
2)刷新内存
底层实现:
synchronized的底层是使用操作系统的mutex lock实现的.
synchronized工作过程本质上是通过获取一个安全的空间来进行保证操作原子性的:
-
获得互斥锁
-
从主内存拷贝变量的最新副本到工作的内存 3.
-
执行代码
-
将更改后的共享变量的值刷新到主内存
-
释放互斥锁
3) 可重入性
public static final Object locker = new Object();synchronized (locker) {synchronized (locker) {}
}
在对于一个对象上同一把锁两次的时候,理论上来说会产生“死锁”现象。
因为一个第二把锁所在的代码块执行的前提是第一把锁释放,但是第一把锁释放的条件是后序的代码块执行完,形成闭环,造成“死锁”。
死锁的成因
1)互斥使用:同一把锁的不同线程同一时间只有一个能够运行
2)不可抢占:后面的线程只能等前面的将锁释放后才能运行
3)循环等待:在A阻塞等待B释放锁的时候,B在等待A释放锁
4)请求保持:一个线程尝试获取多把锁(线程A在已经被锁1加上的情况下获取一个已经被占用的锁2,那么锁1不会被释放)
1和2都是锁的基本特性,3和4是代码结构,当同时满足以上四点的时候才会发生死锁。
5.2 synchronized 使用示例
1)给普通方法上锁
synchronized public void method1() {}
2)给静态方法上锁
synchronized public static void method1() {}
3)给代码块上锁
给当前对象上锁
//3
public void method2() {synchronized (this) {}
}
给类对象上锁
//4
public void method3() {synchronized (Demo2.class) {}
}
其中,3和4等价。
5.3 Java 标准库中的线程安全类
不安全的:ArrayList 、LinkedList、 HashMap、 TreeMap、 HashSet、 TreeSet、 StringBuilder
安全的:Vector (不推荐使用)、 HashTable (不推荐使用) 、ConcurrentHashMap、 StringBuffer
6. volatile关键字
6.1 volatile能够保证内存可见性
内存可见性,一个线程对共享变量值的修改,能够及时地被其他线程看到.
import java.util.Scanner;// volatile的作用
public class Demo3 {//public volatile static int isQuit = 0;public static int isQuit = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()-> {while (isQuit == 0) {}System.out.println("t1退出");});t1.start();Thread.sleep(1000);Thread t2 = new Thread(()-> {System.out.println("请输入");Scanner scanner = new Scanner(System.in);isQuit = scanner.nextInt();});t2.start();Thread.sleep(1000);System.out.println(t1.getState());}
}
由于编译器的优化, t2对于isQuit变量进行修改并不影响t1线程中看到的isQuit变量是0, 这就叫做内存不可见.
但是如果加上volatile, 那么编译器会保证内存的可见性, 放弃优化.(所以会将代码的运行效率降低)
volatile的工作过程:
- 将内存中的数据放进寄存器
- 线程对于数据进行修改
- 将数据写回内存
如果是读取:
- 读取最新值进入工作内存
- 从工作内存中读取volatile变量的副本
6.2 volatile不能保证操作的原子性
volatile虽然一次性将数据读取到工作内存, 待其写完后又放回主内存, 但是在写的过程中, 如果其他线程也对同一个变量进行写入, 这将是合法的, 并且存在线程安全问题.
// 线程安全问题
public class Main {// 加上volatile并不能够得到预期的count值public static volatile int count = 0;// 1public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()-> {for (int i = 0; i < 50000; i++) {count++;}try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {count++;}}, "t2");t1.start();t2.start();Thread.sleep(1000);System.out.println(count);}
}
结果:
75377
6.3 但是synchronized可以保证内存可见性和原子性
相关文章:
【JavaEE】 多线程-初阶
多线程-初阶 1. 认识线程 1.1 概念 1) 线程是什么 多个线程组成了一个进程,线程好比是一跟光纤中的一个玻璃丝,进程是整根光纤。 一个进程中的线程共享这个进程中的资源(内存、硬盘) 2) 为什么需要线程 单核CPU发展出现瓶颈…...
小程序OCR身份证识别
使用两种OCR识别:小程序和腾讯云 1.基于微信小程序OCR插件实现身份证拍照、上传并OCR识别的示例: 首先,在小程序中添加身份证拍照的功能,可以使用wx.chooseImage()选择照片并使用wx.uploadFile()上传,代码如下&#…...
【算法学习】归并算法Merge Sort总结
归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。 1. 基本思想 归并排序使用分治思想,分治模式下每一层递归有三个步骤: 分解(divide)&a…...
Swager如何使用
Swager是一个API文档自动生成工具,可以用于生成API接口文档,供开发者和用户查看和使用。它可以通过描述API接口的规范,自动生成API文档,使得API接口的发布和使用变得更加简单和规范。 下面是使用Swagger的步骤: 首先…...
DHorse v1.4.2 发布,基于 k8s 的发布平台
版本说明 优化特性 在集群列表增加集群版本;修改Jvm的GC指标名; 解决问题 解决shell脚本换行符的问题;解决部署历史列表页,环境名展示错误的问题;解决指标收集功能的异常; 升级指南 升级指南 DHorse…...
Java使用JJWT令牌
最近在B站大学学习Java开发,刚好学到登入验证,在使用JJWT令牌时踩了一些坑,在这里把代码和依赖给出,希望后来者得以借鉴。 依赖 <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api&l…...
“第四十四天”
这道题也不是难,但可能会忽略一种情况,当最大小出现在首位的时候,那个时候如果进行交换的话,大小值可能出现覆盖的情况,最终导致丢失最大值或者最小值,比如最大值 10 在第一位,最小值 0 随意&am…...
Unity Mono和.Net平台浮点算法的区别
static void TestFloat(){{//float speed2.0f/20;float speed 0.1f;float distance 2.0f;long needTime (long)(distance / speed);Log.Debug($"needTime{needTime}"); #if UNITY_EDITORif (needTime ! 19) #elseif (needTime ! 20)//.Net服务器和安卓手机 #endif…...
【SA8295P 源码分析 (二)】64 - QNX 与 Android GVM 显示 Dump 图片方法汇总
【SA8295P 源码分析】64 - QNX 与 Android GVM 显示 Dump 图片方法汇总 一、QNX侧1.1 surfacedump 功能1.2 screenshot 功能二、Android GVM 侧2.1 screencap -p 导出 PNG 图片2.2 screencap 不加 -p 参数,导出 RGB32 图片2.3 dumpsys SurfaceFlinger --display-id 方法系列文…...
shell命令以及运行原理和lLinux权限
shell命令以及运行原理 什么是shell shell是操作系统的外壳程序统称,我们是通过shell去和操作系统沟通的。 从技术角度,shell最简单的定义就是命令行解释器,主要包含两个功能: 将使用者的命令翻译给核心处理 将核心的处理结果…...
斯坦福JSKarel编程机器人使用介绍
斯坦福JSKarel编程机器人使用介绍 为了避免被编程语言固有的复杂性所困扰,有一个被称为卡雷尔(Karel)机器人的微型世界(microworld)的简化环境,可以让编程初学者从中学习理解编程的基本概念,而…...
SpringBoot中pom.xml不引入依赖, 怎么使用parent父项目的依赖
在Spring Boot项目中,如果你想使用父项目的依赖,而不想在pom.xml中显式引入依赖,你可以使用Maven的继承机制。 首先,确保你的Spring Boot项目是一个子项目,即它继承自一个父项目。要实现这一点,在pom.xml文…...
基于vue3+ts5+vue-router4+pinia2的PC端项目搭建教程
导语:在日常开发中,有时候会在项目中引入 ts 来解决一些 js 的问题,下面就简单介绍一下如何使用 vue3tsrouterpinia 来搭建一个项目。 目录 简介创建安装配置实战 简介 vue3 目前是常用的 vue 版本,提供了组合式 API 以及一些新…...
6个无版权、免费、高清图片素材库
找免费无版权图片素材,就上这6个网站,超高质量,可商用,赶紧收藏! 1、菜鸟图库 https://www.sucai999.com/pic.html?vNTYwNDUx 网站主要为新手设计师提供免费素材,这些素材的质量都很高,类别也…...
什么是响应式设计?响应式设计的基本原理是什么?如何兼容低版本的 IE?
什么是响应式设计: 响应式设计(Responsive Design)是一种Web设计和开发方法,旨在使网站在不同设备和屏幕尺寸上都能提供一致的用户体验。响应式设计的目标是适应多种终端,包括桌面计算机、笔记本电脑、平板电脑和移动设备&#x…...
LeetCode 2906. 构造乘积矩阵【前后缀分解,数组】中等
本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章…...
vue3+koa+axios实现前后端通信
vue3koaaxios实现前后端通信 写了一个小demo来实现前后端通信,涉及跨域问题,非常简单可以给大家平时开发的时候参考 服务端: 目录结构如下: router index.js // router的入口文件 // 引入路由 const Router require("koa-router&quo…...
Required MultipartFile parameter ‘file‘ is not present
出现这个原因我们首先想到的是加一个RequestParam("file"),但是还有可能的原因是因为我们的名字有错误 <span class"input-group-addon must">模板上传 </span> <input id"uploadFileUpdate" name"importFileU…...
vue3后台管理系统之layout组件的搭建
1.1静态布局 <template><div class"layout_container"><!-- 左侧导航 --><div class"layout_slider"></div><!-- 顶部导航 --><div class"layout_tabbar"></div><!-- 内容展示区 --><…...
Minio 文件上传(后端处理同文件判断,同一文件秒传)
记录minio 文件上传 MinIO提供多个语言版本SDK的支持,下边找到java版本的文档: 地址:https://docs.min.io/docs/java-client-quickstart-guide.html maven依赖如下: XML <dependency><groupId>io.minio</groupId…...
Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...
汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...
rnn判断string中第一次出现a的下标
# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...
Yolov8 目标检测蒸馏学习记录
yolov8系列模型蒸馏基本流程,代码下载:这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中,**知识蒸馏(Knowledge Distillation)**被广泛应用,作为提升模型…...
从 GreenPlum 到镜舟数据库:杭银消费金融湖仓一体转型实践
作者:吴岐诗,杭银消费金融大数据应用开发工程师 本文整理自杭银消费金融大数据应用开发工程师在StarRocks Summit Asia 2024的分享 引言:融合数据湖与数仓的创新之路 在数字金融时代,数据已成为金融机构的核心竞争力。杭银消费金…...
LangFlow技术架构分析
🔧 LangFlow 的可视化技术栈 前端节点编辑器 底层框架:基于 (一个现代化的 React 节点绘图库) 功能: 拖拽式构建 LangGraph 状态机 实时连线定义节点依赖关系 可视化调试循环和分支逻辑 与 LangGraph 的深…...
jdbc查询mysql数据库时,出现id顺序错误的情况
我在repository中的查询语句如下所示,即传入一个List<intager>的数据,返回这些id的问题列表。但是由于数据库查询时ID列表的顺序与预期不一致,会导致返回的id是从小到大排列的,但我不希望这样。 Query("SELECT NEW com…...
