JUC包下面的四大天王+线程池部分知识
一)Semphore:限流器用我就对了
Java中信号量Semphore是把操作系统原生的信号量封装了一下,本质就是一个计数器,描述了 可用资源的个数,主要涉及到两个操作
如果计数器为
0了,继续Р操作,就会出现阻塞等待的情况
P操作:申请一个可用资源,计数器-1V操作:释放一个可用资源,计数器+1停车场门口有一个灯牌,会显示停车位还剩余多少个,每进去一辆车,显示的停车位数量就-1,就相当于进行了一次P操作,每出去一辆车, 显示的停车位数量就+1,就相当于进行了一次V操作,而当停车场的剩余车位为0时,显示的停车位数量就为0了;
1)创建
Semaphore示例, 初始化为4, 表示有4个可用资源.
2)acquire方法表示申请资源(P操作),release方法表示释放资源(V操作)
public class Main{public static void main(String[] args) {Semaphore semaphore=new Semaphore(10);Runnable runnable=new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"开始申请资源");try {semaphore.acquire();System.out.println(Thread.currentThread().getName()+"已经获取到资源了");semaphore.release();System.out.println(Thread.currentThread().getName()+"开始释放资源了");} catch (InterruptedException e) {throw new RuntimeException(e);}}};for(int i=0;i<10;i++){Thread t=new Thread(runnable);t.start();}} }public class Main{public static int count=0;public static void main(String[] args) throws InterruptedException {Semaphore semaphore=new Semaphore(1);Thread t1=new Thread(()-> {for (int i = 0; i < 10000; i++) {try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t2=new Thread(()->{for(int i=0;i<10000;i++){try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);} }基于Semphore可以实现限流器:
什么是限流:比如说某一个广场,他的日载流量是6W,那么如果说假设有一天来了10W人,但是只能进去6W人,这个时候就只能排队入园了,因为工作人员始终会将人数控制在6W人
咱们再从生活中的事例回到程序当中,假设一个程序只能为 10W 人提供服务,突然有一天因为某个热点事件,造成了系统短时间内的访问量迅速增加到了 50W,那么导致的直接结果是系统崩溃,任何人都不能用系统了,显然只有少人数能用远比所有人都不能用更符合预期,因此这个时候要使用限流了
Semphore本身是依靠计数器的思想来进行实现的,它可以控制对于共享资源的访问数量,当线程需要访问该资源的时候,他必须先进行获取一个许可,就是从计数器中获取到资源
当计数器本身大于0的时候,线程可以获取到这个可用资源并且能够继续执行
当计数器本身等于0的时候,线程将会被阻塞,直到有其他的线程释放资源
Semphore本身有两个重要的操作,acquire()和realse()操作
1)当线程需要访问共享资源的时候,它会调用acquire方法来获取资源,如果计数器的值大于0,那么acquire()方法会将计数器的值减1,并且允许线程继续运行,如果计数器的值等于0,那么acquire()方法会使得线程阻塞,知道有其他线程释放资源
2)当线程使用完成共享资源以后,该线程可以调用realse方法来释放资源,realse()方法会使得计数器的值+1,表示有一个资源可以使用,其他被阻塞的线程可以有机会获得可用资源并且+1;
关于公平模式和非公平模式:
在这里面所谓的公平模式就是说线程调用acquire的先后顺序来获取到这个可用资源的,公平模式遵循先进先出原则,所以非公平模式是抢占式的,也就是说有可能一个新的获取线程恰好在一个许可证释放以后得到了这个许可证,但是这个已经获取许可证的线程前面还存在着一些其他的线程,当然在这里面非公平模式的性能比较高;
假设说,当有时候需要等待某一些线程执行完成了之后,再来执行主线程的代码,此时应该怎么做呢?可能有人会说,简单,用 join() 方法等待线程执行完成之后再执行主线程就行了,当然,如果使用的是 Thread 来执行任务,那这种写法也是可行的。然而真实的(编码)环境中我们是不会使用 Thread 来执行多任务的,而是会使用线程池来执行多任务,这样可以避免线程重复启动和销毁所带来的性能开销;
二)CountDownLatch:别急,等人齐了在开团
撞线:调用latch.countDown()
比赛结束,统计成绩:latch.await(),只要还存在着有任意的一个选手不进行撞线,那么比赛就无法结束,只有说所有的选手比赛撞了线,那么最终的比赛才可以结束
public class Main {public static void main(String[] args) throws InterruptedException {CountDownLatch latch=new CountDownLatch(10);for(int i=0;i<10;i++){Thread t=new Thread(()->{System.out.println("线程"+Thread.currentThread().getName()+"开始起跑");try {Thread.sleep(new Random().nextInt(10000));System.out.println("线程"+Thread.currentThread().getName()+"开始撞线");latch.countDown();} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}latch.await();System.out.println("比赛完成");} }1)使用CountDownLatch可以实现等待所有任务执行完成以后再来执行主任务的功能,他就是类似于说好像比赛中等待所有运动员都完成比赛以后再来公布排名一样,当然咱们在大玩着荣耀的时候也是一样,只有说所有人集合完毕以后在开团
2)而CountDownLatch就是通过计数器来实现等待功能的,当创建CountDownLatch的时候会创建一个大于0的计数器,每一次调用countDown()方法的时候计数器的值会减1,直到计数器的值变成0以后,等待的任务就可以继续执行了
3)countDownLatch在底层实现的时候是依靠内部创建并维护了一个voltaile的计数器,当调用countDown()方法的时候,会尝试将整数计数器-1,CountDownLatch 在创建的时候需要传入一个整数,在这个整数“倒数”到 0 之前,主线程需要一直挂起等待,直到其他的线程都执行之后,主线才能继续执行
public static void main(String[] args) throws InterruptedException {//创建CountDownLatch实现两个计数器CountDownLatch latch=new CountDownLatch(2);//创建线程池执行任务ExecutorService service= Executors.newFixedThreadPool(2);service.submit(new Runnable() {@Overridepublic void run() {System.out.println("我是线程池提交的第一个任务");latch.countDown();}});service.submit(new Runnable() {@Overridepublic void run() {System.out.println("我是线程池提交的第二个任务");latch.countDown();}});latch.await();System.out.println("线程池中的任务已经全部执行完成");}三)循环栅栏(Cycbarrier):人齐了老司机就可以发车了
循环栅栏实现一个可以循环利用的屏障
https://img-blog.csdnimg.cn/img_convert/f10e1adb034e3ebaa2c02b11596386ee.gif
1)CycliBarrier作用是让一组线程之间可以相互等待,当到达一个共同点的时候,所有之前等待的线程会冲破栅栏,一起向下执行
2)现在举个例子来说:伟哥要做末班车回家,公交车站的司机会等待车上面的所有乘客坐满以后再来发车,还有比如说王者荣耀,得等待5个队友游戏都加载完了才可以进入到游戏
3)本质上来说是让多个线程共同相互等待,知道说当所有的线程都到达了屏障点以后,之前的所有线程才可以继续向下执行,CycBarrier本身就象老司机开车一样,如果车上面还有空闲的座位,那么司机就得等着,只有说当作为坐满以后,老司机才发车
public static void main(String[] args) {CyclicBarrier barrier=new CyclicBarrier(10, new Runnable() {@Overridepublic void run() {System.out.println("现在司机上面的人都到齐了开始进行发车");System.out.println("当前线程池中的任务都已经执行完成了");}});ExecutorService service=Executors.newFixedThreadPool(10);for(int i=0;i<10;i++){service.submit(new Runnable() {@Overridepublic void run() {try {Thread.sleep(new Random().nextInt(5000));System.out.println("当前乘客开始上车"+Thread.currentThread().getName());barrier.await();//当前判断线程池中的任务执行完成可以执行多次System.out.println("当前线程下车"+Thread.currentThread().getName());} catch (InterruptedException e) {throw new RuntimeException(e);} catch (BrokenBarrierException e) {throw new RuntimeException(e);}}});}} }在CycliBarrier底层是基于计数器来实现的,当count不为0的时候,每一个线程在到达屏障点以后会先进行调用await()方法将自己阻塞,此时计数器会减1,此时这个线程会阻塞在这个屏障处,当循环栅栏的计数器被减为0的时候,所有调用await()的线程就会被唤醒,就会冲破栅栏,一起执行,CountDownLatch和CycliBarrier在底层都是依靠计数器来实现的,但是CountDownLatch只能使用一次,但是CycliBarrier却可以使用多次,这就是两者最大的区别
总结:CycliBarrier在底层是依靠ReentranLock来实现计数器的原子性更新的,CycliBarrier最常使用的就是await()方法,使用该方法就会将计数器的值减1,并判断当前的计数器是否为0,如果不是0就阻塞等待,并且当计数器变成0以后,该线程也就是阻塞在循环栅栏的线程才可以继续执行剩余任务;
三)线程池的状态:
1)Running状态:运行状态,线程池创建完成以后就进入到这个状态,如果不手动调用关闭方法,那么线程池在整个程序运行过程中都是这个状态;
2)ShutDown状态:关闭状态,线程池本身不再接受新任务的提交,但是会有先将线程池中已经存在的任务处理完成
3)Stop停止状态:不再接受新任务的提交,并且会中断正在执行的任务,放弃任务队列中已经存在的任务
4)tidying状态:整理状态,所有的任务都执行完成以后,也包括任务队列中的任务执行完成,当前线程池中的活动线程数降为0的状态,到达此状态以后会调用线程池的terminated方法
5)terminated状态:销毁状态,当调用线程池的terminated方法以后会进入到这个状态
ThreadPoolExecutor executor=new ThreadPoolExecutor(10, 10,100, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100), new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {return new Thread(r);}}){@Overrideprotected void terminated() {super.terminated();System.out.println("线程池终止");}};
1)当进行调用shutDown方法的时候,线程池中的状态会由Running状态到达shutDown状态,最后在到达tidying状态,最后到达terminated状态
2)当进行调用shutDownNow方法的时候,线程池中的状态会由running状态到达stop状态,最后在到达tidying状态,最后到达terminated状态
3)进行调用terminated方法,线程池会直接从tidying状态到达terminated状态,可以在阻塞队列的时候重写此方法,默认来说这个方法是空的
四)如何判断线程池中的任务都已经执行完成了?
1)在很多场景下,都希望等待线程池中的所有任务都执行完,然后再来执行下一步操作,对于Thread类来说,这样的实现是很简单的,加上一个join方法就解决了,但是对于线程池的判断就比较麻烦了
2)从上面的执行结果可以看出来,程序先打印了任务执行完成,再来继续打印并执行线程池的任务,这种执行顺序混乱的结果不是我们想要看到的,我们期望的结果就是等到鲜橙汁中的所有任务都执行完成了,再来进行打印线程池执行完成的信息;
3)产生少数问题的原因就是主线程main和线程池是并发执行的,所以说当线程池还没有执行完main现成的打印结果就已经执行了,想要解决这个问题就需要在打印结果之前,先判断线程池中的任务是否已经执行完成,如果没有执行完成就等到任务执行完成再来打印结果
public static void main(String[] args) {ExecutorService service=Executors.newFixedThreadPool(10);for(int i=0;i<10;i++){service.submit(new Runnable() {@Overridepublic void run() {System.out.println("开始执行线程池中的任务");}});}System.out.println("线程池中的所有任务执行完成");}
1)使用isTerminated()方法来判断:
1)使用线程池的终止状态来进行判断线程池中的任务是否已经全部执行完成,但是如果想要让线程之中的状态改变就需要调用shutDown()方法,不然线程池会一直处于Running运行状态那么就没有办法来进行判断是否处于终止状态来判断线程池中的任务是否已经全部执行
2)shutdown方法是启动线程池有序关闭的方法,它在关闭之前会执行完成所有已经提交的任务,并且不会再进行接收新的任务,当线程池中的所有任务都执行完成以后,线程池就处于终止状态了,此时isTerminated()方法返回的结果也就是true了;
缺点:需要关闭线程池
ThreadPoolExecutor executor=new ThreadPoolExecutor(10, 10,100, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100), new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {return new Thread(r);}}){@Overrideprotected void terminated() {super.terminated();System.out.println("线程池终止");}};executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务1");}});executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务2");}});executor.shutdown();while(!executor.isTerminated()){}System.out.println("线程池中的任务已经执行完成了");2)判断getCompletedTaskCount和getTaskCount是否相等
getTaskCount()返回执行计划任务的总数,但是因为本身任务和线程的状态都在不断地发生变化,因此返回的值是一个近似值;
getCompetedTaskCount()返回完成执行的任务总数,但是因为本身任务和线程的状态都在不断地发生变化,因此返回的值是一个近似值,但是在连续的调用中并不会减少
虽然不需要关闭线程池,但是可能会造成一定的误差
3)调用countDownLatch和CycliBarrier
需要注意的是countDownLatch中的countDown()方法和CycliBarrier中的await()方法需要在线程池的run方法的最后调用
4)使用FutureTask
FutureTask中的优势就是判断比较精准,调用每一个线程的FutureTask的get方法就是等待该任务执行完成的,需要使用submit进行提交:
public static void main(String[] args) throws ExecutionException, InterruptedException {ThreadPoolExecutor executor=new ThreadPoolExecutor(10,10,0,TimeUnit.SECONDS,new LinkedBlockingDeque<>(100));FutureTask<Integer> task1=new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int a=10;a++;System.out.println("a++完成");return a;}});FutureTask<Integer> task2=new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int b=11;b++;System.out.println("b++完成");return b;}});executor.submit(task1);executor.submit(task2);Integer result1= task1.get();Integer result2=task2.get();System.out.println("线程池中的任务都已经执行完成");}
五)submit和execute的区别:
1)接收到的参数不同:submit方法只能接受到runnable接口的任务,但是submit方法及可以接受到runnable方法的任务,也可以接收到callable,futureTask类型的任务,前者没有返回值,后者可以后返回值;
2)execute()的返回值是void,线程提交后不能得到线程的返回值,submit()的返回值是Future,通过Future的get()方法可以获取到线程执行的返回值,get()方法是同步的,执行get()方法时,如果线程还没执行完,会同步等待,直到线程执行完成
注意:虽然submit()方法可以提交Runnable类型的参数,但执行Future方法的get()时,线程执行完会返回null,不会有实际的返回值,这是因为Runable本来就没有返回值
相关文章:
JUC包下面的四大天王+线程池部分知识
一)Semphore:限流器用我就对了 Java中信号量Semphore是把操作系统原生的信号量封装了一下,本质就是一个计数器,描述了 可用资源的个数,主要涉及到两个操作 如果计数器为0了,继续Р操作,就会出现阻塞等待的情况 P操作:申…...
AGV系统控制位置管理功能
# ファイル: agv_locattion.py # 説明: AGV (Automated Guided Vehicle) の位置情報を管理し、UDPサーバーとして動作するGUIアプリケーションです。 # 必要なライブラリをインポート import tkinter as tk import socket import threading def AGV_handle_submit(canvas, st…...
JavaScript从入门到精通系列第三十三篇:详解正则表达式语法(二)
文章目录 一:正则表达式 1: 检查一个字符串中是否有. 2:第二种关键表达 3:第三种关键表达 编辑4:第四种关键表达 5:第五种关键表达 6:第六种关键表达 二:核心表达二 1&am…...
由于找不到 d3dx9_43.dll,无法继续执行代码。重新安装程序可能会解决此问题
电脑出现d3dx9_43.dll缺失的问题,通常是由于DirectX组件未安装或损坏导致的。为了解决这个问题,我为您提供了以下四个解决方法: d3dx9_43.dll解决方法1. 使用dll修复程序修复 首先,使用系统文件程序dll进行修复操作非常简单&…...
AI全栈大模型工程师(二十一)LangChain和SemanticKernel怎么选
LangChain 和 Semantic Kernel 怎么选? #%% md 划重点: 两者都值得学C#、JavaScript 和 Java 现在没得选做原型,首选 LangChain。功能多,开发快做产品,还是 SK 长期更可依赖建议只用 SK 的 Connectors 和 Plugins 能力…...
npm install 报错 chromedriver 安装失败的解决办法
npm install chromedriver --chromedriver_cdnurlhttp://cdn.npm.taobao.org/dist/chromedriver...
C语言--每日五道选择题--Day6
第一题 1、声明以下变量,则表达式: ch/i (f*d – i) 的结果类型为( ) char ch; int i; float f; double d; A: char B: int C: float D: double 答案及解析 D 基本数据类型的等级从低到高如下:char-> int-> long-> f…...
element-ui 封装 表格
一、封装表格组件 <template><el-table :data"list" :default-sort"{ prop: date }" style"width: 100%"><template v-for"item in tableColumn"><el-table-columnv-if"item.filters":prop"item…...
数据的使用、表关系的创建、Django框架的请求生命周期流程图
目录 一、数据的增删改查 1. 用户列表的展示 2. 修改数据的逻辑分析 3. 删除功能的分析 二、如何创建表关系 三、Django的请求生命周期流程图 一、数据的增删改查 1. 用户列表的展示 把数据表中得用户数据都给查询出来展示在页面上 查询数据 def userlist(request):&qu…...
Python基础教程:类--继承和方法的重写
嗨喽,大家好呀~这里是爱看美女的茜茜呐 什么是继承 继承就是让类与类之间产生父子关系,子类可以拥有父类的静态属性和方法 继承就是可以获取到另一个类中的静态属性和普通方法(并非所有成员) 在python中,新建的类可…...
Three.js提供了多种类型的灯光
Three.js提供了多种类型的灯光,包括环境光、点光源、平行光源和聚光灯。这些灯光可以用来照亮场景中的物体,使其看起来更加真实。 环境光(AmbientLight):环境光会均匀地照亮场景中的所有物体,没有方向,不能用来投射阴…...
精通Nginx(10)-负载均衡
负载均衡就是将前端过来的负载分发到两台或多台应用服务器。Nginx支持多种协议的负载均衡,包括http(s)、TCP、UDP(关于TCP、UDP负载均衡另文讲述)等。 目录 HTTP负载均衡 负载均衡策略 轮询 least_conn(最少连接) hash(通用哈希) ip_hash(IP 哈希) random(随…...
Hls学习(一)
1:CPU、DSP、GPU都算软件可编程的硬件 2:dsp在递归方面有所减弱,在递归方面有所增强,比如递归啊等,GPU可以同时处理多个进程,对于大块数据,流处理比较适用 3:为了提高运算量处理更多…...
Maven打包引入本地依赖包
Maven打包引入本地依赖包 SpringBoot 工程,Maven 在构建项目时,如何引入本地 Jar 包? 适合场景: 引用的依赖不在 Maven 仓库第三方公司提供的 SDK 包Maven 内网离线开发引入被定制改动过的 Jar 包 解决方法: 在 I…...
Docker常用命令及部署微服务项目
Docker常用命令及部署微服务项目 1、Docker常用命令 1、设置Yum源 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 2、安装docker yum -y install docker-ce 3、启动docker service docker start 4、验证 docker version 5…...
okhttp添加公共参数
在项目开发中很多时候后台都会给一些全局的公共入参,比如携带手机信息或者时间戳等字段。而我们在使用okhttp时,就需要我们单独就行二次封装处理了,对于请求全局参数,每次请求都要去写一次,那是肯定不行的。 所以就要我…...
基于SpringBoot的SSMP整合案例(开启日志与分页查询条件查询功能实现)
开启事务 导入Mybatis-Plus框架后,我们可以使用Mybatis-Plus自带的事务,只需要在配置文件中配置即可 使用配置方式开启日志,设置日志输出方式为标准输出mybatis-plus:global-config:db-config:table-prefix: tb_id-type: autoconfiguration:…...
android studio 修改图标
Android Studio 修改图标 简介 Android Studio 是一款由谷歌推出的用于开发 Android 应用程序的集成开发环境(IDE)。在开发过程中,我们可以根据自己的需求修改 Android Studio 的图标,以个性化我们的开发环境。 本文将介绍如何在…...
pytorch学习之第二课之预测温度
主要有以下几个步骤 第一:导入相应的工具包 第二:导入需要使用的数据集 第三:对导入的数据集输入进行预处理,找出特征与标签,查看数据特征的类型,判断是否需要标准化或者归一化处理 第四:构建神…...
基于Mahony互补滤波的IMU数据优化_学习笔记整理
这周自己被安排进行优化软件 IMU 姿态解算项目,之前自己只简单了解四元数,对IMU数据处理从未接触,通过这一周的学习感觉收获颇丰,在今天光棍节之际,,,用大半天的时间对这一周的收获进行整理&…...
React hook之useRef
React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...
渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
如何将联系人从 iPhone 转移到 Android
从 iPhone 换到 Android 手机时,你可能需要保留重要的数据,例如通讯录。好在,将通讯录从 iPhone 转移到 Android 手机非常简单,你可以从本文中学习 6 种可靠的方法,确保随时保持连接,不错过任何信息。 第 1…...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...
PH热榜 | 2025-06-08
1. Thiings 标语:一套超过1900个免费AI生成的3D图标集合 介绍:Thiings是一个不断扩展的免费AI生成3D图标库,目前已有超过1900个图标。你可以按照主题浏览,生成自己的图标,或者下载整个图标集。所有图标都可以在个人或…...


