当前位置: 首页 > news >正文

JavaWeb12-线程通讯(线程等待和唤醒)

目录

1.方法介绍

1.1.wait()/wait(long timeout):让当前线程进入等待状态。

1.1.1.wait执行流程

1.1.2.wait结束等待的条件

1.1.3.wait() VS wait(long timeout)

1.1.4.为什么wait要放在Object中?

--->PS:wait(0) 和 sleep(0) 的区别

--->PS:wait 和 sleep 释放锁行为的区别

--->PS:(常见面试题)wait 和 sleep 的区别(小结)

--->PS:3种方法让线程进入休眠/等待状态

1.2.notify():唤醒当前对象上一个休眠的线程(随机)。

1.3.notifyAll():唤醒当前对象上的所有线程。

2.注意事项:


由于线程之间是抢占式执⾏的,因此线程之间执⾏的先后顺序难以预知。但是实际开发中有时候希望合理地协调多个线程之间的执⾏先后顺序。

球场上的每个运动员都是独⽴的 "执⾏流",可以认为是⼀个 "线程"。⽽完成⼀个具体的进攻得分动作,则需要多个运动员相互配合, 按照⼀定的顺序执⾏⼀定的动作,线程1 先 "传球",线程2 才能 "扣篮"。

1.方法介绍

完成这个协调⼯作(线程通讯),主要涉及到以下三个⽅法。注意这三个方法都是对象级别的(需要通过"对象."来调用),【不是锁级别,如Thread.sleep()】都是Object类的内置方法。

1.1.wait()/wait(long timeout):让当前线程进入等待状态。

1.1.1.wait执行流程

  1. 使当前执⾏代码的线程进⾏等待。(把线程放到等待队列中)。
  2. 释放当前的锁。
  3. 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁。

1.1.2.wait结束等待的条件

  1. 其他线程调⽤该对象的 notify ⽅法。wait/notify 唤醒顺序是无序的。
  2. wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间)。
  3. 其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常。

