java学习--多线程
多线程

了解多线程
多线程是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
并发和并行
- 并行:在同一时刻,有多个指令在CPU上同时执行
- 并发:在同一时刻,有多个指令在CPU上交替执行
进程和线程
进程:正在运行的软件
- 独立性:进程是一个独立运行的基本单位,同时也是系统分配调度资源的独立单位。
- 动态性:进程的实质就是程序的一次执行过程,动态产生,动态消亡。
- 并发性:任何进程都可以和其他进程并发执行
线程:是进程中单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则成为多线程程序
多进程的实现方案
- 继承Thread类的方式进行实现
- 定义一个类继承Thread类
- 在定义的类中重写run()方法
- 创建自定义类的对象
- 启动线程
- 实现Runable接口的方式实现
- 自定义一个类实现Runable接口
- 在自定义类中重写run()方法
- 创建自定义对象
- 创建Tread类对象,把自定义对象作为构造方法的参数
- 启动线程
- 利用Callable和Future接口方式实现
- 定义一个类实现Callable接口
- 在该类中重写call方法
- 创建自定义类的对象
- 创建Future的实现类FutureTask对象,把自定义类对象作为构造方法的参数
- 创建Tread类的对象,把FutureTask对象最为构造方法的参数
- 启动线程
三种方式的对比
| 优点 | 缺点 | |
|---|---|---|
| 实现Runnable,Callable接口 | 扩展性强,实现该接口的同时还可以继承其他类 | 编程相对复杂,不能直接使用Thread类中的方法 |
| 继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可扩展性差,不能继承其他类 |
使用getName()获取当前正在执行现成的名称
使用setName()给当前线程设置名称,也可使用子类的带参构造方法设置名称
使用Thread的Sleep()方法 使得线程睡眠特定的时间
实现Runnable结构创建多线程程序的好处
- 避免了单继承的局限性
- 一个类只能继承一个类,自定义类继承了Thread类就不能继承其他类
- 实现了Runnable接口,还可以继承其他类,实现其它接口
- 增强了程序的扩展性,降低了程序的耦合性
- 实现了Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
- 实现类中,重写了run方法,用来设置线程任务
- 创建Thread类对象,调用start方法,用来开启新的线程
线程安全问题
解决措施:
使用同步机制解决
方法一:同步代码块
syncheonized(对象){代码块
}
方法二:同步方法
使用步骤:
- 把访问了共享数据的代码提取出来,放到一个方法中
- 在方法上添加synchronized修饰符
- 格式
修饰符 synchronized 返回值类型 方法名(参数列表){可能会出现安全问题的代码(访问了共享数据的代码)
}
线程池
线程池可以看成是一个池子,这个池子中存储很多个线程
系统创建一个线程的成本是很高的,因为他涉及到与操作系统的交互,当程序需要创建大量生存期很短暂的线程是,频繁的创建何晓辉线程对系统的资源消耗可能大于业务处理对线程的消耗。为了提高性能,我们可以采用线程池。
线程池在启动时,会创建大量空闲线程,当我们向线程池提价搜任务是,线程池就会启动一个线程来执行该任务。等待任务执行完毕,线程并不会死亡,而是咋次返回到线程池中称为空闲状态,等待哦下一次任务的执行。
线程池的设计思路
- 准备一个任务容器
- 一次性启动多个消费者线程
- 刚开始任务容器是空的,所有线程都在等待
- 知道一个外部线程向这个任务容器扔了一个“任务”,就会有一个消费者线程被唤醒
- 这个消费者线程取出任务,并执行任务,执行完毕后,继续等待下一次任务的到来
线程池-Executors默认线程池
概述:在开发中我们使用JDK中自带的线程池
我们可以使用Excutors中所提供的的静态方法来创建线程池
static ExcutorsService newCachedThreadPool()创建一个默认线程池
static newFixedThreadPool(int nThreads)创建一个指定最多线程数量的线程池
代码实现
ackage practise2.Exam2;import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;//static ExecutorsService new CachedThreadPool() 创建一个默认的线程池
//static newFixedThreadsPool(int nThreads) 创建一个指定最多线程数量的线程池
public class Exam1 {public static void main(String[] args) {//创建一个默认线程池对象,池子是空的,默认最多可以容纳int类型的最大值ExecutorService executorService= Executors.newCachedThreadPool();//Executors ---可以帮助我们创建线程池对象//ExecutorService ---可以帮助我们控制线程池executorService.submit(()->{System.out.println(Thread.currentThread().getName()+"在执行了");});executorService.submit(()->{System.out.println(Thread.currentThread().getName()+"在执行了");});executorService.shutdown();}
}
线程池-Executors创建指定上限的线程池
使用Executors中所提供的静态方法来创建线程池
static ExecutorsService newFixcedThreadPool(int nThread):创建一个指定最多线程数量的线程池
代码实现:
package practise2.Exam2;import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;//static ExecutorService newFixedThreadPool(int nThread):创建一个指定最多线程数量的线程池
public class Exam2 {public static void main(String[] args) {//参数不是初始值而是最大值ExecutorService executorService= Executors.newFixedThreadPool(10);ThreadPoolExecutor pool= (ThreadPoolExecutor) executorService;System.out.println(pool.getPoolSize()); //0executorService.submit(()->{System.out.println(Thread.currentThread().getName()+"在执行了");});executorService.submit(()->{System.out.println(Thread.currentThread().getName()+"在执行了");});System.out.println(pool.getPoolSize());//2//executorService.shutdown();}
}
线程池-ThreadPoolExecutor
创建线程池对象:
ThreadPoolExecutors threadPoolExecutor =new ThreadPoolExecutor(核心线程数连发,最大线程数量,空闲线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
代码实现
package practise2.Exam2;import pracise1.exam5.MyRunnable;import java.util.concurrent.*;public class Exam3 {public static void main(String[] args) {//参数一:核心线程数量//参数二:最大线程数//参数三:空闲线程最大存活时间//参数四:时间单位//参数五:任务队列//参数六:创建线程工厂//参数七:人物的拒绝策略ThreadPoolExecutor pool =new ThreadPoolExecutor(2,5,2, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());pool.submit(new MyRunnable());pool.submit(new MyRunnable());pool.shutdown();}
}
线程池-参数详解
创建线程池对象
ThreadPoolExecutor threadPoolExecutor =new ThreadPoolExecutor(
核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略
)
| 参数 | 含义 | 限制 |
|---|---|---|
| 参数一 | 核心线程数量 | 不能小于0 |
| 参数二 | 最大线程数 | 不能小于等于0,最大线程数大于等于核心线程数 |
| 参数三 | 空闲线程最大存活空间 | 不能小于0 |
| 参数四 | 时间单位 | 时间单位 |
| 参数五 | 任务队列 | 不能为null |
| 参数六 | 创建线程工厂 | 不能为null |
| 参数七 | 人物的拒绝策略 | 不能为null |
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
corePoolSize: 核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime: 空闲线程最大存活时间,不能小于0
unit: 时间单位
workQueue: 任务队列,不能为null
threadFactory: 创建线程工厂,不能为null
handler: 任务的拒绝策略,不能为null
线程池-非默认任务拒绝策略
RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,他下面存在4个子类。
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor。CallerRunsPolicy: 调佣任务的run()方法绕过线程池直接执行
注:明确线程池对多可执行的任务数=队列容量+最大线程数
package practise2.Exam2;import java.util.concurrent.*;public class Exam {public static void main(String[] args) {
// ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(1,3,20,
// TimeUnit.SECONDS,new ArrayBlockingQueue<>(1),
// Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());//提交5个任务,而该线程池最多可以处理4个任务,当我们使用ABortPOlicy这个任务处理策略是后,就会抛出异常ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(1,3,20,TimeUnit.SECONDS,new ArrayBlockingQueue<>(1),Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());//提交了5个任务for (int i = 0; i < 5; i++) {//定义一个变量,来制定当前执行的任务,这个变量需要被final修饰final int y=i;threadPoolExecutor.submit(()->{//System.out.println(Thread.currentThread().getName()+"------>>执行了任务");System.out.println(Thread.currentThread().getName()+"---->>执行了任务"+y);});}}
}
package practise2.Exam2;import java.util.concurrent.*;public class Exam5 {public static void main(String[] args) {ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(1,3,20, TimeUnit.SECONDS,new ArrayBlockingQueue<>(1),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());//提交五个任务for (int i = 0; i < 5; i++) {threadPoolExecutor.submit(()->{System.out.println(Thread.currentThread().getName()+"---->>执行了任务");});}}
}
通过控制台的输出,我们可以看次策略没有通过线程池中得到线程执行任务,而是直接调用任务的run()方法绕过线程池直接执行。
原子性
volatile-问题
代码分析:
volatile解决
以上案例出现的问题:
当A线程修改了共享数据时,B线程没有及时获取道最新的值,如果还在使用原先的值,就会出现问题。
- 堆内存是唯一的,每一个线程都有自己的线程栈
- 每一个线程在使用堆内存里面的变量时,都会先拷贝一份到变量的副本中
- 在线程中,每一次使用是从变量的副本中获取的
volatile关键字:强制线程在每一次使用时,都会看一下公共区域最新的值。
public class Money {public static volatile int money=100000;
}
public class MyThread1 extends Thread{@Overridepublic void run() {while (Money.money==100000){}System.out.println("结婚基金已经不是十万了");}
}
public class MyThread2 extends Thread{@Overridepublic void run() {try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}Money.money=90000;}
}
public class Demo {public static void main(String[] args) {MyThread1 myThread1=new MyThread1();myThread1.setName("小垃圾");myThread1.start();MyThread2 myThread2=new MyThread2();myThread2.setName("小趴菜");myThread2.start();}}
synchronized解决
- 线程获得锁
- 清空变量副本
- 拷贝共享最新的值到变量副本中
- 执行代码
- 将修改后变量副本中的值赋值给共享数据
- 释放锁
代码实现
原子性
概述:在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可分割的整体。
Volatile关键字不能保证原子性
解决方案:我们可以给count++操作添加锁,那么count++操作就是临界区中的代码,临界区中的代码一次只能被一个线程去执行,所以count++就变成了的原子操作。
原子性—AtomicInteger
概述:java从JDK1.5开始提供了java.uyil.concurrent.atomic包(简称Atomic包),这个包中的原子提供了一种用法简单,性能高效,线程安全地根新一个变量的方式,因为变量的类型有很多个,所以在Atomic包中一共提供了13个类,属于4种类型的原子更新方式,分别是:
原子更新基本类型,原子更新数组,原子更新引用和原子更行属性(字段)
使用原子的方式甘心基本类型,使用原子的方式更新基本类型Atomic包提供了一下3个类:
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型
以上三个类提供的方法几乎一模一样,以AtomicInteger为例讲解
public AtomicInteger(); //初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): //初始化一个指定值的原子型Integerint get(); //获取值
int getAndIncrement(); //以原子的方式将当前值加1,注意,这里返回的是自增前的值
int incrementAndGet(); //以原子的方式将当前值加1,注意,这里返回的是自增前的值
int addAndGet(int data): //以原子的方式将输入的数值与示例中的值(AtomicInteger里的value)相加,并返回结果。
int getandSet(int value): //以原子方式设置为newValur的值,并返回旧值
代码实现:
AtomicInteger-内存解析
AtomicInteger原理:
自旋锁+CAS算法
CAS算法:
有三个操作数(内存值V,旧的预期值A,要修改的值B)
当旧的预期值A==内存值 此时修改成功,将V改为B
当旧的预期值A!=内存值,此时修改失败,不做任何操作
并重新获取现在的最新值(这个重新获取的动作就是自旋)
AtomicInteger-源码解析
代码实现:
源码解析
乐观锁和悲观锁
synchronized和CAS的区别
相同点:
在多线程的情况下,都可以保证共享数据的安全性
不同点:
synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每次操作共享数据之前,都会上锁。(悲观锁)
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁,只不过在修改共享数据的时候,会检查一下。
如果别人修改过,则获取最新数据。
如果别人没有修改过,那么我们直接修改共享数据的值(乐观锁)
并发工具类
并发 工具类-Hashtable
Hashtable出现的原因:
在集合类中HashMap是比较常用的集合类对象,但是HashMap是现成不安全的(多线程环境下可能会存在问题)。为了保证数据得到安全性我们可以使用Hashtable,但是Hashtable的效率低下。
代码实现:
并发工具类-ConcurrentHashMap基本使用
ConcurrentHashMap出现的原因
在集合类中HashMap是比较常用的集合对象,但是hashMap是线程不安全的多线程环境下可能会存在的问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下,基于以上两个原因我们可以使用JDK1.5以后提供的ConcurrentHashMap。
体系结构
ConcurrentHashMap
Map接口
- HashMap
- Hashtable
- TreeMap
- ConcurrentMap
总结:
- HashMap是线程不安全的,多线程环境下有数据安全问题
- hashtable 是线程安全的,但是会将整张表锁起来,效率低下‘
- ConcurrentHashMap也是线程安全的,效率高,在JDK7和JDK8中,底层原理不同
相关文章:
java学习--多线程
多线程 了解多线程 多线程是指从软件或者硬件上实现多个线程并发执行的技术。 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。 并发和并行 并行:在同一时刻,有多个指令在CPU上同时执行并发࿱…...
90后阿里P7技术专家晒出工资单:狠补了这个,真香...
最近一哥们跟我聊天装逼,说他最近从阿里跳槽了,我问他跳出来拿了多少?哥们表示很得意,说跳槽到新公司一个月后发了工资,月入5万多,表示很满足!这样的高薪资着实让人羡慕,我猜这是税后…...
2023美赛C题:Wordle筛选算法
Wordle 规则介绍 Wordle 每天会更新一个5个字母的单词,在6次尝试中猜出单词就算成功。每个猜测必须是一个有效的单词(不能是不能组成单词的字母排列)。 每次猜测后,字母块的颜色会改变,颜色含义如下: 程…...
SpringBoot 集成 Kafka
SpringBoot 集成 Kafka1 安装 Kafka2 创建 Topic3 Java 创建 Topic4 SpringBoot 项目4.1 pom.xml4.2 application.yml4.3 KafkaApplication.java4.4 CustomizePartitioner.java4.5 KafkaInitialConfig.java4.6 SendMessageController.java5 测试1 安装 Kafka Docker 安装 Kafk…...
OpenCV 图像金字塔算子
本文是OpenCV图像视觉入门之路的第14篇文章,本文详细的介绍了图像金字塔算子的各种操作,例如:高斯金字塔算子 、拉普拉斯金字塔算子等操作。 高斯金字塔中的较高级别(低分辨率)是通过先用高斯核对图像进行卷积再删除偶…...
【自学Linux】Linux一切皆文件
Linux一切皆文件 Linux一切皆文件教程 Linux 中所有内容都是以文件的形式保存和管理的,即一切皆文件,普通文件是文件,目录是文件,硬件设备(键盘、监视器、硬盘、打印机)是文件,就连套接字&…...
CUDA C++扩展的详细描述
CUDA C扩展的详细描述 文章目录CUDA C扩展的详细描述CUDA函数执行空间说明符B.1.1 \_\_global\_\_B.1.2 \_\_device\_\_B.1.3 \_\_host\_\_B.1.4 Undefined behaviorB.1.5 __noinline__ and __forceinline__B.2 Variable Memory Space SpecifiersB.2.1 \_\_device\_\_B.2.2. \_…...
为什么重写equals必须重写hashCode
关于这个问题,看了网上很多答案,感觉都参差不齐,没有答到要点,这次就记录一下! 首先我们为什么要重写equals?这个方法是用来干嘛的? public boolean equals (Object object&#x…...
< 每日小技巧:N个很棒的 Vue 开发技巧, 持续记录ing >
每日小技巧:6 个很棒的 Vue 开发技巧👉 ① Watch 妙用> watch的高级使用> 一个监听器触发多个方法> watch 监听多个变量👉 ② 自定义事件 $emit() 和 事件参数 $event👉 ③ 监听组件生命周期常规写法hook写法ὄ…...
数据结构与算法之二分查找分而治之思想
决定我们成为什么样人的,不是我们的能力,而是我们的选择。——《哈利波特与密室》二分查找是查找算法里面是很优秀的一个算法,特别是在有序的数组中,这种算法思想体现的淋漓尽致。一.题目描述及其要求请实现无重复数字的升序数组的…...
训练自己的中文word2vec(词向量)--skip-gram方法
训练自己的中文word2vec(词向量)–skip-gram方法 什么是词向量 将单词映射/嵌入(Embedding)到一个新的空间,形成词向量,以此来表示词的语义信息,在这个新的空间中,语义相同的单…...
ubuntu系统环境配置和常用软件安装
系统环境 修改文件夹名称为英文 参考链接 export LANGen_US xdg-user-dirs-gtk-update 常用软件安装 常用工具 ping 和ifconfig工具 sudo apt install -y net-tools inetutils-ping 截图软件 sudo apt install -y net-tools inetutils-ping flameshot 录屏 sudo apt-get i…...
【1139. 最大的以 1 为边界的正方形】
来源:力扣(LeetCode) 描述: 给你一个由若干 0 和 1 组成的二维网格 grid,请你找出边界全部由 1 组成的最大 正方形 子网格,并返回该子网格中的元素数量。如果不存在,则返回 0。 示例 1&#…...
windows11安装sqlserver2022报错
window11安装SQL Server 2022 报错 糟糕… 无法安装SQL Server (setup.exe)。此 SQL Server安装程序介质不支持此OS的语言,或没有SQL Server英语版本的安装文件。请使用匹配的特定语言SQL Server介质;或安装两个特定语言MUI,然后通过控制面板的区域设置…...
Python快速上手系列--日志模块--详解篇
前言本篇主要说说日志模块,在写自动化测试框架的时候我们就需要用到这个模块了,方便我们快速的定位错误,了解软件的运行情况,更加顺畅的调试程序。为什么要用到日志模块,直接print不就好了!那得写多少print…...
【THREE.JS学习(1)】绘制一个可以旋转、放缩的立方体
学习新技能,做一下笔记。在使用ThreeJS的时候,首先创建一个场景const scene new THREE.Scene();接着,创建一个相机其中,THREE.PerspectiveCamera()四个参数分别为:1.fov 相机视锥体竖直方向视野…...
数仓实战 - 滴滴出行
项目大致流程: 1、项目业务背景 1.1 目的 本案例将某出行打车的日志数据来进行数据分析,例如:我们需要统计某一天订单量是多少、预约订单与非预约订单的占比是多少、不同时段订单占比等 数据海量 – 大数据 hive比MySQL慢很多 1.2 项目架…...
python虚拟环境与环境变量
一、环境变量 1.环境变量 在命令行下,使用可执行文件,需要来到可执行文件的路径下执行 如果在任意路径下执行可执行文件,能够有响应,就需要在环境变量配置 2.设置环境变量 用户变量:当前用户登录到系统,…...
BeautifulSoup文档4-详细方法 | 用什么方法对文档树进行搜索?
4-详细方法 | 用什么方法对文档树进行搜索?1 过滤器1.1 字符串1.2 正则表达式1.3 列表1.4 True1.5 可以自定义方法2 find_all()2.1 参数原型2.2 name参数2.3 keyword 参数2.4 string 参数2.5 limit 参数2.6 recursive 参数3 find()4 find_parents()和find_parent()5…...
初识Tkinter界面设计
目录 前言 一、初识Tkinter 二、Label控件 三、Button控件 四、Entry控件 前言 本文简单介绍如何使用Python创建一个界面。 一、初识Tk...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...
【Go语言基础【12】】指针:声明、取地址、解引用
文章目录 零、概述:指针 vs. 引用(类比其他语言)一、指针基础概念二、指针声明与初始化三、指针操作符1. &:取地址(拿到内存地址)2. *:解引用(拿到值) 四、空指针&am…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...
scikit-learn机器学习
# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
免费数学几何作图web平台
光锐软件免费数学工具,maths,数学制图,数学作图,几何作图,几何,AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...
Rust 开发环境搭建
环境搭建 1、开发工具RustRover 或者vs code 2、Cygwin64 安装 https://cygwin.com/install.html 在工具终端执行: rustup toolchain install stable-x86_64-pc-windows-gnu rustup default stable-x86_64-pc-windows-gnu 2、Hello World fn main() { println…...
从面试角度回答Android中ContentProvider启动原理
Android中ContentProvider原理的面试角度解析,分为已启动和未启动两种场景: 一、ContentProvider已启动的情况 1. 核心流程 触发条件:当其他组件(如Activity、Service)通过ContentR…...
