线程(二)——线程安全
如何理解线程安全:
多线程并发执行的时候,有时候会触发一些“bug”,虽然代码能够执行,线程也在工作,但是过程和结果都不符合我们的开发时的预期,所以我们将此类线程称之为“线程安全问题”。
例如:在多个线程并发的时候,操作系统对多线程的调度特性会导致结果存在偶然性,这个偶然性可能很小,但也不小(eg:假设偶然性为0.1‰,并发的线程有200_000个,那出现的偶然性结果也会有200个,也就是每20w个用户就会影响到200个用户体验,如果是更大体量的那就影响更大了)
代码实例:如图,我们的实例预期结果本来应该是5000+5000 =10000
多次运行结果都不一样,为什么不一样呢? 问题的关键就在于——并发执行会有偶然性,如果是串行执行那么就不会有问题~
进一步来体会并发执行的过程:
从内核的时间轴来看线程代码的执行
通过上述的这些问题,我们再细致的说说线程不安全的原因:
1、内核对线程调度的随机性(非人力能干涉的不可控因素~)
2、当前代码有多个线程对变量进行操作:(变量也可以是硬盘上的数据/网络上的数据)
①多个线程修改同一个变量——>不安全,代码在执行时如果被其他的线程抢占执行,那么结果很有可能就是错的~
②多个线程读取同一个变量——>没事儿~只读不改,就相当于每个线程在内存中拷贝一份这个变量过来~
③多个线程修改不同的变量——>没事儿~你改你的,我改我的,井水不犯河水~
④单个线程修改同一个变量——>没事儿~每改一次就从内存拿出来一次,改完就放回内存去~
3、线程针对变量的修改不是原子性的(如果线程不是抢占式执行,那么没有原子性也没有关系~)
什么是原子性?
所谓“原子性”,就是不可拆分的最小单位,也就是说,当对一个变量的修改是执行一个最小量级的命令——CPU指令,则称这个操作是具有原子性的。
拿上述count++举例,count++这个语句在CPU内核中是分为三步实现:在内存中将count拿出来,在CPU寄存器上进行count+1,将计算结果放回内存。这一句简单的语句需要三个指令来实现,那么对count这个变量的修改就不具备原子性~
可见性:
多个线程在修改同一个变量的时候,能够让其他的线程同时看见这个变量。
JMM里的模拟内存:
每一个线程都有各自的一块工作内存,在线程创造的时候申请分配工作内存,线程销毁的时候释放工作内存。对一个变量进行修改不会直接在主内存内对变量进行修改,而是在主内存中拷贝一份到线程的工作内存中进行修改,然后进行数据更新后再写入主内存~
内存的可见性:
众所周知,每个线程都会申请一块属于自己的工作内存,对于数据的修改,线程总是先从主内存拷贝一份到工作内存,然后在寄存器上进行操作之后再将数据放回内存。
当有多个线程对同一个变量操作时,如何让两个线程的操作都是有效的?这时候就涉及到内存的可见性~
例:现有两个线程,t1线程只有在线程t2通知之后才会停下,而我们利用控制台输入一个非0的整数来控制t2通知t1
static class Counter {public int count = 0;}public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()->{while(counter.count == 0){}System.out.println(Thread.currentThread().getName()+"接到通知,马上停下来...");},"t1");Thread t2 = new Thread(()->{Scanner sc = new Scanner(System.in);System.out.println(Thread.currentThread().getName()+"发出停止通知...");counter.count = sc.nextInt();},"t2");t1.start();t2.start();}
可是结果却是t1没有收到t2的通知
为什么会这样?这是因为t2将修改之后的变量刷新到内存,但是这个结果没有在t1中同步刷新,所以就产生了上述的结果
如何解决这一点?用volatile修饰count即可~
拿上面的例子思考一下,如何避免获得上述这种抢占式执行的结果?
①要想避免这种抢占式执行产生的结果,最好的做法就是给线程上锁
②当两个线程执行过程要对同一个变量进行修改的时候,修改的那段代码可以加上同一个锁,这样就会阻塞其中一个线程让其等待,等锁内的线程执行完工作内容再接着执行另一个线程
synchronized关键字:
对于多个线程针对同一个对象,我们如果想要保证程序的原子性,那么就得给这个对象加一个锁,而synchronized就是干这件事的~
进入synchronized(){}的作用域中表示加锁,执行完作用域中的代码就进行解锁。
如果synchronized针对的是多个对象,那么就不会产生锁竞争,也就不会出现阻塞等待,线程各自干各自的活儿。
public class demo2 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{System.out.println(Thread.currentThread().getName()+"加锁前....");synchronized (locker){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()+"解锁后....");},"线程1");Thread t2 = new Thread(()->{System.out.println(Thread.currentThread().getName()+"加锁前....");synchronized (locker2){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName()+"解锁后....");},"线程2");t1.start();t2.start();t1.join();t1.join();}
}
结果就是两个线程是并发执行的,各干各的,没有产生阻塞等待~
synchronized具有不可抢占性——即如果有人已经持有这把锁,那么在这把锁释放之前其他的线程是拿不到的。
用实例体会一下抢锁的过程:
public class demo3 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()->{synchronized (locker){System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");}},"线程1");Thread t2 = new Thread(()->{synchronized (locker){System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");}},"线程2");Thread t3 = new Thread(()->{synchronized (locker){System.out.println(Thread.currentThread().getName()+"抢到锁进行加锁....");}},"线程3");t1.start();t2.start();t3.start();}}
通过结果,我们可以明白——针对同一个对象进行加锁,谁能先拿到锁是随机的。但是,为什么这里的线程1能一直先拿到锁?这是因为后面t2、t3线程启动需要时间,在这短短的启动时间里,t1可以先获得锁~
在其他的语言中,加锁操作并不是一个synchronized就能搞定,而是线程{ lock(); 其他代码.....; unlock(); }~有时候往往会把unlock()给忘了,这时候就出错了,而synchronized在封装过程中这些都帮我们写好了,我们直接用没有后顾之忧~
wait()和notify():
调用wait()方法干的事:
①让当前线程进行阻塞
②释放当前的锁
③满足一定条件被唤醒,重新尝试获取这个锁(不一定唤醒了就能获取的到,依旧是和其他线程抢占执行)
class WaitTask implements Runnable{private Object locker = new Object();public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker){try {System.out.println("开始阻塞...");locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
class NotifyTask implements Runnable{private Object locker = new Object();public NotifyTask(Object locker){this.locker = locker;}@Overridepublic void run() {synchronized (locker){locker.notify();System.out.println("线程已经被唤醒...");}}
}
public class demo1 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));
/* Thread t2 = new Thread(new WaitTask(locker));Thread t3 = new Thread(new WaitTask(locker));*/Thread t4 = new Thread(new NotifyTask(locker));t1.start();
// t2.start();
// t3.start();Thread.sleep(5000);t4.start();}
}
唤醒线程的方法:①调用该对象的notify()方法;②wait()等待超时;③其他线程调用Interrupted方法,抛出InterruptedExption异常。
notify()方法是唤醒等待中的线程,当有多个线程处于wait()时,由线程调度器随机挑选一个进行唤醒(依旧是没有先到先得的原则)
wait()要搭配synchronized一起使用,不然就会抛异常
当线程调用wait()之后需要调用notify()来唤醒线程,不然就是让程序死等
notify()一次只能唤醒一个线程,而notifyAll()能够一次性唤醒所有线程
一次性唤醒所有等待的线程之后依旧是抢占式执行,依旧有先后执行顺序
区分wait()和sleep():
wait()是用于线程之间的通信,而sleep()只是单纯地让线程阻塞一段时间
1.wait()需要和synchronized搭配使用,而sleep不需要
2.wait()是Object类的方法,而sleep()是Thread类的静态方法
相关文章:
线程(二)——线程安全
如何理解线程安全: 多线程并发执行的时候,有时候会触发一些“bug”,虽然代码能够执行,线程也在工作,但是过程和结果都不符合我们的开发时的预期,所以我们将此类线程称之为“线程安全问题”。 例如ÿ…...
Altium Designer学习笔记 31 PCB布线优化_GND处理
基于Altium Designer 23学习版,四层板智能小车PCB 更多AD学习笔记:Altium Designer学习笔记 1-5 工程创建_元件库创建Altium Designer学习笔记 6-10 异性元件库创建_原理图绘制Altium Designer学习笔记 11-15 原理图的封装 编译 检查 _PCB封装库的创建Al…...
第四节、电机定角度转动【51单片机-TB6600驱动器-步进电机教程】
摘要:本节介绍用电机转动角度计算步骤,从而控制步进电机转角 一、 计算过程 1.1 驱动器接收一个脉冲后,步进电机转动一步,根据驱动器设置的细分值 计算一个脉冲对应电机转动的角度step_x s t e p x s t e p X … … ① step_{x…...
亚马逊云科技用生成式AI,向开发的复杂性动手了
生成式 AI、分布式扩展功能全面进化,还降价了。 同一天的发布,完全不同的方向。 今天凌晨,云计算巨头亚马逊云科技的 re:Invent 与大号创业公司 OpenAI 的发布「撞了车」。后者公布了一系列生成式 AI 应用,价格更贵、性能更强大&a…...
SharpDevelop IDE IViewContent.cs类
文件位置:IViewContent.cs /// <summary>/// IViewContent is the base interface for "windows" in the document area of SharpDevelop./// A view content is a view onto multiple files, or other content that opens like a document/// (e.…...
【工具变量】地级市城市全社会用电量数据(2006-2021年)
一、数据范围:覆盖中国300多个地级市 二、包含指标: 省份、地级市、年份、全社会用电量。 三、数据来源:国家电网查询数据。对于极大部分城市,国网售电量就是全社会用电量(往年的售电量和全社会用电量数据相同),此外…...
vue列表滚动动画效果
一、效果展示: 录屏2024-12-07 02.16.59 二、步骤: 1. 需要用<transition-group>包裹需要渲染的item列表 ⚠️注意:这里的:key"item.id",必须要用id之类的,不能用index <transition-group name&qu…...
33.5 remote实战项目之设计prometheus数据源的结构
本节重点介绍 : 项目要求 通过remote read读取prometheus中的数据通过remote write向prometheus中写入数据 准备工作 新建项目 prome_remote_read_write设计prometheus 数据源的结构初始化 项目要求 通过remote read读取prometheus中的数据通过remote write向prometheus中写…...
Ceph文件存储
Ceph文件存储1.概念:数据以文件的形式存储在存储介质上,每个文件都有一个唯一的文件名并存储在一个目录结构中。提供方便的文件访问接口,支持多种文件操作,如创建、删除、读取、写入、复制等。用于存储和管理个人文件,如文档、图片…...
力扣-图论-5【算法学习day.55】
目录 前言 习题 1.移除可疑的方法 后言 前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向和记录学习过程(例如想要掌握基础用法,该刷哪些题?)我的解析也不会做的非常详细,只会提供思路和一些关键…...
Linux-音频应用编程
ALPHA I.MX6U 开发板支持音频,板上搭载了音频编解码芯片 WM8960,支持播放以及录音功能!本章我们来学习 Linux 下的音频应用编程,音频应用编程相比于前面几个章节所介绍的内容、其难度有所上升,但是笔者仅向大家介绍 Li…...
SQL复杂查询功能介绍及示例
文章目录 1. 多表连接(JOIN)功能介绍应用场景示例查询及初始表格customers 表(未查询前)orders 表(未查询前)INNER JOIN 示例LEFT JOIN 示例 2. 子查询(Subquery)功能介绍应用场景示…...
Python使用Selenium自动实现表单填写之蛇年纪念币蛇钞预约(附源码,源码有注释解析,已测试可用
Python实现纪念币预约自动填写表单 声明:本文只做技术交流,不可用代码为商业用途,文末有源码下载,已测试可用。 Part 1 配置文件改写(源码 有详细的注释说明 读取配置文件,自己组数据库,录入信息 配置文件 Part 2 主函数 每一期的xpath路径都不一样 所以需要提前去网站…...
快速掌握HTML
目录 1. HTML文件的基本结构* 前端开发工具搭建* vscode的三个插件* 编写第一个html代码* 快速生成代码框架 *html特殊字符2. 双标签2.1 标题标签 h12.2 段落标签 p2.3 格式化标签2.4 超链接标签 a2.5 表格标签2.6 列表标签1. 无序列表:ul标签( 快捷键:u…...
Linux 音频驱动实验
音频是我们最常用到的功能,音频也是 linux 和安卓的重点应用场合。I.MX6ULL 带有 SAI接口,正点原子的 I.MX6ULL ALPHA 开发板通过此接口外接了一个 WM8960 音频 DAC 芯片,本章我们就来学习一下如何使能 WM8960 驱动,并且通过 WM89…...
【分布式】Redis分布式缓存
一、什么是Redis分布式缓存 Redis分布式缓存是指使用Redis作为缓存系统来存储和管理数据的分布式方案。在分布式系统中,多台服务器共同对外提供服务,为了提高系统的性能和可扩展性,通常会引入缓存来减轻数据库的压力。Redis作为一种高性能的…...
几个Linux系统安装体验: 一些系统对比和使用记录
本文对一些系统做对比,并记录一些使用过程。 背景 本文所记录的内容,不保证绝对性,也不代表任何方的意见、意思、看法、观点。如有不适,权当笑料。 个人使用记录 centos7 centos7为笔者的主力系统,生产工具。虚拟机…...
c++数据结构算法复习基础--11--高级排序算法-快速排序-归并排序-堆排序
高阶排序 1、快速排序 冒泡排序的升级算法 每次选择一个基准数,把小于基准数的放到基准数的左边,把大于基准数的放到基准数的右边,采用 “ 分治算法 ”处理剩余元素,直到整个序列变为有序序列。 最好和平均的复杂度:…...
特朗普画像
任务内容 Description 特朗普当选了,网上流传着很多段子,也出了特朗普的头像。有人说,特朗普 的头像像一团云。所以今年马云去了美国和特朗普谈中美企业的发展。那么你能帮 忙打印出特朗普的头像吗? 抽象派认为,特朗普…...
torch如何产生3d随机变形场(DVFs)
随机变形场(Deformation Vector Fields, DVFs)是一种在图像处理和计算机视觉中常用的技术,用于生成变形后的图像或增强数据集的多样性。它通过创建一个在空间中定义的位移场,以实现图像的随机变形。以下是生成随机DVFs的主要步骤和方法: 1. 随机噪声生成 随机变形场的基…...
【PlantUML系列】用例图(三)
目录 一、组成部分 二、典型案例 一、组成部分 参与者(Actors):使用关键字 actor 后跟参与者的名称。用例(Use Cases):使用关键字 usecase 后跟用例的名称和编号(可选)。系统边界…...
PHP使用RabbitMQ(正常连接与开启SSL验证后的连接)
代码中包含了PHP在一般情况下使用方法和RabbitMQ开启了SSL验证后的使用方法(我这边消费队列是使用接口请求的方式,每次只从中取出一条) 安装amqp扩展 PHP使用RabbitMQ前,需要安装amqp扩展,之前文章中介绍了Windows环…...
距离与AoA辅助的三维测距算法(适用于四个基站的情况的单点定位),MATLAB代码
本MATLAB 代码实现了一个基于LOS/NLOS混合环境的单点定位系统,主要用于估计目标物体的单点位 文章目录 代码运行结果源代码代码功能概述主要步骤分析初始化部分 绘图与输出 代码运行结果 定位结果如下: 命令行的坐标和误差输出: 部分代码…...
计算机网络原理之HTTP与HTTPS
一、前言 为了理解HTTP,我们有必要事先了解一下TCP/IP协议簇。 通常我们使用的网络(包括互联网)是在TCP/IP协议簇的基础上运作的。而HTTP属于它内部的一个子集。 计算机与网络设备要相互通信,双方必须基于相同的方法。比如&#…...
使用did包进行多期DID分析
本例中,样例数据来自 Callaway 和 Sant’Anna (2020),研究问题是各州提高最低工资对县级青少年就业率影响。 样例数据集包含 2003 年至 2007 年 500 个县级青少年就业率的数据,其中一些州在 2004 年首次接受治疗,也有一些在 2006…...
Windows宝塔面板下IIS环境如何部署SSL证书?
Windows宝塔面板下IIS环境如何部署SSL证书? 平时服务器linux宝塔用的较多,所以linux系统宝塔,如何部署SSL证书还是比较熟悉,今天遇到一个windows的部署SSL证书,还是头一次,所以记录一下,以防忘…...
【MySQL】函数
函数 1.日期函数2.字符串函数3.数学函数4.其他函数 点赞???收藏???关注??? 你的支持是对我最大的鼓励,我们一起努力吧??? 在mysql中其实内置了很多的函数操作,这些函数可以让我们在数据统计的时候以及查表的时候进行各自各样的操作。 1.日…...
面试复盘 part 02·1202-1207 日
作品集讲述部分 分析反思 作品集讲述部分,视觉讲述部分需要更换,需要换成其他视觉相关的修改 具体话术 这是一个信息展示优化方案,用户为财务,信息区分度不足,理解成本较高,因此选择需要降低理解成本。…...
RISC-V 汇编语言
安装RISCV工具链 riscv-gnu-toolchain工具链和模拟器安装记录 - 知乎 (zhihu.com) riscv-gnu-toolchain工具链分elf-gcc、linux-gnu-gcc两个版本,以及对应的32位和64位版本。两个版本的主要区别是: riscv32-unknown-elf-gcc、riscv64-unknown-elf-gcc…...
MySQL Explain 指南
MySQL Explain 指南 idselect_typetablepartitionstypepossible_keyskeykeylenrefrowsfilteredExtra 使用 explain 执行 DML 语句时,数据不会发生变化。explain 的结果可能包含多行数据,每行对应一个表。若涉及 union 操作,MySQL 会创建临时表…...
keil报错---connection refused due to device mismatch
解决办法如下: 记得改成1 把Enable取消...
ubuntu下的chattts 学习4:Advanced Usage
源码 import ChatTTS import torch import torchaudiochat ChatTTS.Chat() chat.load(compileFalse) # Set to True for better performance ################################### # Sample a speaker from Gaussian.rand_spk chat.sample_random_speaker() print(rand_spk)…...
Ubuntu桌面突然卡住,图形界面无反应
1.可能等待几分钟,系统会自动反应过来。你可以选择等待几分钟。 2.绝大多数情况系统是不会反应过来的,这时候可以进入tty终端直接注销用户。 (1)Ubuntu有6个tty终端,按住CtrlAltF1可以进入tty1终端,(同理CtrlAltF2&a…...
毕设记录_论文阅读(动磁式音圈电机的开发与应用)_20241207
前言 提醒: 文章内容为方便作者自己后日复习与查阅而进行的书写与发布,其中引用内容都会使用链接表明出处(如有侵权问题,请及时联系)。 其中内容多为一次书写,缺少检查与订正,如有问题或其他拓展…...
我有一个Python项目,已经用docker打包镜像也push了,k8s怎么部署呢?
要在Kubernetes (k8s) 部署你的Python项目,你需要创建一系列的Kubernetes资源定义文件(通常是以.yaml为扩展名),这些文件描述了你希望在集群中运行的应用程序。以下是部署的基本步骤: 1. **准备Docker镜像**࿱…...
GAN(生成对抗网络)原理与目标函数
GAN(生成对抗网络)原理与目标函数 什么是 GAN? GAN 是一种生成模型,全名是 生成对抗网络 (Generative Adversarial Network)。它由两个部分组成: 生成器 (Generator, G):负责生成“假数据”。判别器 (Di…...
[Java]项目入门
这篇简单介绍一些入门的有关项目和行业的知识,并带着实现一个小项目。便于已经编程入门的各位准备进阶到下一个阶段。 先大致地介绍,一个完整的项目(不看客户端、服务端的分类)基本可以划分为三部分: 1.前端。比如你现在看到的CSDN页面就是一…...
自定义指令,全局,局部,注册
让输入框自动获取焦点(每次刷新自动获取焦点) <template><div><h3>自定义指令</h3><input ref"inp" type"text"></div> </template><script> export default {mounted(){this.$refs.inp.focus…...
存储类内存,非易失性内存)的升级换代,将有利于促进【PCIe交换芯片】市场的发展
摘要 根据 HengCe(恒策咨询)的统计及预测,2023年全球PCIe交换芯片市场销售额达到了10.05亿美元,预计2030年将达到23.81亿美元,年复合增长率(CAGR)为12.5%(2024-2030)。地…...
泷羽sec-burp(7)
免责声明 学习视频来自 B 站up主泷羽sec,如涉及侵权马上删除文章。 笔记的只是方便各位师傅学习知识,以下代码、网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负。 泷羽sec官网:http…...
OpenCV图像处理——二值化原理与代码实现(C++/Python)
概述 在 OpenCV 中,二值化(Binarization)是一种图像处理操作,它的目的是将一幅灰度图像转换为仅包含两种像素值(通常为 0 和 255,分别代表黑色和白色)的二值图像。通过设定一个合适的阈值&…...
Scala 高阶模式案例解析:从入门到实战
引言 Scala 作为一种功能强大的多范式编程语言,因其函数式编程特性而广受欢迎。其中,高阶模式(High-Order Patterns)是 Scala 函数式编程的核心概念之一,为开发者提供了解决复杂问题的优雅方式。本篇文章将全面解析 S…...
30天学会Go--第8天 GO语言 Gin Web框架学习与实践
30天学会Go–第8天 GO语言 Gin Web框架学习与实践 文章目录 30天学会Go--第8天 GO语言 Gin Web框架学习与实践前言一、Gin 的简介与安装1.1 Gin 的特点1.2 安装 Gin 二、Gin 的基础用法2.1 路由2.1.1 基本路由2.1.2 路由参数2.1.3 查询参数2.1.4 路由分组 2.2 中间件2.2.1 使用…...
用R(语言)学R-Learning R,In R
一、安装swirl包 在R语言控制面板,对话框输入以下命令: swirl 是一个非常有用的 R 包,它允许你通过交互式教程来学习 R 语言。以下是使用 swirl 包的基本步骤: 安装 swirl 包:首先,你需要在 R 中安装 swi…...
【银河麒麟操作系统运维】某平台多台虚拟机异常重启分析及处理
了解更多银河麒麟操作系统全新产品,请点击访问 麒麟软件产品专区:https://product.kylinos.cn 开发者专区:https://developer.kylinos.cn 文档中心:https://documentkylinos.cn 现象描述 某虚拟化平台多台虚拟机于凌晨触发异常…...
线性代数中的谱分解
一、谱分解的基本原理 谱分解(Spectral Decomposition)是线性代数中的一个重要概念,特别是在研究矩阵的特征值和特征向量时。它指的是将一个矩阵分解为其特征值和特征向量的组合,从而简化矩阵的运算和分析。谱分解通常适用于对称…...
synchronized(juc)
目录 线程一:interrupt设计模式两阶段终止模式interrupt打断park线程 二:守护线程三:线程状态五个状态(从操作系统角度来说)六种状态(从java API的角度) 共享模型之管程一:上下文切换的安全问题临界区和竟态条件 二:synchronized解决安全问题…...
HTML Input 文件上传功能全解析:从基础到优化
🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…...
深入理解Spring事务
目录 什么是Spring事务为什么需要Spring事务Spring事务的实现 Spring事务的传播机制Spring事务的底层原理 EnableTransactionManagement --开启Spring管理事务Import(TransactionManagementConfigurationSelector.class) --提供两个beanAutoProxyRegistrar --启用AOP的功能&am…...
React学习笔记2-初识React
这篇七个点:1 环境搭建, 2 JSX, 3 组件,4 数据流,5 生命周期,6 React与DOM,7 实例 1 环境搭建 1.1 引用React CDN <!DOCTYPE html> <html lang"en"> <head><met…...