/*** wait使用*/
public class WaitDemo {public static void main(String[] args) {Object lock = new Object();Thread t1 = new Thread(() -> {System.out.println("线程1开始执行");try {synchronized (lock) {System.out.println("线程1调用wait方法....");//无限期等待状态lock.wait();}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1执行完成");},"线程1");t1.start();}
}

import java.util.concurrent.TimeUnit;public class WaitSleepDemo7 {public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread t1 = new Thread(() -> {synchronized (lock){System.out.println("线程1:开始执行");try{lock.wait(0);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1:结束执行");}System.out.println("线程1:终止执行");},"wait0");t1.start();TimeUnit.SECONDS.sleep(1);System.out.println("执行线程1的终止方法");t1.interrupt();}
}

1.1.3.wait() VS wait(long timeout)

1-不同点:wait(long timeout):当线程超过了设置的时间之后,自动恢复执行;而wait():无限等待状态。(0表示无限等待状态)

2-不同点:wait(long timeout):线程会进入WAITING状态;而wait():线程会进入TIMED_WAITING状态。

import java.time.LocalDateTime;public class WaitDemo4 {public static void main(String[] args) {Object lock = new Object();Object lock2 = new Object();new Thread(() -> {System.out.println("线程1:开始执行");synchronized (lock){try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1:执行完成");}},"无参wait线程").start();new Thread(() -> {synchronized (lock2){System.out.println("线程2:开始执行 |" + LocalDateTime.now());try {lock2.wait(60 * 60 * 60 * 1000); //1h} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程2:执行完成 |" + LocalDateTime.now());}},"有参wait线程").start();}
}

 

3-共同点:无论是wait(long timeout)还是wait(),都会使当前线程进入休眠状态。

4-共同点:无论是wait(long timeout)还是wait(),都能使用notify/notifyAll进行唤醒。

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;public class WaitDemo6 {public static void main(String[] args) {Object lock = new Object();new Thread(() -> {System.out.println("线程1:开始执行");synchronized (lock){try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1:执行完成");}},"无参wait线程").start();new Thread(() -> {synchronized (lock){System.out.println("线程2:开始执行 |" + LocalDateTime.now());try {lock.wait(60 * 60 * 60 * 1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程2:执行完成 |" + LocalDateTime.now());}},"有参wait线程").start();new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock){System.out.println("唤醒所有线程");lock.notifyAll();}}).start();}
}

1.1.4.为什么wait要放在Object中?

wait 使用要加锁,也就是要操作锁,锁是针对对象级别的而非线程级别的,线程和对象是⼀对多,所以 wait 最便利的方式是放在 Object 中。

wait(num)和sleep(num):当num>0时,二者执行效果一样。

--->PS:wait(0) 和 sleep(0) 的区别

  1. wait(0) 表示无期限地等待,直到有线程唤醒它为止。
  2. Thread.sleep(0) 调用后会让出CPU执行权,让线程稍做休眠,然后重新调度,重新触发一次 CPU 竞争,不管是否竞争到CPU执行权,都会继续执行,直到执行结束。(类似于Thread.yield())
/*** wait(0)和sleep(0)*/
public class WaitSleepDemo7 {public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread t1 = new Thread(() -> {synchronized (lock){System.out.println("线程1:开始执行");try{lock.wait(0);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1:执行结束");}},"wait(0)");t1.start();Thread t2 = new Thread(() -> {System.out.println("线程2:开始执行");try {Thread.sleep(0);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程2:执行结束");},"sleep(0)");t2.start();}
}

--->PS:wait 和 sleep 释放锁行为的区别

wait和sleep在有锁的情况下,锁的处理行为是完全不同的:

  1. wait方法(不管是有参还是无参)在执行时都会释放锁。
  2. sleep方法不会释放锁。
import java.util.concurrent.TimeUnit;/*** wait和sleep释放锁行为的区别*/
public class WaitSleepDemo8 {public static void main(String[] args) throws InterruptedException {Object lock = new Object();Object lock2 = new Object();Thread t1 = new Thread(() -> {synchronized (lock){System.out.println("线程1:开始执行");try{lock.wait(3 * 1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1:结束执行");}},"wait");t1.start();Thread t2 = new Thread(() -> {synchronized (lock2) {System.out.println("线程2:开始执行");try {Thread.sleep(3 * 1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程2:结束执行");}},"sleep");t2.start();//创建2个线程,先让线程休眠1秒之后,尝试获取锁,看能不能获取到锁//如果可以获取到锁,说明休眠时线程是释放锁的,而如果获取不到锁,说明是不释放锁的Thread t3 = new Thread(() -> {try{TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("尝试获取wait方法的锁");synchronized (lock){System.out.println("成功获取wait的锁");}},"wait2");t3.start();Thread t4 = new Thread(() -> {try{TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("尝试获取sleep方法的锁");synchronized (lock2){System.out.println("成功获取sleep的锁");}},"sleep2");t4.start();}
}

为什么wait释放锁而sleep不释放锁?

JVM 强制语法检查,wait 方法默认等待无期限。

--->PS:(常见面试题)wait 和 sleep 的区别(小结)

二者的相同点:

  1. 都可以让线程休眠。
  2. 都可以响应 interrupt 中断的请求。
  3. wait(num) 和 sleep(num):当num>0时,二者执行效果一样。

二者的不同点:

  1. wait 必须在 synchronized 中使用;而 sleep 却不用。
  2. wait 是 Object 的方法(属于对象级别);而sleep 是 Thread 的方法(属于线程级别)。
  3. wait 释放锁;而sleep 不释放锁。
  4. wait 有可能无限期地等待下去;而sleep 有明确的终止等待时间。 即:wait 可传参,也可不传参;而 sleep必须要传递一个数值类型的参数,否则会报错。
  5. wait 和 sleep 产生的线程状态是不同的:无参的 wait 是 WAITING 状态;而sleep 是 TIMED_WAITING 状态。
  6. 一般情况下(响应 interrupt 除过),wait 可以接收一个 notify/notifyAll 之后就继续执行;而 sleep 只能等待超过时间之后再恢复执行。
  7. wait(0) 表示无期限地等待;而sleep(0)表示重新触发一次 CPU 竞争。

--->PS:3种方法让线程进入休眠/等待状态

  1. sleep(传参设置休眠时间;不可唤醒)
  2. TimeUnit(传参设置休眠时间;不可唤醒)
  3. wait(可传参设置休眠时间,也可不传参无限等待;可以唤醒)

1.2.notify():唤醒当前对象上一个休眠的线程(随机)。

  • ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

  • 如果有多个线程等待,则由线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到") (官方是随机的,但具体实现针对不同的JVM是不一样的)

/*** notify使用*/
public class WaitDemo2 {public static void main(String[] args) {Object lock = new Object();Object lock2 = new Object();Thread t1 = new Thread(() -> {System.out.println("线程1开始执行");try {synchronized (lock) {System.out.println("线程1调用wait方法....");//无限期的等待状态lock.wait();}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1执行完成");},"线程1");Thread t2 = new Thread(() -> {System.out.println("线程2开始执行");try {synchronized (lock) {System.out.println("线程2调用wait方法....");//无限期的等待状态lock.wait();}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程2执行完成");},"线程2");Thread t3 = new Thread(() -> {System.out.println("线程3开始执行");try {synchronized (lock2) {System.out.println("线程3调用wait方法....");//无限期的等待状态lock2.wait();}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程3执行完成");},"线程3");t1.start();t2.start();t3.start();//唤醒lock对象上休眠的线程(随机唤醒一个)Thread t4 = new Thread(() -> {try {Thread.sleep(1500);} catch (InterruptedException e) {}System.out.println("线程4:开始执行,唤醒线程");synchronized (lock) {//发出唤醒通知lock.notify();System.out.println("线程4:执行了唤醒操作");try{Thread.sleep(2000);} catch (InterruptedException e) {}System.out.println("线程4:synchronized执行完了");}}, "线程4");t4.start();}
}

1.3.notifyAll():唤醒当前对象上的所有线程。

/*** notifyAll使用*/
public class WaitDemo3 {public static void main(String[] args) {Object lock = new Object();Object lock2 = new Object();Thread t1 = new Thread(() -> {System.out.println("线程1开始执行");try {synchronized (lock) {System.out.println("线程1调用wait方法....");//无限期的等待状态lock.wait();System.out.println("线程1:恢复执行之后又进入休眠状态");Thread.sleep(2000);System.out.println("线程1执行完成");}} catch (InterruptedException e) {e.printStackTrace();}},"线程1");Thread t2 = new Thread(() -> {System.out.println("线程2开始执行");try {synchronized (lock2) {System.out.println("线程2调用wait方法....");//无限期的等待状态lock2.wait();System.out.println("线程2:恢复执行之后又进入休眠状态");Thread.sleep(2000);System.out.println("线程2执行完成");}} catch (InterruptedException e) {e.printStackTrace();}},"线程2");Thread t3 = new Thread(() -> {System.out.println("线程3开始执行");try {synchronized (lock) {System.out.println("线程3调用wait方法....");//无限期的等待状态lock.wait();System.out.println("线程3:恢复执行之后又进入休眠状态");Thread.sleep(2000);System.out.println("线程3执行完成");}} catch (InterruptedException e) {e.printStackTrace();}},"线程3");t1.start();t2.start();t3.start();//唤醒lock对象上休眠的线程(随机唤醒一个)Thread t4 = new Thread(() -> {try {Thread.sleep(500);} catch (InterruptedException e) {}System.out.println("线程4:开始执行,唤醒线程");synchronized (lock){//发出唤醒通知lock.notifyAll();System.out.println("线程4:执行了唤醒操作");try{Thread.sleep(2000);} catch (InterruptedException e) {}System.out.println("线程4:synchronized执行完了");}}, "线程4");t4.start();}
}

2.注意事项:

①wait/notify/notifyAll必须要配合synchronized一起使用,否则会报错(JVM的强制规定,之所以这样规定,是为了解决线程通讯时执行混乱的问题,synchronized起到约束限制的作用)。

②wait/notify/notifyAll进行synchronized加锁,一定要使用同一个对象进行加锁。

③当调用了notify/notifyAll之后,当前线程不会⻢上释放该对象锁,要等到执⾏notify/notifyAll⽅法的线程将程序执⾏完,也就是退出同步代码块(synchronized)之后才会释放对象锁。【程序也并不会立即恢复执行,而是尝试获取锁,只有得到锁之后才能继续执行。】

④notify/notifyAll可以多次调用,也可以在wait之前调用(是无用功)。

⑤notifyAll 并不是唤醒所有 wait 等待的线程,而是唤醒当前对象处于 wait 等待的所有线程。

相关文章:

JavaWeb12-线程通讯(线程等待和唤醒)

目录 1.方法介绍 1.1.wait()/wait(long timeout):让当前线程进入等待状态。 1.1.1.wait执行流程 1.1.2.wait结束等待的条件 1.1.3.wait() VS wait(long timeout) 1.1.4.为什么wait要放在Object中? --->PS:wait(0) 和 sleep(0) 的区…...

江苏专转本如何事半功倍的备考

专转本如何事半功倍的备考 一个人学习成绩的优劣取决于他的学习能力,学习能力包括三个要素:规范的学习行为;良好的学习习惯;有效的学习方法。有了规范的学习行为才能培养出良好的学习习惯,形成了良好的学习习惯就会形成…...

linux下安装mongoDB

一、下载mongoDB包 下载地址: https://www.mongodb.com/try/download/community 个人建议:如果是学习阶段,使用5以下版本更好些。 二、安装及配置 1、安装 # 1、解压 $ tar -zxvf mongodb-linux-x86_64-rhel70-4.4.19-rc1.tgz# 2、迁移目…...

掌握MySQL分库分表(七)广播表、绑定表实战,水平分库+分表实现及之后的查询和删除操作

文章目录什么是广播表广播表实战数据库配置表Java配置实体类配置文件测试广播表水平分库分表配置文件运行测试什么是绑定表?绑定表实战配置数据库配置Java实体类配置文件运行测试水平分库分表后的查询和删除操作查询操作什么是广播表 指所有的分片数据源中都存在的…...

企业为什么需要数据可视化报表

数据可视化报表是在商业环境、市场环境已经改变之后,发展出来为当前企业提供替代解决办法的重要方案。而且信息化、数字化时代,很多企业已经进行了初步的信息化建设,沉淀了大量业务数据,这些数据作为企业的资产,是需要…...

5个有效的华为(HUAWEI)手机数据恢复方法

5个有效的手机数据恢复方法 华为智能手机中的数据丢失比许多人认为的更为普遍。发生这种类型的丢失有多种不同的原因,因此数据恢复软件的重要性。您永远不知道您的智能手机何时会在这方面垮台;因此,预防总比哀叹好,这就是为什么众…...

【Java并发编程】线程安全(一)Synchronized原理

Synchronized底层实现 简单来说,Synchronized关键字的执行主体是线程对象,加锁是通过一个锁对象来完成的是,而锁对象底层关联了一个c源码的monitor的对象,monitor对象底层又对应了操作系统级别的互斥锁,同一时刻只有一…...

[apollo]vue3.x中apollo的使用

[apollo]vue3.x中apollo的使用通过客户端获取Apollo配置环境工具的安装获取Apollo配置相关代码错误提示Uncaught (in promise) Error: Apollo client with id default not found. Use provideApolloClient() if you are outside of a component setup通过开放接口获取Apollo配置…...

system()函数启用新进程占有原进程的文件描述符表的问题

我在A程序中占用了/dev/video0这个独占模式的设备文件,在A中用system函数启用了B程序,B程序的代码中并不包含对/dev/video0的访问,但是我发现B程序也占用了/dev/video0,并且我在A程序中关闭了/dev/video0后,A程序不再占…...

nignx(安装,正反代理,安装tomcat设置反向代理,ip透传)

1安装nginx 安装wget Yum install -y wget 下载(链接从官网找到右键获取) 以下过程root 安装gcc Yum -y install gcc c 安装pcre Yum install -y pcre pcre-devel Openssl Yum install -y openssl openssl-devel 安装zlib Yum install -y zlib zlib-devel 安装make Yum inst…...

sklearn模块常用内容解析笔记

文章目录 回归模型评价指标R2_score预备知识R2_score计算公式r2_score使用方法注意事项参考文献回归模型评价指标R2_score 回归模型的性能的评价指标主要有:RMSE(平方根误差)、MAE(平均绝对误差)、MSE(平均平方误差)、R2_score。但是当量纲不同时,RMSE、MAE、MSE难以衡量模…...

我的 System Verilog 学习记录(2)

引言 从本文开始,就开始系统学习 System Verilog ,不只是语法,还有结合 Questa Sim 的实际编程练习、Debug。 本文简单介绍 System Verilog 语言的用途以及学习的必要性。 前文链接: 我的 System Verilog 学习记录&#xff08…...

【调研报告】Monorepo 和 Multirepo 的风格对比及使用示例

带有权重的Monorepo和Multirepo对比 功能/特性MonorepoMultirepoMonorepo权重值Multirepo权重值代码管理管理多个代码库更加复杂管理单个代码库更加简单37依赖管理可以简化依赖管理依赖冲突可能会更加困难73构建和部署构建和部署更加容易构建和部署可能需要更多的配置82团队协…...

Retrofit源码分析

文章目录一、简介二、源码分析2.1Retrofit的本质流程2.2源码分析2.2.1 创建Retrofit实例步骤1步骤2步骤3步骤4步骤5总结2.2.2创建网络请求接口的实例外观模式 & 代理模式1.外观模式2. 代理模式步骤3步骤4总结2.2.3执行网络请求同步请求OkHttpCall.execute()1.发送请求过程2…...

Mybatis-Plus入门系列(20) -兼容多种数据库

有道无术,术尚可求,有术无道,止于术。 文章目录前言方案分析1. 分页2. XML自定义SQL案例演示1. 配置2. 简单分页查询3. 带方言的分页查询参考前言 在我们实际开发软件产品过程中,数据库的类型可能不是确定的,也有客户…...

JetPack板块—Android X解析

Android Jetpack简述 AndroidX 是Android团队用于在Jetpack中开发,测试,打包,发布和版本管理的开源项目。相比于原来的Android Support库,AndroidX 可以称得上是一次重大的升级改进。 和Support库一样,AndroidX与Android 操作系…...

C++学习笔记-数字

当我们使用数字时,通常我们使用原始数据类型,例如 int,short,long,float 和 double 等。数字数据类型,它们的可能值和取值范围在讨论 C 数据类型时已经解释了。 C 定义数字 我们已经在之前笔记的各种实例…...

Nginx——Nginx的基础原理

摘要 Nginx 是俄罗斯人编写的十分轻量级的 HTTP 服务器,是一个高性能的HTTP和反向代理服务器,同时也是一个 IMAP/POP3/SMTP 代理服务器。Nginx 是由俄罗斯人 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,它已经在该站点运行超过两年半了。…...

服务端开发Java之备战秋招面试篇1

在这个面试造火箭工作拧螺丝的时代背景下,感觉不是很好,不过还好也是拿到了还行的offer,准备去实习了,接下来就是边实习边准备秋招了,这半年把(技术栈八股文面经算法题项目)吃透,希望…...

【C++的OpenCV】第三课-OpenCV图像加载和显示

我们开始学习OpenCV一、OpenCV加载图片和显示图片1.1 imread()函数的介绍1.2 cv::namedWindow()函数的介绍1.4 imshow()函数介绍1.5 Mat容器介绍二、 代码实例(带注释)2.1 代码2.2 执行结果一、OpenCV加载图片和显示图片 本章节中,将会学习到…...

利用ngx_stream_return_module构建简易 TCP/UDP 响应网关

一、模块概述 ngx_stream_return_module 提供了一个极简的指令&#xff1a; return <value>;在收到客户端连接后&#xff0c;立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量&#xff08;如 $time_iso8601、$remote_addr 等&#xff09;&a…...

从零实现富文本编辑器#5-编辑器选区模型的状态结构表达

先前我们总结了浏览器选区模型的交互策略&#xff0c;并且实现了基本的选区操作&#xff0c;还调研了自绘选区的实现。那么相对的&#xff0c;我们还需要设计编辑器的选区表达&#xff0c;也可以称为模型选区。编辑器中应用变更时的操作范围&#xff0c;就是以模型选区为基准来…...

visual studio 2022更改主题为深色

visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中&#xff0c;选择 环境 -> 常规 &#xff0c;将其中的颜色主题改成深色 点击确定&#xff0c;更改完成...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)

文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...

SQL慢可能是触发了ring buffer

简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...

【SpringBoot自动化部署】

SpringBoot自动化部署方法 使用Jenkins进行持续集成与部署 Jenkins是最常用的自动化部署工具之一&#xff0c;能够实现代码拉取、构建、测试和部署的全流程自动化。 配置Jenkins任务时&#xff0c;需要添加Git仓库地址和凭证&#xff0c;设置构建触发器&#xff08;如GitHub…...

深度剖析 DeepSeek 开源模型部署与应用:策略、权衡与未来走向

在人工智能技术呈指数级发展的当下&#xff0c;大模型已然成为推动各行业变革的核心驱动力。DeepSeek 开源模型以其卓越的性能和灵活的开源特性&#xff0c;吸引了众多企业与开发者的目光。如何高效且合理地部署与运用 DeepSeek 模型&#xff0c;成为释放其巨大潜力的关键所在&…...

Axure Rp 11 安装、汉化、授权

Axure Rp 11 安装、汉化、授权 1、前言2、汉化2.1、汉化文件下载2.2、windows汉化流程2.3、 macOs汉化流程 3、授权 1、前言 Axure Rp 11官方下载链接&#xff1a;https://www.axure.com/downloadthanks 2、汉化 2.1、汉化文件下载 链接: https://pan.baidu.com/s/18Clf…...

HTML版英语学习系统

HTML版英语学习系统 这是一个完全免费、无需安装、功能完整的英语学习工具&#xff0c;使用HTML CSS JavaScript实现。 功能 文本朗读练习 - 输入英文文章&#xff0c;系统朗读帮助练习听力和发音&#xff0c;适合跟读练习&#xff0c;模仿学习&#xff1b;实时词典查询 - 双…...

Electron简介(附电子书学习资料)

一、什么是Electron&#xff1f; Electron 是一个由 GitHub 开发的 开源框架&#xff0c;允许开发者使用 Web技术&#xff08;HTML、CSS、JavaScript&#xff09; 构建跨平台的桌面应用程序&#xff08;Windows、macOS、Linux&#xff09;。它将 Chromium浏览器内核 和 Node.j…...